From fc8e5b7e26b8888168b11c9ab89110d0049cfe29 Mon Sep 17 00:00:00 2001 From: Amal Thundiyil Date: Thu, 9 Dec 2021 12:01:33 +0530 Subject: [PATCH] Start of OpenAPI to Hydra Parser v2 - Separated concerns by adding `/parsers` and `/processors` - Parsers are responsible for parsing the text of the OpenAPI Spec and extracting relevant information for creating a `HydraDoc` - Processors then use this information and act as an adapter to the `hydra-python-core` which then convert it to a Hydra Object - These Hydra objects are then used by the `APIClassProcessor` to finally create a relevant Hydra Class and then send it to the `OpenAPIDocParser` which assimilates everything to create a `HydraDoc` --- .gitignore | 3 + .../hydra_openapi_parser_v2/__init__.py | 0 .../hydra_openapi_parser_v2/exceptions.py | 8 + .../hydra_openapi_parser_v2/openapi_parser.py | 51 +++ .../parsers/__init__.py | 0 .../parsers/api_info_parser.py | 37 +++ .../parsers/components_parser.py | 12 + .../parsers/method_parser.py | 64 ++++ .../parsers/param_parser.py | 41 +++ .../parsers/path_parser.py | 46 +++ .../parsers/ref_parser.py | 36 ++ .../parsers/resp_parser.py | 36 ++ .../parsers/schema_parser.py | 90 +++++ .../processors/__init__.py | 0 .../processors/api_class_processor.py | 77 +++++ .../processors/api_info_processor.py | 24 ++ .../processors/class_processor.py | 50 +++ .../processors/collection_processor.py | 33 ++ .../processors/op_processor.py | 39 +++ .../processors/prop_processor.py | 20 ++ .../processors/status_processor.py | 15 + .../hydra_openapi_parser_v2/utils.py | 90 +++++ hydra_openapi_parser/openapi_parser.py | 24 +- samples/v2/hydra_doc_sample.py | 313 ++++++++++++++++++ samples/v2/petstore-expanded.yaml | 157 +++++++++ samples/v2/uspto.yaml | 210 ++++++++++++ 26 files changed, 1468 insertions(+), 8 deletions(-) create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/__init__.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/exceptions.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/openapi_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/__init__.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/api_info_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/components_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/method_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/param_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/path_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/ref_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/resp_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/parsers/schema_parser.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/__init__.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_class_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_info_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/class_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/collection_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/op_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/prop_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/processors/status_processor.py create mode 100644 hydra_openapi_parser/hydra_openapi_parser_v2/utils.py create mode 100644 samples/v2/hydra_doc_sample.py create mode 100644 samples/v2/petstore-expanded.yaml create mode 100644 samples/v2/uspto.yaml diff --git a/.gitignore b/.gitignore index c8d4f69..f30d00f 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,9 @@ ENV/ env.bak/ venv.bak/ +# VScode project settings +.vscode + # Spyder project settings .spyderproject .spyproject diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/__init__.py b/hydra_openapi_parser/hydra_openapi_parser_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/exceptions.py b/hydra_openapi_parser/hydra_openapi_parser_v2/exceptions.py new file mode 100644 index 0000000..bde0584 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/exceptions.py @@ -0,0 +1,8 @@ +class HydraCollectionException(Exception): + def __init__(self, message) -> None: + super().__init__(message) + + +class ExpectsValueError(Exception): + def __init__(self, message) -> None: + super().__init__(message) diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/openapi_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/openapi_parser.py new file mode 100644 index 0000000..3ff5725 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/openapi_parser.py @@ -0,0 +1,51 @@ +import os +import yaml +from processors.api_info_processor import APIInfoProcessor +from processors.api_class_processor import APIClassProcessor +from utils import gen_entrypoint, gen_doc_file +from hydra_python_core.doc_writer import HydraStatus + + +class OpenAPIDocParser: + def __init__(self, inp_path: str) -> None: + # construct the path for the input OpenAPI doc + self.current_dir = os.path.dirname(__file__) + input_file_path = os.path.join(self.current_dir, inp_path) + with open(input_file_path) as stream: + try: + self.openapi_doc = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + + def parse(self, out_path: str) -> str: + # create basic hydra doc with general info (@id, @context, description etc.) + info = APIInfoProcessor(self.openapi_doc) + api_info_doc = info.generate() + api_doc = gen_entrypoint(api_info_doc) + + # create supported classes for hydra doc + api_classes = APIClassProcessor(self.openapi_doc) + supported_classes, supported_collections = api_classes.generate() + for supported_class in supported_classes: + api_doc.add_supported_class(supported_class) + + # create supported collections for hydra doc + for supported_collection in supported_collections: + api_doc.add_supported_collection(supported_collection) + + hydra_doc = api_doc.generate() + + if out_path: + # construct the path for the output Hydra doc + output_file_path = os.path.join(self.current_dir, out_path) + with open(output_file_path, "w") as f: + f.write(gen_doc_file(hydra_doc)) + + return hydra_doc + + +if __name__ == "__main__": + petstore_doc_path = "../samples/v2/petstore-expanded.yaml" + uspto_doc_path = "../samples/v2/uspto.yaml" + openapi_doc = OpenAPIDocParser(petstore_doc_path) + hydra_doc = openapi_doc.parse("../samples/v2/hydra_doc_sample.py") diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/__init__.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/api_info_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/api_info_parser.py new file mode 100644 index 0000000..cb0baf8 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/api_info_parser.py @@ -0,0 +1,37 @@ +import re +from urllib.parse import urlparse + + +class InfoParser: + def __init__(self, doc) -> None: + self.doc = doc + + def parse(self): + info = dict() + info_ = dict() + for key, value in self.doc.get("info").items(): + info[key] = value + info["url"] = self.doc.get("servers")[0].get("url") + + # check for variables in the url + if self.doc.get("servers")[0].get("variables"): + server = self.doc.get("servers")[0] + for variable, variable_details in server.get("variables").items(): + info["url"] = info["url"].replace( + rf"{{{variable}}}", variable_details.get("default") + ) + url = urlparse(info["url"]) + + info_ = { + "api": info.get(url.path.split("/")[0], "api"), + "title": info.get("title", ""), + "desc": info.get("description", ""), + "base_url": f"{url.scheme}://{url.netloc}", + "doc_name": info.get("", "vocab"), + } + info_["api"] = "{}/v{}".format( + info_["api"], info.get("version", "1.0.0").split(".")[0] + ) + info_["entrypoint"] = f'{info_["base_url"]}/{info_["api"]}' + info_["id"] = f'{info_["entrypoint"]}/{info_["doc_name"]}' + return info_ diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/components_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/components_parser.py new file mode 100644 index 0000000..0cddc54 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/components_parser.py @@ -0,0 +1,12 @@ +from utils import component_class_mapping + + +class ComponentParser: + def __init__(self, component, definition) -> None: + self.component = component + self.definition = definition + + def parse(self): + Parser = component_class_mapping(self.component) + parser = Parser(self.definition) + return parser.parse() diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/method_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/method_parser.py new file mode 100644 index 0000000..1e025ca --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/method_parser.py @@ -0,0 +1,64 @@ +from hydra_python_core.doc_writer import ( + HydraClassOp, + HydraClassProp, + HydraError, + HydraStatus, +) +from exceptions import HydraCollectionException +from processors.api_info_processor import APIInfoProcessor +from parsers.param_parser import ParameterParser +from parsers.resp_parser import ResponseParser +from processors.op_processor import OperationProcessor +from parsers.schema_parser import SchemaParser + +from typing import Any, List, Dict, Union + + +class MethodParser: + def __init__(self, method: str, method_details: Dict[str, Any], id: str) -> None: + self.method = method.upper() + self.method_details = method_details + self.id = id + + def parse(self) -> List[Union[HydraClassOp, List[HydraClassProp]]]: + method_title = str + hydra_props: List[HydraClassProp] = [] + hydra_op: HydraClassOp + possible_status: List[Union[HydraStatus, HydraError]] = [] + expects_resource = "" + returns_resource = "" + for key, value in self.method_details.items(): + if key == "parameters": + for parameter in value: + param_parser = ParameterParser(parameter) + hydra_class_prop = param_parser.parse() + hydra_props.append(hydra_class_prop) + elif key == "responses": + for code, response in value.items(): + response_parser = ResponseParser(code, response) + hydra_status = response_parser.parse() + possible_status.append(hydra_status) + if response_parser.parse_code() != 500: + returns_resource = response_parser.parse_returns() + + elif key == "operationId": + method_title = value + elif key == "requestBody": + request_content = value.get("content") + for _, expects in request_content.items(): + schema_parser = SchemaParser(expects.get("schema")) + hydra_classes, _ = schema_parser.parse() + for title, _ in hydra_classes.items(): + expects_resource = title + + operation_processor = OperationProcessor( + title=method_title, + method=self.method, + id=self.id, + possible_status=possible_status, + expects=expects_resource, + returns=returns_resource, + ) + hydra_op = operation_processor.generate() + + return [hydra_op, hydra_props] diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/param_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/param_parser.py new file mode 100644 index 0000000..d4adde3 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/param_parser.py @@ -0,0 +1,41 @@ +from typing import Type +from processors.prop_processor import PropertyProcessor +from utils import type_ref_mapping +from exceptions import ExpectsValueError, HydraCollectionException + + +class ParameterParser: + def __init__(self, parameter) -> None: + self.parameter = parameter + + def parse(self): + is_collection = False + prop = "null" + title = ("null",) + required = (False,) + + for key, value in self.parameter.items(): + if key == "schema": + schema = value + if schema.get("type") == "array": + is_collection = True + else: + try: + prop = type_ref_mapping(schema.get("type")) + except KeyError: + raise ExpectsValueError( + "{} is incorrect parameter for 'supportedProperty'.".format( + schema.get("type") + ) + ) + elif key == "required": + required = value + elif key == "name": + title = value + + if is_collection: + raise HydraCollectionException("Parsed parameter is a collection.") + + property_processor = PropertyProcessor(prop, title, required) + hydra_prop = property_processor.generate() + return hydra_prop diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/path_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/path_parser.py new file mode 100644 index 0000000..85035cc --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/path_parser.py @@ -0,0 +1,46 @@ +from typing import Union, List, Dict +from hydra_python_core.doc_writer import ( + HydraClassOp, + HydraClassProp, +) +from exceptions import HydraCollectionException +from parsers.method_parser import MethodParser + + +class PathParser: + def __init__(self, path_name, path_method, id) -> None: + self.path_method = path_method + self.id = f'{id}?resource={path_name.split("/")[1].capitalize()}' + self.hydra_class_ops = [] + self.hydra_class_props = [] + self.hydra_collection_ops = {} + + def is_parsed(self): + if self.hydra_class_ops or self.hydra_collection_ops or self.hydra_class_props: + return True + return False + + def get_class_props(self): + if not self.is_parsed(): + self.parse() + return self.hydra_class_props + + def get_class_ops(self) -> List[HydraClassOp]: + if not self.is_parsed(): + self.parse() + return self.hydra_class_ops + + def get_collection_ops(self) -> Dict[str, bool]: + if not self.is_parsed(): + self.parse() + return self.hydra_collection_ops + + def parse(self) -> None: + for method_name, method_details in self.path_method.items(): + try: + method_parser = MethodParser(method_name, method_details, self.id) + hydra_class_op_, hydra_class_props_ = method_parser.parse() + self.hydra_class_ops.append(hydra_class_op_) + self.hydra_class_props.extend(hydra_class_props_) + except HydraCollectionException: + self.hydra_collection_ops[method_name] = True diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/ref_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/ref_parser.py new file mode 100644 index 0000000..94297a3 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/ref_parser.py @@ -0,0 +1,36 @@ +from utils import parser_class_mapping +from os.path import isdir + + +class RefParser: + def __init__(self, pointer) -> None: + self.pointer = pointer + + def find_root(self): + path_to_ref = self.pointer.split("/") + if path_to_ref[0] == "#": + return path_to_ref[::1] + elif isdir(path_to_ref[0]): + return "directory" + else: + return "url" + + def parse(self): + root = self.find_root() + hydra_class = {} + hydra_collection = {} + if root == "directory": + pass + elif root == "url": + pass + else: + # components within the same file + from processors.api_class_processor import APIClassProcessor + + component_type = root[2] + if component_type == "schemas": + hydra_entity = root[3] + hydra_class = APIClassProcessor.hydra_classes.get(hydra_entity) + hydra_collection = APIClassProcessor.hydra_collections.get(hydra_entity) + + return [hydra_class, hydra_collection] diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/resp_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/resp_parser.py new file mode 100644 index 0000000..c140a22 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/resp_parser.py @@ -0,0 +1,36 @@ +from processors.status_processor import StatusProcessor +from parsers.schema_parser import SchemaParser + + +class ResponseParser: + def __init__(self, code, response) -> None: + self.code = code + self.response = response + self.returns = "" + + def parse_code(self): + if self.code.isnumeric(): + return int(self.code) + else: + # handles default response + return 500 + + def parse_returns(self): + return self.returns + + def parse(self): + response = {"code": self.parse_code(), "desc": "", "title": ""} + for key, value in self.response.items(): + if key == "description": + response["desc"] = value + if key == "content": + for _, expects in value.items(): + schema_parser = SchemaParser(expects.get("schema")) + hydra_classes, _ = schema_parser.parse() + for title, _ in hydra_classes.items(): + self.returns = title + + status_processor = StatusProcessor(response) + hydra_status = status_processor.generate() + + return hydra_status diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/schema_parser.py b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/schema_parser.py new file mode 100644 index 0000000..9b39b4f --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/parsers/schema_parser.py @@ -0,0 +1,90 @@ +from hydra_python_core.doc_writer import HydraClassProp, HydraCollection, HydraClass +from exceptions import HydraCollectionException +from processors.api_info_processor import APIInfoProcessor +from processors.class_processor import ClassProcessor +from processors.collection_processor import CollectionProcessor +from processors.prop_processor import PropertyProcessor +from parsers.ref_parser import RefParser +from utils import type_ref_mapping +from typing import Dict, List, Union, Any + + +class SchemaParser: + def __init__(self, schema_details: Dict[str, Any]) -> None: + self.schema_details = schema_details + self.id = APIInfoProcessor.api_info["id"] + + @staticmethod + def create_props(title, property_details, required=False): + if property_details.get("type") == "array": + raise HydraCollectionException("Property contains a hydra collection.") + prop = type_ref_mapping(property_details["type"]) + property_processor = PropertyProcessor(prop, title, required) + hydra_prop = property_processor.generate() + return hydra_prop + + @staticmethod + def parse_props(schema_definition): + props = {} + if schema_definition.get("required"): + for required_prop in schema_definition.get("required"): + property_details = schema_definition.get("properties")[required_prop] + props[required_prop] = SchemaParser.create_props( + required_prop, property_details, True + ) + if schema_definition.get("properties"): + for prop in schema_definition.get("properties"): + if props.get(prop): + continue + property_details = schema_definition.get("properties")[prop] + props[prop] = SchemaParser.create_props(prop, property_details, True) + return list(props.values()) + + @staticmethod + def get_props(schema_definition): + hydra_class_props = [] + for key, value in schema_definition.items(): + if key in ["allOf", "anyOf", "oneOf"]: + for schema_definition_ in value: + hydra_class_props.extend(SchemaParser.get_props(schema_definition_)) + else: + if schema_definition.get("$ref"): + pass + # ref_parser = RefParser(value) + # hydra_class = ref_parser.parse() + elif schema_definition.get("type") == "array": + raise HydraCollectionException("Schema contains hydra collection") + else: + return SchemaParser.parse_props(schema_definition) + return hydra_class_props + + def parse(self) -> List[List[Union[HydraClass, HydraCollection]]]: + hydra_classes = {} + hydra_collections = {} + for name, definition in self.schema_details.items(): + # schmas containing refs + if name == "$ref": + ref_parser = RefParser(definition) + hydra_class, hydra_collection = ref_parser.parse() + if hydra_class: + hydra_classes[hydra_class.__dict__.get("title")] = hydra_class + if hydra_collection: + hydra_collections[ + hydra_collection.__dict__.get("title") + ] = hydra_collection + else: + # for schemas defined in the paramters/responses + if name in ["type", "properties", "required"]: + definition = self.schema_details + try: + hydra_props = SchemaParser.get_props(definition) + class_processor = ClassProcessor( + title=name, id=self.id, hydra_props=hydra_props + ) + hydra_class = class_processor.generate() + hydra_classes[name] = hydra_class + except HydraCollectionException: + collection_processor = CollectionProcessor(title=name, id=self.id) + hydra_collection = collection_processor.generate() + hydra_collections[name] = hydra_collection + return [hydra_classes, hydra_collections] diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/__init__.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_class_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_class_processor.py new file mode 100644 index 0000000..8ed703c --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_class_processor.py @@ -0,0 +1,77 @@ +from parsers.ref_parser import RefParser +from processors.api_info_processor import APIInfoProcessor +from processors.class_processor import ClassProcessor +from processors.collection_processor import CollectionProcessor +from parsers.components_parser import ComponentParser +from parsers.path_parser import PathParser +from hydra_python_core.doc_writer import HydraClass, HydraCollection +from typing import Dict, List, Any, Union + + +class APIClassProcessor: + hydra_classes: Dict[str, HydraClass] = {} + hydra_collections: Dict[str, HydraCollection] = {} + + def __init__(self, openapi_doc: Dict[str, Any]) -> None: + self.doc = openapi_doc + self.id = APIInfoProcessor.api_info["id"] + + def generate_from_components(self): + for component, definition in self.doc.get("components").items(): + component_parser = ComponentParser(component, definition) + component_classes, component_collections = component_parser.parse() + return [component_classes, component_collections] + + def generate_from_paths(self): + hydra_title = str + hydra_classes = {} + hydra_collections = {} + + for path_name, path_method in self.doc.get("paths").items(): + hydra_title = path_name.split("/")[1].capitalize() + if path_method.get("$ref"): + ref_parser = RefParser(path_method.get("$ref")) + hydra_class, hydra_collection = ref_parser.parse() + APIClassProcessor.hydra_classes[hydra_title] = hydra_class + APIClassProcessor.hydra_collections[hydra_title] = hydra_collection + else: + path_parser = PathParser(path_name, path_method, self.id) + hydra_class_ops = path_parser.get_class_ops() + hydra_class_props = path_parser.get_class_props() + hydra_collection_ops = path_parser.get_collection_ops() + if not hydra_classes.get(hydra_title): + hydra_classes[hydra_title] = [[], []] + if not hydra_collections.get(hydra_title): + hydra_collections[hydra_title] = {} + hydra_classes[hydra_title][0].extend(hydra_class_ops) + hydra_classes[hydra_title][1].extend(hydra_class_props) + hydra_collections[hydra_title].update(hydra_collection_ops) + + return [hydra_classes, hydra_collections] + + def generate(self) -> List[List[Union[HydraClass, HydraCollection]]]: + component_classes, component_collections = self.generate_from_components() + APIClassProcessor.hydra_classes.update(component_classes) + APIClassProcessor.hydra_collections.update(component_collections) + + path_classes, path_collections = self.generate_from_paths() + for hydra_title, [hydra_ops, hydra_props] in path_classes.items(): + class_processor = ClassProcessor( + title=hydra_title, + id=self.id, + hydra_ops=hydra_ops, + hydra_props=hydra_props, + ) + hydra_class = class_processor.generate() + APIClassProcessor.hydra_classes[hydra_title] = hydra_class + + for hydra_title, hydra_ops in path_collections.items(): + collection_processor = CollectionProcessor( + title=hydra_title, id=self.id, hydra_ops=hydra_ops + ) + hydra_collection = collection_processor.generate() + APIClassProcessor.hydra_collections[hydra_title] = hydra_collection + + hydra_classes_ = list(APIClassProcessor.hydra_classes.values()) + hydra_collections_ = list(APIClassProcessor.hydra_collections.values()) + return [hydra_classes_, hydra_collections_] diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_info_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_info_processor.py new file mode 100644 index 0000000..7fccf8c --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/api_info_processor.py @@ -0,0 +1,24 @@ +from typing import Any, Dict +from parsers.api_info_parser import InfoParser +from hydra_python_core.doc_writer import HydraDoc + + +class APIInfoProcessor: + api_info: Dict[str, str] = {} + + def __init__(self, openapi_doc: Dict[str, Any]) -> None: + self.doc = openapi_doc + + def generate(self) -> HydraDoc: + info_parser = InfoParser(self.doc) + APIInfoProcessor.api_info = info_parser.parse() + + api_info_doc = HydraDoc( + API=APIInfoProcessor.api_info["api"], + title=APIInfoProcessor.api_info["title"], + desc=APIInfoProcessor.api_info["desc"], + entrypoint=APIInfoProcessor.api_info["entrypoint"], + base_url=APIInfoProcessor.api_info["base_url"], + doc_name=APIInfoProcessor.api_info["doc_name"], + ) + return api_info_doc diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/class_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/class_processor.py new file mode 100644 index 0000000..cfa16c8 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/class_processor.py @@ -0,0 +1,50 @@ +from hydra_python_core.doc_writer import ( + HydraClass, + HydraClassOp, + HydraClassProp, + HydraLink, +) +from typing import List, Union, Optional + + +class ClassProcessor: + def __init__( + self, + title: str, + id: Union[str, HydraLink], + desc: str = "", + hydra_ops: List[HydraClassOp] = [], + hydra_props: List[HydraClassProp] = [], + ) -> None: + self.title = title + self.desc = desc if desc else f"Class for {title}" + self.hydra_ops = hydra_ops + self.hydra_props = self.filter_props(hydra_props) + self.id = id + + @staticmethod + def filter_props(objects): + filtered_objs = {} + for object_ in objects: + title = object_.__dict__.get("title") + if not filtered_objs.get(title): + filtered_objs[title] = object_ + elif object_.__dict__ != filtered_objs.get(title).__dict__: + filtered_objs[title] = object_ + return filtered_objs.values() + + def generate(self) -> HydraClass: + hydra_class = HydraClass( + title=self.title, + desc=self.desc, + _id=self.id, + endpoint=True, + ) + + for hydra_op in self.hydra_ops: + hydra_class.add_supported_op(hydra_op) + + for hydra_prop in self.hydra_props: + hydra_class.add_supported_prop(hydra_prop) + + return hydra_class diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/collection_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/collection_processor.py new file mode 100644 index 0000000..9b01577 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/collection_processor.py @@ -0,0 +1,33 @@ +from typing import Dict, Optional, Union +from hydra_python_core.doc_writer import HydraCollection, HydraLink + + +class CollectionProcessor: + def __init__( + self, + title: str, + id: Union[str, HydraLink], + desc: str = "", + hydra_ops: Dict[str, bool] = {}, + manages: Optional[Dict[str, str]] = {}, + ) -> None: + self.title = title + "Collection" + self.desc = desc if desc else f"A collection for {title.lower()}" + self.hydra_ops = hydra_ops + self.manages = ( + manages + if manages + else {"object": f"{id}?resource={title}", "property": "rdfs:type"} + ) + + def generate(self) -> HydraCollection: + hydra_collection = HydraCollection( + collection_name=self.title, + collection_description=self.desc, + manages=self.manages, + get=self.hydra_ops.get("get", False), + put=self.hydra_ops.get("put", False), + post=self.hydra_ops.get("post", False), + delete=self.hydra_ops.get("delete", False), + ) + return hydra_collection diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/op_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/op_processor.py new file mode 100644 index 0000000..f997bbf --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/op_processor.py @@ -0,0 +1,39 @@ +from hydra_python_core.doc_writer import HydraClassOp, HydraStatus, HydraError +from typing import List, Union + + +class OperationProcessor: + def __init__( + self, + title: str, + method: str, + id: str, + expects: str = None, + returns: str = None, + expects_header: List[str] = [], + returns_header: List[str] = [], + possible_status: List[Union[HydraStatus, HydraError]] = [], + ) -> None: + self.method = method.upper() + self.title = title + self.expects = expects + self.returns = returns + self.expects_header = expects_header + self.returns_header = returns_header + self.possible_status = possible_status + if returns: + self.returns = id.replace(f"?resource={title}", f"?resource={returns}") + if expects: + self.expects = id.replace(f"?resource={title}", f"?resource={expects}") + + def generate(self): + hydra_class_op = HydraClassOp( + title=self.title, + method=self.method, + expects=self.expects, + returns=self.returns, + expects_header=self.expects_header, + returns_header=self.returns_header, + possible_status=self.possible_status, + ) + return hydra_class_op diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/prop_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/prop_processor.py new file mode 100644 index 0000000..f654f29 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/prop_processor.py @@ -0,0 +1,20 @@ +from hydra_python_core.doc_writer import HydraClassProp, HydraLink +from typing import Union + + +class PropertyProcessor: + def __init__(self, prop: Union[str, HydraLink], title: str, required: bool) -> None: + self.prop = prop + self.title = title + self.required = required + + def generate(self): + hydra_prop = HydraClassProp( + prop=self.prop, + title=self.title, + read=False, + write=False, + required=self.required, + ) + + return hydra_prop diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/processors/status_processor.py b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/status_processor.py new file mode 100644 index 0000000..9f391c6 --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/processors/status_processor.py @@ -0,0 +1,15 @@ +from hydra_python_core.doc_writer import HydraStatus + + +class StatusProcessor: + def __init__(self, response) -> None: + self.response = response + + def generate(self): + hydra_status = HydraStatus( + code=self.response["code"], + title=self.response["title"], + desc=self.response["desc"], + ) + + return hydra_status diff --git a/hydra_openapi_parser/hydra_openapi_parser_v2/utils.py b/hydra_openapi_parser/hydra_openapi_parser_v2/utils.py new file mode 100644 index 0000000..709135f --- /dev/null +++ b/hydra_openapi_parser/hydra_openapi_parser_v2/utils.py @@ -0,0 +1,90 @@ +from hydra_python_core.doc_writer import HydraDoc +import json +from typing import Dict, Any, List + + +def parser_class_mapping(category): + from parsers.param_parser import ParameterParser + from parsers.resp_parser import ResponseParser + from parsers.path_parser import PathParser + from parsers.method_parser import MethodParser + from parsers.schema_parser import SchemaParser + + parser_class_mapping = dict() + parser_class_mapping = { + "path": PathParser, + "response": ResponseParser, + "parameter": ParameterParser, + "method": MethodParser, + "schema": SchemaParser, + } + return parser_class_mapping[category] + + +def component_class_mapping(component): + from parsers.param_parser import ParameterParser + from parsers.schema_parser import SchemaParser + from parsers.resp_parser import ResponseParser + + components = dict() + components = { + "schemas": SchemaParser, + "parameters": ParameterParser, + "responses": ResponseParser, + "securitySchemes": "", + "requestBodies": "", + "headers": "", + "examples": "", + "links": "", + "callbacks": "", + } + return components[component] + + +def type_ref_mapping(type: str) -> str: + """ + Returns semantic ref for OAS data types + :param type: data type + :return: ref + """ + dataType_ref_map = dict() + # todo add support for byte , binary , password ,double data types + dataType_ref_map["integer"] = "https://schema.org/Integer" + dataType_ref_map["string"] = "https://schema.org/Text" + dataType_ref_map["long"] = "http://books.xmlschemata.org/relaxng/ch19-77199.html" + dataType_ref_map["float"] = "https://schema.org/Float" + dataType_ref_map["boolean"] = "https://schema.org/Boolean" + dataType_ref_map["dateTime"] = "https://schema.org/DateTime" + dataType_ref_map["date"] = "https://schema.org/Date" + + return dataType_ref_map[type] + + +def gen_entrypoint(api_doc: HydraDoc) -> HydraDoc: + """ + Generates Entrypoint, Base Collection and Base Resource for the documentation + :param api_doc: contains the Hydra Doc created + """ + api_doc.add_baseCollection() + api_doc.add_baseResource() + api_doc.gen_EntryPoint() + return api_doc + + +def gen_doc_file(hydra_doc: Dict[str, Any]) -> str: + """ + Helper function to dump generated hydradoc > py file. + :param doc: generated hydra doc + :return: hydra doc created + """ + dump = json.dumps(hydra_doc, indent=4, sort_keys=True) + hydra_doc = '''"""\nGenerated API Documentation for Server API using + server_doc_gen.py."""\n\ndoc = {}'''.format( + dump + ) + hydra_doc = "{}\n".format(hydra_doc) + hydra_doc = hydra_doc.replace("true", '"true"') + hydra_doc = hydra_doc.replace("false", '"false"') + hydra_doc = hydra_doc.replace("null", '"null"') + + return hydra_doc diff --git a/hydra_openapi_parser/openapi_parser.py b/hydra_openapi_parser/openapi_parser.py index e2e92ee..e66768a 100644 --- a/hydra_openapi_parser/openapi_parser.py +++ b/hydra_openapi_parser/openapi_parser.py @@ -2,11 +2,12 @@ Module to take in Open Api Specification and convert it to HYDRA Api Doc """ +import os import yaml import json from typing import Any, Dict, Match, Optional, Tuple, Union, List, Set from hydra_python_core.doc_writer import (HydraDoc, HydraClass, - HydraClassProp, HydraClassOp) + HydraClassProp, HydraClassOp) import sys @@ -356,7 +357,7 @@ def allow_parameter(parameter: Dict[str, Any]) -> bool: # can add rules about param processing # param can be in path too , that is already handled when we declared # the class as collection from the endpoint - params_location = ["body", "integer", "string", "long", "float",\ + params_location = ["body", "integer", "string", "long", "float", "boolean", "dateTime", "Date", "array"] try: if parameter["type"] in params_location: @@ -364,11 +365,11 @@ def allow_parameter(parameter: Dict[str, Any]) -> bool: except KeyError: if parameter["in"] in params_location: return True - + return False -def type_ref_mapping(type: str)->str: +def type_ref_mapping(type: str) -> str: """ Returns semantic ref for OAS data types :param type: data type @@ -383,7 +384,7 @@ def type_ref_mapping(type: str)->str: dataType_ref_map["boolean"] = "https://schema.org/Boolean" dataType_ref_map["dateTime"] = "https://schema.org/DateTime" dataType_ref_map["date"] = "https://schema.org/Date" - + return dataType_ref_map[type] @@ -533,7 +534,7 @@ def parse(doc: Dict[str, Any]) -> Dict[str, Any]: name = try_catch_replacement(doc, "basePath", "api") schemes = try_catch_replacement(doc, "schemes", "http") api_doc = HydraDoc(name, title, desc, name, - "{}://{}".format(schemes[0], baseURL)) + "{}://{}".format(schemes[0], baseURL), "vocab") get_paths(global_) for name in global_["class_names"]: for prop in global_[name]["prop_definition"]: @@ -573,12 +574,19 @@ def dump_documentation(hydra_doc: Dict[str, Any]) -> str: if __name__ == "__main__": - with open("../samples/petstore_openapi.yaml", 'r') as stream: + # construct the path for the input OpenAPI doc + current_dir = os.path.dirname(__file__) + input_file_path = os.path.join( + current_dir, "../samples/petstore_openapi.yaml") + with open(input_file_path) as stream: try: doc = yaml.load(stream) except yaml.YAMLError as exc: print(exc) hydra_doc = parse(doc) - with open("../samples/hydra_doc_sample.py", "w") as f: + # construct the path for the output Hydra API doc + output_file_path = os.path.join( + current_dir, "../samples/hydra_doc_sample.py") + with open(output_file_path, "w") as f: f.write(dump_documentation(hydra_doc)) diff --git a/samples/v2/hydra_doc_sample.py b/samples/v2/hydra_doc_sample.py new file mode 100644 index 0000000..4843c6f --- /dev/null +++ b/samples/v2/hydra_doc_sample.py @@ -0,0 +1,313 @@ +""" +Generated API Documentation for Server API using + server_doc_gen.py.""" + +doc = { + "@context": { + "ApiDocumentation": "hydra:ApiDocumentation", + "description": "hydra:description", + "domain": { + "@id": "rdfs:domain", + "@type": "@id" + }, + "entrypoint": { + "@id": "hydra:entrypoint", + "@type": "@id" + }, + "expects": { + "@id": "hydra:expects", + "@type": "@id" + }, + "expectsHeader": "hydra:expectsHeader", + "hydra": "http://www.w3.org/ns/hydra/core#", + "label": "rdfs:label", + "manages": "hydra:manages", + "method": "hydra:method", + "object": { + "@id": "hydra:object", + "@type": "@id" + }, + "possibleStatus": "hydra:possibleStatus", + "property": { + "@id": "hydra:property", + "@type": "@id" + }, + "range": { + "@id": "rdfs:range", + "@type": "@id" + }, + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "readable": "hydra:readable", + "required": "hydra:required", + "returns": { + "@id": "hydra:returns", + "@type": "@id" + }, + "returnsHeader": "hydra:returnsHeader", + "search": "hydra:search", + "statusCode": "hydra:statusCode", + "subClassOf": { + "@id": "rdfs:subClassOf", + "@type": "@id" + }, + "subject": { + "@id": "hydra:subject", + "@type": "@id" + }, + "supportedClass": "hydra:supportedClass", + "supportedOperation": "hydra:supportedOperation", + "supportedProperty": "hydra:supportedProperty", + "title": "hydra:title", + "writeable": "hydra:writeable", + "xsd": "https://www.w3.org/TR/xmlschema-2/#" + }, + "@id": "http://petstore.swagger.io/api/v1/vocab", + "@type": "ApiDocumentation", + "description": "A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification", + "entrypoint": "http://petstore.swagger.io/api/v1", + "possibleStatus": [], + "supportedClass": [ + { + "@id": "http://petstore.swagger.io/api/v1/vocab", + "@type": "hydra:Class", + "description": "Class for Pet", + "supportedOperation": [], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "https://schema.org/Integer", + "readable": "false", + "required": "true", + "title": "id", + "writeable": "false" + } + ], + "title": "Pet" + }, + { + "@id": "http://petstore.swagger.io/api/v1/vocab", + "@type": "hydra:Class", + "description": "Class for NewPet", + "supportedOperation": [], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "https://schema.org/Text", + "readable": "false", + "required": "true", + "title": "name", + "writeable": "false" + }, + { + "@type": "SupportedProperty", + "property": "https://schema.org/Text", + "readable": "false", + "required": "true", + "title": "tag", + "writeable": "false" + } + ], + "title": "NewPet" + }, + { + "@id": "http://petstore.swagger.io/api/v1/vocab", + "@type": "hydra:Class", + "description": "Class for Error", + "supportedOperation": [], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "https://schema.org/Integer", + "readable": "false", + "required": "true", + "title": "code", + "writeable": "false" + }, + { + "@type": "SupportedProperty", + "property": "https://schema.org/Text", + "readable": "false", + "required": "true", + "title": "message", + "writeable": "false" + } + ], + "title": "Error" + }, + { + "@id": "http://petstore.swagger.io/api/v1/vocab", + "@type": "hydra:Class", + "description": "Class for Pets", + "supportedOperation": [ + { + "@type": "http://schema.org/UpdateAction", + "expects": "http://petstore.swagger.io/api/v1/vocab?resource=Pets", + "expectsHeader": [], + "method": "POST", + "possibleStatus": [ + { + "@context": "https://www.w3.org/ns/hydra/core", + "@type": "Status", + "description": "pet response", + "statusCode": 200, + "title": "" + }, + { + "@context": "https://www.w3.org/ns/hydra/core", + "@type": "Status", + "description": "unexpected error", + "statusCode": 500, + "title": "" + } + ], + "returns": "http://petstore.swagger.io/api/v1/vocab?resource=Pets", + "returnsHeader": [], + "title": "addPet" + }, + { + "@type": "http://schema.org/FindAction", + "expects": "", + "expectsHeader": [], + "method": "GET", + "possibleStatus": [ + { + "@context": "https://www.w3.org/ns/hydra/core", + "@type": "Status", + "description": "pet response", + "statusCode": 200, + "title": "" + }, + { + "@context": "https://www.w3.org/ns/hydra/core", + "@type": "Status", + "description": "unexpected error", + "statusCode": 500, + "title": "" + } + ], + "returns": "http://petstore.swagger.io/api/v1/vocab?resource=Pets", + "returnsHeader": [], + "title": "find pet by id" + }, + { + "@type": "http://schema.org/DeleteAction", + "expects": "", + "expectsHeader": [], + "method": "DELETE", + "possibleStatus": [ + { + "@context": "https://www.w3.org/ns/hydra/core", + "@type": "Status", + "description": "pet deleted", + "statusCode": 204, + "title": "" + }, + { + "@context": "https://www.w3.org/ns/hydra/core", + "@type": "Status", + "description": "unexpected error", + "statusCode": 500, + "title": "" + } + ], + "returns": "", + "returnsHeader": [], + "title": "deletePet" + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "https://schema.org/Integer", + "readable": "false", + "required": "true", + "title": "id", + "writeable": "false" + } + ], + "title": "Pets" + }, + { + "@id": "http://www.w3.org/ns/hydra/core#Collection", + "@type": "hydra:Class", + "description": "null", + "supportedOperation": [], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "http://www.w3.org/ns/hydra/core#member", + "readable": "false", + "required": "null", + "title": "members", + "writeable": "false" + } + ], + "title": "Collection" + }, + { + "@id": "http://www.w3.org/ns/hydra/core#Resource", + "@type": "hydra:Class", + "description": "null", + "supportedOperation": [], + "supportedProperty": [], + "title": "Resource" + }, + { + "@id": "http://petstore.swagger.io/api/v1/vocab?resource=PetsCollection", + "@type": "Collection", + "description": "A collection for pets", + "manages": { + "object": "http://petstore.swagger.io/api/v1/vocab?resource=Pets", + "property": "rdfs:type" + }, + "subClassOf": "http://www.w3.org/ns/hydra/core#Collection", + "supportedOperation": [ + { + "@id": "_:PetsCollection_retrieve", + "@type": "http://schema.org/FindAction", + "description": "Retrieves all the members of PetsCollection", + "expects": "null", + "expectsHeader": [], + "method": "GET", + "possibleStatus": [], + "returns": "http://petstore.swagger.io/api/v1/vocab?resource=Pets", + "returnsHeader": [] + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "description": "The members of PetsCollection", + "property": "http://www.w3.org/ns/hydra/core#member", + "readable": "true", + "required": "false", + "title": "members", + "writeable": "true" + } + ], + "title": "PetsCollection" + }, + { + "@id": "http://petstore.swagger.io/api/v1#EntryPoint", + "@type": "hydra:Class", + "description": "The main entry point or homepage of the API.", + "supportedOperation": [ + { + "@id": "_:entry_point", + "@type": "http://petstore.swagger.io/http://petstore.swagger.io/api/v1#EntryPoint", + "description": "The APIs main entry point.", + "expects": "null", + "expectsHeader": [], + "method": "GET", + "possibleStatus": [], + "returns": "null", + "returnsHeader": [] + } + ], + "supportedProperty": [], + "title": "EntryPoint" + } + ], + "title": "Swagger Petstore" +} diff --git a/samples/v2/petstore-expanded.yaml b/samples/v2/petstore-expanded.yaml new file mode 100644 index 0000000..aab81be --- /dev/null +++ b/samples/v2/petstore-expanded.yaml @@ -0,0 +1,157 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification + termsOfService: http://swagger.io/terms/ + contact: + name: Swagger API Team + email: apiteam@swagger.io + url: http://swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +servers: + - url: http://petstore.swagger.io/api +paths: + /pets: + get: + description: | + Returns all pets from the system that the user has access to + Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. + Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. + operationId: findPets + parameters: + - name: tags + in: query + description: tags to filter by + required: false + style: form + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + "200": + description: pet response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewPet" + responses: + "200": + description: pet response + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{id}: + get: + description: Returns a user based on a single ID, if the user does not have access to the pet + operationId: find pet by id + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: pet response + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + delete: + description: deletes a single pet based on the ID supplied + operationId: deletePet + parameters: + - name: id + in: path + description: ID of pet to delete + required: true + schema: + type: integer + format: int64 + responses: + "204": + description: pet deleted + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + allOf: + - $ref: "#/components/schemas/NewPet" + - type: object + required: + - id + properties: + id: + type: integer + format: int64 + + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: string + + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/samples/v2/uspto.yaml b/samples/v2/uspto.yaml new file mode 100644 index 0000000..d301152 --- /dev/null +++ b/samples/v2/uspto.yaml @@ -0,0 +1,210 @@ +openapi: 3.0.1 +servers: + - url: '{scheme}://developer.uspto.gov/ds-api' + variables: + scheme: + description: 'The Data Set API is accessible via https and http' + enum: + - 'https' + - 'http' + default: 'https' +info: + description: >- + The Data Set API (DSAPI) allows the public users to discover and search + USPTO exported data sets. This is a generic API that allows USPTO users to + make any CSV based data files searchable through API. With the help of GET + call, it returns the list of data fields that are searchable. With the help + of POST call, data can be fetched based on the filters on the field names. + Please note that POST call is used to search the actual data. The reason for + the POST call is that it allows users to specify any complex search criteria + without worry about the GET size limitations as well as encoding of the + input parameters. + version: 1.0.0 + title: USPTO Data Set API + contact: + name: Open Data Portal + url: 'https://developer.uspto.gov' + email: developer@uspto.gov +tags: + - name: metadata + description: Find out about the data sets + - name: search + description: Search a data set +paths: + /: + get: + tags: + - metadata + operationId: list-data-sets + summary: List available data sets + responses: + '200': + description: Returns a list of data sets + content: + application/json: + schema: + $ref: '#/components/schemas/dataSetList' + example: + { + "total": 2, + "apis": [ + { + "apiKey": "oa_citations", + "apiVersionNumber": "v1", + "apiUrl": "https://developer.uspto.gov/ds-api/oa_citations/v1/fields", + "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/oa_citations.json" + }, + { + "apiKey": "cancer_moonshot", + "apiVersionNumber": "v1", + "apiUrl": "https://developer.uspto.gov/ds-api/cancer_moonshot/v1/fields", + "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/cancer_moonshot.json" + } + ] + } + /{dataset}/{version}/fields: + get: + tags: + - metadata + summary: >- + Provides the general information about the API and the list of fields + that can be used to query the dataset. + description: >- + This GET API returns the list of all the searchable field names that are + in the oa_citations. Please see the 'fields' attribute which returns an + array of field names. Each field or a combination of fields can be + searched using the syntax options shown below. + operationId: list-searchable-fields + parameters: + - name: dataset + in: path + description: 'Name of the dataset.' + required: true + example: "oa_citations" + schema: + type: string + - name: version + in: path + description: Version of the dataset. + required: true + example: "v1" + schema: + type: string + responses: + '200': + description: >- + The dataset API for the given version is found and it is accessible + to consume. + content: + application/json: + schema: + type: string + '404': + description: >- + The combination of dataset name and version is not found in the + system or it is not published yet to be consumed by public. + content: + application/json: + schema: + type: string + /{dataset}/{version}/records: + post: + tags: + - search + summary: >- + Provides search capability for the data set with the given search + criteria. + description: >- + This API is based on Solr/Lucene Search. The data is indexed using + SOLR. This GET API returns the list of all the searchable field names + that are in the Solr Index. Please see the 'fields' attribute which + returns an array of field names. Each field or a combination of fields + can be searched using the Solr/Lucene Syntax. Please refer + https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for + the query syntax. List of field names that are searchable can be + determined using above GET api. + operationId: perform-search + parameters: + - name: version + in: path + description: Version of the dataset. + required: true + schema: + type: string + default: v1 + - name: dataset + in: path + description: 'Name of the dataset. In this case, the default value is oa_citations' + required: true + schema: + type: string + default: oa_citations + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + type: object + additionalProperties: + type: object + '404': + description: No matching record found for the given criteria. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + criteria: + description: >- + Uses Lucene Query Syntax in the format of + propertyName:value, propertyName:[num1 TO num2] and date + range format: propertyName:[yyyyMMdd TO yyyyMMdd]. In the + response please see the 'docs' element which has the list of + record objects. Each record structure would consist of all + the fields and their corresponding values. + type: string + default: '*:*' + start: + description: Starting record number. Default value is 0. + type: integer + default: 0 + rows: + description: >- + Specify number of rows to be returned. If you run the search + with default values, in the response you will see 'numFound' + attribute which will tell the number of records available in + the dataset. + type: integer + default: 100 + required: + - criteria +components: + schemas: + dataSetList: + type: object + properties: + total: + type: integer + apis: + type: array + items: + type: object + properties: + apiKey: + type: string + description: To be used as a dataset parameter value + apiVersionNumber: + type: string + description: To be used as a version parameter value + apiUrl: + type: string + format: uriref + description: "The URL describing the dataset's fields" + apiDocumentationUrl: + type: string + format: uriref + description: A URL to the API console for each API