From 470a3400b86388625a2fa0b9de5ac23af65eb5a2 Mon Sep 17 00:00:00 2001 From: "Robert P. Goldman" Date: Thu, 13 Aug 2020 17:08:44 -0500 Subject: [PATCH 1/2] Choose root Schema. Allow the user to choose a root schema and pull out a self contained json schema .json file for that schema. Adds "--root" argument. --- openapi2jsonschema/command.py | 105 ++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 19 deletions(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index 9cd1bfc..0eeb360 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -1,6 +1,10 @@ #!/usr/bin/env python import json +import re +from copy import deepcopy +from typing import Any, Dict, Optional + import yaml import urllib import os @@ -34,6 +38,12 @@ default="_definitions.json", help="Prefix for JSON references (only for OpenAPI versions before 3.0)", ) +@click.option( + "-r", + "--root", + default=None, + help="Root class to generate schema for. Will generate a standalone JSON schema file for this class.", +) @click.option( "--stand-alone", is_flag=True, help="Whether or not to de-reference JSON schemas" ) @@ -49,7 +59,7 @@ help="Prohibits properties not in the schema (additionalProperties: false)", ) @click.argument("schema", metavar="SCHEMA_URL") -def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): +def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict, root: Optional[str]): """ Converts a valid OpenAPI specification into a set of JSON Schema files """ @@ -71,6 +81,8 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): version = data["swagger"] elif "openapi" in data: version = data["openapi"] + else: + raise ValueError("Unable to determine OpenAPI version.") if not os.path.exists(output): os.makedirs(output) @@ -127,11 +139,11 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): components = data["components"]["schemas"] for title in components: - kind = title.split(".")[-1].lower() + kind = title.split(".")[-1] # .lower() if kubernetes: - group = title.split(".")[-3].lower() - api_version = title.split(".")[-2].lower() - specification = components[title] + group = title.split(".")[-3] # .lower() + api_version = title.split(".")[-2] # .lower() + specification = deepcopy(components[title]) specification["$schema"] = "http://json-schema.org/schema#" specification.setdefault("type", "object") @@ -162,20 +174,20 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): # This list of Kubernetes types carry around jsonschema for Kubernetes and don't # currently work with openapi2jsonschema if ( - kubernetes - and stand_alone - and kind - in [ - "jsonschemaprops", - "jsonschemapropsorarray", - "customresourcevalidation", - "customresourcedefinition", - "customresourcedefinitionspec", - "customresourcedefinitionlist", - "customresourcedefinitionspec", - "jsonschemapropsorstringarray", - "jsonschemapropsorbool", - ] + kubernetes + and stand_alone + and kind + in [ + "jsonschemaprops", + "jsonschemapropsorarray", + "customresourcevalidation", + "customresourcedefinition", + "customresourcedefinitionspec", + "customresourcedefinitionlist", + "customresourcedefinitionspec", + "jsonschemapropsorstringarray", + "jsonschemapropsorbool", + ] ): raise UnsupportedError("%s not currently supported" % kind) @@ -223,6 +235,61 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): ) all_file.write(json.dumps(contents, indent=2)) + if root is not None: + # should fix this naming.... + outfile: str = f"{output}/{root}.json" + if not components[root]: + raise ValueError(f"Unable to find JSON class {outfile}") + contents = components[root] + contents = rewrite_links(contents) + + info(f"Generating standalone schema for {root} type") + contents["definitions"] = {} + contents["$schema"] = "http://json-schema.org/schema#" + + info("Incorporating individual schemas") + + title: str + spec: Dict[str, Any] + for title, spec in components.items(): + if title == root: + continue + + specification: Dict[str, Any] = deepcopy(spec) + specification.setdefault("type", "object") + + debug(f"Merging schema for {title}:") + debug(f"{specification}") + + contents["definitions"][title] = rewrite_links(specification) + + with open(outfile, "w") as root_file: + root_file.write(json.dumps(contents, indent=2)) + + +# Tail-recursive. This is going to be bad. But we can rewrite it later. +def rewrite_links(spec): + def dict_rewrite(dct): + new = {} + for key, value in spec.items(): + if key == "$ref": + matchval = re.match('.*/([^/]+)$', value) + if matchval: + name: str = matchval.group(1) + else: + raise ValueError(f"Unable to extract a class name from {value}") + new[key] = "#/definitions/%s" % name + else: + new[key] = rewrite_links(value) + return new + + if isinstance(spec, dict): + return dict_rewrite(spec) + elif isinstance(spec, list): + return [rewrite_links(x) for x in spec] + else: + return spec + if __name__ == "__main__": default() From 195c55bf89f1540fb840e676c061ecdc6e18ccf5 Mon Sep 17 00:00:00 2001 From: "Robert P. Goldman" Date: Mon, 17 Aug 2020 16:41:59 -0500 Subject: [PATCH 2/2] Don't generate excess files. Previously, when giving the --root argument, you would get all the files o2js would ordinarily generate *and* the requested standalone file for the root. Now we don't generate the unnecessary files. --- openapi2jsonschema/command.py | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index 0eeb360..cf01adf 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -215,27 +215,33 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict, r updated = allow_null_optional_fields(updated) specification["properties"] = updated - with open("%s/%s.json" % (output, full_name), "w") as schema_file: - debug("Generating %s.json" % full_name) - schema_file.write(json.dumps(specification, indent=2)) + # Normal mode of operation -- generate one JSON schema file per schema + # defined in the OpenAPI spec. + if root is None: + with open("%s/%s.json" % (output, full_name), "w") as schema_file: + dbg("Generating %s.json" % full_name) + schema_file.write(json.dumps(specification, indent=2)) except Exception as e: error("An error occured processing %s: %s" % (kind, e)) - with open("%s/all.json" % output, "w") as all_file: - info("Generating schema for all types") - contents = {"oneOf": []} - for title in types: - if version < "3": - contents["oneOf"].append( - {"$ref": "%s#/definitions/%s" % (prefix, title)} - ) - else: - contents["oneOf"].append( - {"$ref": (title.replace("#/components/schemas/", "") + ".json")} - ) - all_file.write(json.dumps(contents, indent=2)) + # unless you are generating a single file for a single JSON schema, + # then also generate an `all.json` file. + if root is None: + with open("%s/all.json" % output, "w") as all_file: + info("Generating schema for all types") + contents = {"oneOf": []} + for title in types: + if version < "3": + contents["oneOf"].append( + {"$ref": "%s#/definitions/%s" % (prefix, title)} + ) + else: + contents["oneOf"].append( + {"$ref": (title.replace("#/components/schemas/", "") + ".json")} + ) + all_file.write(json.dumps(contents, indent=2)) - if root is not None: + else: # should fix this naming.... outfile: str = f"{output}/{root}.json" if not components[root]: