"""JSON Schema output plugin """ # pylint: disable=C0111 from __future__ import print_function import optparse import logging import json from pyang import plugin from pyang import statements from pyang import types from pyang import error def pyang_plugin_init(): plugin.register_plugin(JSONSchemaPlugin()) class JSONSchemaPlugin(plugin.PyangPlugin): def add_output_format(self, fmts): fmts['jsonschema'] = self def add_opts(self, optparser): optlist = [ optparse.make_option('--jsonschema-debug', dest='schema_debug', action="store_true", help='JSON Schema debug'), optparse.make_option('--jsonschema-path', dest='schema_path', help='JSON Schema path'), optparse.make_option('--jsonschema-title', dest='schema_title', help='JSON Schema title'), ] group = optparser.add_option_group("JSON Schema-specific options") group.add_options(optlist) def setup_ctx(self, ctx): ctx.opts.stmts = None def setup_fmt(self, ctx): ctx.implicit_errors = False def emit(self, ctx, modules, fd): root_stmt = modules[0] if ctx.opts.schema_debug: logging.basicConfig(level=logging.DEBUG) print("") if ctx.opts.schema_path is not None: logging.debug("schema_path: %s", ctx.opts.schema_path) path = ctx.opts.schema_path root_stmt = find_stmt_by_path(modules[0], path) else: path = None if ctx.opts.schema_title is not None: schema_title = ctx.opts.schema_title else: schema_title = root_stmt.arg description_str = "Generated by pyang from module %s" % modules[0].arg result = {"title": schema_title, "$schema": "http://json-schema.org/draft-04/schema#", "description": description_str, "type": "object", "properties": {}} schema = produce_schema(root_stmt) result["properties"].update(schema) fd.write(json.dumps(result, indent=2)) def find_stmt_by_path(module, path): logging.debug("in find_stmt_by_path with: %s %s path: %s", module.keyword, module.arg, path) if path is not None: spath = path.split("/") if spath[0] == '': spath = spath[1:] children = [child for child in module.i_children if child.keyword in statements.data_definition_keywords] while spath is not None and len(spath) > 0: match = [child for child in children if child.arg == spath[0] and child.keyword in statements.data_definition_keywords] if len(match) > 0: logging.debug("Match on: %s, path: %s", match[0].arg, spath) spath = spath[1:] children = match[0].i_children logging.debug("Path is now: %s", spath) else: logging.debug("Miss at %s, path: %s", children, spath) raise error.EmitError("Path '%s' does not exist in module" % path) logging.debug("Ended up with %s %s", match[0].keyword, match[0].arg) return match[0] def produce_schema(root_stmt): logging.debug("in produce_schema: %s %s", root_stmt.keyword, root_stmt.arg) result = {} for child in root_stmt.i_children: if child.keyword in statements.data_definition_keywords: if child.keyword in producers: logging.debug("keyword hit on: %s %s", child.keyword, child.arg) add = producers[child.keyword](child) result.update(add) else: logging.debug("keyword miss on: %s %s", child.keyword, child.arg) else: logging.debug("keyword not in data_definition_keywords: %s %s", child.keyword, child.arg) return result def produce_type(type_stmt): logging.debug("In produce_type with: %s %s", type_stmt.keyword, type_stmt.arg) type_id = type_stmt.arg if types.is_base_type(type_id): if type_id in _numeric_type_trans_tbl: type_str = numeric_type_trans(type_id) elif type_id in _other_type_trans_tbl: type_str = other_type_trans(type_id, type_stmt) else: logging.debug("Missing mapping of base type: %s %s", type_stmt.keyword, type_stmt.arg) type_str = {"type": "string"} elif hasattr(type_stmt, "i_typedef") and type_stmt.i_typedef is not None: logging.debug("Found typedef type in: %s %s (typedef) %s", type_stmt.keyword, type_stmt.arg, type_stmt.i_typedef) typedef_type_stmt = type_stmt.i_typedef.search_one('type') typedef_type = produce_type(typedef_type_stmt) type_str = typedef_type else: logging.debug("Missing mapping of: %s %s", type_stmt.keyword, type_stmt.arg, type_stmt.i_typede) type_str = {"type": "string"} return type_str def produce_leaf(stmt): logging.debug("in produce_leaf: %s %s", stmt.keyword, stmt.arg) arg = qualify_name(stmt) type_stmt = stmt.search_one('type') type_str = produce_type(type_stmt) return {arg: type_str} def produce_list(stmt): logging.debug("in produce_list: %s %s", stmt.keyword, stmt.arg) arg = qualify_name(stmt) if stmt.parent.keyword != "list": result = {arg: {"type": "array", "items": []}} else: result = {"type": "object", "properties": {arg: {"type": "array", "items": []}}} if hasattr(stmt, 'i_children'): for child in stmt.i_children: if child.keyword in producers: logging.debug("keyword hit on: %s %s", child.keyword, child.arg) if stmt.parent.keyword != "list": result[arg]["items"].append(producers[child.keyword](child)) else: result["properties"][arg]["items"].append(producers[child.keyword](child)) else: logging.debug("keyword miss on: %s %s", child.keyword, child.arg) logging.debug("In produce_list for %s, returning %s", stmt.arg, result) return result def produce_leaf_list(stmt): logging.debug("in produce_leaf_list: %s %s", stmt.keyword, stmt.arg) arg = qualify_name(stmt) type_stmt = stmt.search_one('type') type_id = type_stmt.arg if types.is_base_type(type_id) or type_id in _other_type_trans_tbl: type_str = produce_type(type_stmt) result = {arg: {"type": "array", "items": [type_str]}} else: logging.debug("Missing mapping of base type: %s %s, type: %s", stmt.keyword, stmt.arg, type_id) result = {arg: {"type": "array", "items": [{"type": "string"}]}} return result def produce_container(stmt): logging.debug("in produce_container: %s %s", stmt.keyword, stmt.arg) arg = qualify_name(stmt) if stmt.parent.keyword != "list": result = {arg: {"type": "object", "properties": {}}} else: result = {"type": "object", "properties": {arg:{"type": "object", "properties": {}}}} if hasattr(stmt, 'i_children'): for child in stmt.i_children: if child.keyword in producers: logging.debug("keyword hit on: %s %s", child.keyword, child.arg) if stmt.parent.keyword != "list": result[arg]["properties"].update(producers[child.keyword](child)) else: result["properties"][arg]["properties"].update(producers[child.keyword](child)) else: logging.debug("keyword miss on: %s %s", child.keyword, child.arg) logging.debug("In produce_container, returning %s", result) return result def produce_choice(stmt): logging.debug("in produce_choice: %s %s", stmt.keyword, stmt.arg) result = {} # https://tools.ietf.org/html/rfc6020#section-7.9.2 for case in stmt.search("case"): if hasattr(case, 'i_children'): for child in case.i_children: if child.keyword in producers: logging.debug("keyword hit on (long version): %s %s", child.keyword, child.arg) result.update(producers[child.keyword](child)) else: logging.debug("keyword miss on: %s %s", child.keyword, child.arg) # Short ("case-less") version # https://tools.ietf.org/html/rfc6020#section-7.9.2 for child in stmt.substmts: logging.debug("checking on keywords with: %s %s", child.keyword, child.arg) if child.keyword in ["container", "leaf", "list", "leaf-list"]: logging.debug("keyword hit on (short version): %s %s", child.keyword, child.arg) result.update(producers[child.keyword](child)) logging.debug("In produce_choice, returning %s", result) return result producers = { # "module": produce_module, "container": produce_container, "list": produce_list, "leaf-list": produce_leaf_list, "leaf": produce_leaf, "choice": produce_choice, } _numeric_type_trans_tbl = { # https://tools.ietf.org/html/draft-ietf-netmod-yang-json-02#section-6 "int8": ("number", None), "int16": ("number", None), "int32": ("number", "int32"), "int64": ("integer", "int64"), "uint8": ("number", None), "uint16": ("number", None), "uint32": ("integer", "uint32"), "uint64": ("integer", "uint64") } def numeric_type_trans(dtype): trans_type = _numeric_type_trans_tbl[dtype][0] # Should include format string in return value # tformat = _numeric_type_trans_tbl[dtype][1] return {"type": trans_type} def string_trans(stmt): logging.debug("in string_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"type": "string"} return result def enumeration_trans(stmt): logging.debug("in enumeration_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"properties": {"type": {"enum": []}}} for enum in stmt.search("enum"): result["properties"]["type"]["enum"].append(enum.arg) logging.debug("In enumeration_trans for %s, returning %s", stmt.arg, result) return result def bits_trans(stmt): logging.debug("in bits_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"type": "string"} return result def boolean_trans(stmt): logging.debug("in boolean_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"type": "boolean"} return result def empty_trans(stmt): logging.debug("in empty_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"type": "array", "items": [{"type": "null"}]} # Likely needs more/other work per: # https://tools.ietf.org/html/draft-ietf-netmod-yang-json-10#section-6.9 return result def union_trans(stmt): logging.debug("in union_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"oneOf": []} for member in stmt.search("type"): member_type = produce_type(member) result["oneOf"].append(member_type) return result def instance_identifier_trans(stmt): logging.debug("in instance_identifier_trans with stmt %s %s", stmt.keyword, stmt.arg) result = {"type": "string"} return result def leafref_trans(stmt): logging.debug("in leafref_trans with stmt %s %s", stmt.keyword, stmt.arg) # TODO: Need to resolve i_leafref_ptr here result = {"type": "string"} return result _other_type_trans_tbl = { # https://tools.ietf.org/html/draft-ietf-netmod-yang-json-02#section-6 "string": string_trans, "enumeration": enumeration_trans, "bits": bits_trans, "boolean": boolean_trans, "empty": empty_trans, "union": union_trans, "instance-identifier": instance_identifier_trans, "leafref": leafref_trans } def other_type_trans(dtype, stmt): return _other_type_trans_tbl[dtype](stmt) def qualify_name(stmt): # From: draft-ietf-netmod-yang-json # A namespace-qualified member name MUST be used for all members of a # top-level JSON object, and then also whenever the namespaces of the # data node and its parent node are different. In all other cases, the # simple form of the member name MUST be used. if stmt.parent.parent is None: # We're on top pfx = stmt.i_module.arg logging.debug("In qualify_name with: %s %s on top", stmt.keyword, stmt.arg) return pfx + ":" + stmt.arg if stmt.top.arg != stmt.parent.top.arg: # Parent node is different pfx = stmt.top.arg logging.debug("In qualify_name with: %s %s and parent is different", stmt.keyword, stmt.arg) return pfx + ":" + stmt.arg return stmt.arg