diff --git a/.gitignore b/.gitignore index 7bff731..894a44c 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,3 @@ venv.bak/ # mypy .mypy_cache/ - diff --git a/README.md b/README.md index b0fe508..bd958c4 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ # hydra-openapi-parser + +How to work with this project +``` +pip install -e git+git://github.com/HTTP-APIs/hydra-python-core#egg=hydra-python-core +pip install -e . +``` + diff --git a/openapi_parser/__init__.py b/openapi_parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_parser/openapi_parser.py b/openapi_parser/openapi_parser.py new file mode 100644 index 0000000..daed915 --- /dev/null +++ b/openapi_parser/openapi_parser.py @@ -0,0 +1,542 @@ +""" +Module to take in Open Api Specification and convert it to HYDRA Api Doc + +""" +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) +import sys + + +def try_catch_replacement(block: Any, get_this: str, default: Any) -> str: + """ + Replacement for the try catch blocks. HELPER FUNCTION + :param block: Data from where information has to be parsed + :param get_this: The key using which we have to fetch values from the block + :param default: default value incase the key does not exist + :return: string containing the value + """ + try: + return block[get_this] + except KeyError: + return default + + +def generateEntrypoint(api_doc: HydraDoc) -> None: + """ + 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() + + +def generate_empty_object() -> Dict[str, Any]: + """ + Generate Empty object + :return: empty object + """ + object = { + "class_name": "", + "class_definition": HydraClass, + "prop_definition": list(), + "op_definition": list(), + "collection": False, + "path": "", + "methods": set() + } + return object + + +def check_collection(schema_obj: Dict[str, Any], method: str) -> bool: + """ + Checks if the method is collection or not , checks if the method is valid + :param class_name: name of class being parsed + :param global_: global state + :param schema_obj: the dict containing the method object + :param method: method ie GET,POST,PUT,DELETE + :return: object that is formed or updated + """ + collection = bool + # get the object type from schema block + try: + type = schema_obj["type"] + # if object type is array it means the method is a collection + if type == "array": + collection = True + else: + # here type should be "object" + collection = False + except KeyError: + collection = False + # checks if the method is something like 'pet/{petid}' + if valid_endpoint(method) == "collection" and collection is False: + collection = True + return collection + + +def check_array_param(paths_: Dict[str, Any]) -> bool: + """ + Check if the path is supported or not + :param paths_: the path object from doc + :return: TRUE if the path is supported + """ + for method in paths_: + for param in paths_[method]["parameters"]: + try: + if param["type"] == "array" and method == "get": + return False + except KeyError: + pass + return True + + +def valid_endpoint(path: str) -> str: + """ + Checks is the path ie endpoint is constructed properly or not + rejects 'A/{id}/B/C' + :param path: endpoint + :return: + """ + # "collection" or true means valid + path_ = path.split('/') + for subPath in path_: + if "{" in subPath: + if subPath != path_[len(path_) - 1]: + return "False" + else: + return "Collection" + return "True" + + +def get_class_name(class_location: List[str]) -> str: + """ + To get class name from the class location reference given + :param class_location: list containing the class location + :return: name of class + """ + return class_location[len(class_location) - 1] + + +def get_data_at_location( + class_location: List[str], doc: Dict[str, Any]) -> Dict[str, Any]: + """ + TO get the dict at the class location provided + :param class_location: list containing the class location + :param doc: the open api doc + :return: class defined at class location in the doc + """ + data = doc + index = 0 + while index <= len(class_location) - 3: + data = data[class_location[index + 1]][class_location[index + 2]] + index = index + 1 + return data + + +def sanitise_path(path: str) -> str: + """ + Removed any variable present in the path + :param path: + :return: + """ + path_ = path.split('/') + new_path = list() + for subPath in path_: + if "{" in subPath: + pass + else: + new_path.append(subPath) + result = '/'.join(new_path)[1:] + + return result + + +def get_class_details(global_: Dict[str, + Any], + data: Dict[str, + Any], + class_name: str, + path="") -> None: + """ + fetches details of class and adds the class to the dict along with the + classDefinition until this point + :param global_: global state + :param class_name: name of class + :param data: data from the location given in $ref + :param path: Optional , custom enpoint to be assigned + :return: None + """ + doc = global_["doc"] + path = sanitise_path(path) + + class_name = class_name + # we simply check if the class has been defined or not + + if not hasattr(global_[class_name]["class_definition"], 'endpoint'): + desc = data + try: + classDefinition = HydraClass( + class_name, + class_name, + desc["description"], + endpoint=True, + path=path) + except KeyError: + classDefinition = HydraClass( + class_name, class_name, class_name, endpoint=True, path=path) + # we need to add object to global before we can attach props + added = generateOrUpdateClass(class_name, False, global_, "") + if added: + global_[class_name]["class_definition"] = classDefinition + + properties = data["properties"] + try: + required = data["required"] + except KeyError: + required = set() + + for prop in properties: + vocabFlag = True + errFlag = False + if prop not in global_["class_names"]: + try: + ref = properties[prop]["$ref"].split('/') + + if ref[0] == "#": + get_class_details( + global_, + get_data_at_location( + ref, + global_["doc"]), + get_class_name(ref), + get_class_name(ref)) + else: + vocabFlag = False + except KeyError: + # throw exception + # ERROR + errFlag = True + pass + except AttributeError: + # ERROR thow + pass + flag = False + if prop in required and len(required) > 0: + flag = True + if vocabFlag: + if errFlag: + global_[class_name]["prop_definition"].append( + HydraClassProp("", prop, required=flag, read=True, + write=True)) + else: + global_[class_name]["prop_definition"].append( + HydraClassProp("vocab:".format(prop), prop, required=flag, + read=True, write=True)) + else: + global_[class_name]["prop_definition"].append(HydraClassProp( + prop, prop, required=flag, read=True, write=True)) + global_[class_name]["path"] = path + global_[class_name]["class_definition"] = classDefinition + global_["class_names"].add(class_name) + + +def generateOrUpdateClass(name, collection, global_, path) -> bool: + """ + Generates or Updates the class if it already exists + :param name: class name + :param collection: if the class is collection or not + :param global_: global state + :param path: path + :return: bool showing if the operation was successful + """ + if valid_endpoint(path): + if name in global_["class_names"] and collection is True: + global_[name]["collection"] = True + return True + elif name in global_["class_names"] and collection is False: + return True + else: + object_ = generate_empty_object() + object_["class_name"] = name + object_["collection"] = collection + global_[name] = object_ + global_["class_names"].add(name) + return True + else: + return False + + +def check_for_ref(global_: Dict[str, Any], + path: str, + block: Dict[str, Any]) -> str: + """ + Checks for references in responses and parameters key , + and adds classes to state + :param global_: global state + :param path: endpoint + :param block: block containing specific part of doc + :return: class name + """ + + # will check if there is an external ref , go to that location, + # add the class in globals , will also verify + # if we can parse this method or not , if all good will return class name + for obj in block["responses"]: + try: + try: + # can only be internal + class_location = block["responses"][obj]["schema"]["$ref"].\ + split('/') + except KeyError: + class_location = \ + block["responses"][obj]["schema"]["items"]["$ref"].\ + split('/') + collection = check_collection( + schema_obj=block["responses"][obj]["schema"], + method=path) + success = generateOrUpdateClass( + get_class_name(class_location), collection, global_, path) + if not success: + return "" + + get_class_details( + global_, + get_data_at_location( + class_location, + global_["doc"]), + get_class_name(class_location), + path=path) + return class_location[2] + except KeyError: + pass + + # when we would be able to take arrays as parameters we will use + # check_if_collection here as well + flag = try_catch_replacement(block, "parameters", "False") + if flag != "False": + for obj in block["parameters"]: + try: + try: + class_location = obj["schema"]["$ref"].split('/') + except KeyError: + class_location = obj["schema"]["items"]["$ref"].split('/') + collection_ = check_collection(obj["schema"], path) + success = generateOrUpdateClass( + get_class_name(class_location), collection_, global_, path) + if not success: + return "" + get_class_details( + global_, + get_data_at_location( + class_location, + global_["doc"]), + get_class_name(class_location), + path=path) + return class_location[2] + except KeyError: + pass + # cannot parse because no external ref + + print("Cannot parse path {} because no ref to local class provided".format(path)) + return "" + + +def allow_parameter(parameter: Dict[str, Any]) -> bool: + """ + Checks the validity of params that are to be processed + according to rules of param passing + :param parameter: the parameter to be parsed + :return: if its valid or not + """ + # 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"] + if parameter["in"] not in params_location: + return False + return True + + +def get_parameters(global_: Dict[str, Any], + path: str, method: str, class_name: str) -> str: + """ + Parse paramters from method object + :param global_: global state + :param path: endpoint + :param method: method under consideration + :param class_name: name of class + :return: param + """ + param = str + for parameter in global_["doc"]["paths"][path][method]["parameters"]: + # will call schema_parse with class name and schema block + # after checking if type exists + # coz there are instances where no schema key is present + if allow_parameter(parameter): + try: + # check if class has been pared + if parameter["schema"]["$ref"].split( + '/')[2] in global_["class_names"]: + param = "vocab:{}".format( + parameter["schema"]["$ref"].split('/')[2]) + + else: + # if not go to that location and parse and add + get_class_details( + global_, + get_data_at_location( + parameter["schema"]["$ref"]), + parameter["schema"]["$ref"].split('/')[2], + path=path) + param = "vocab:{}".format( + parameter["schema"]["$ref"].split('/')[2]) + except KeyError: + param = "" + + return param + + +def get_ops(global_: Dict[str, Any], path: str, + method: Dict[str, Any], class_name: str) -> None: + """ + Get operations from path object and store in global path + :param global_: global state + :param path: endpoint + :param method: method block + :param class_name:class name + """ + if method not in global_[class_name]["methods"]: + op_method = method + + op_expects = None + op_name = try_catch_replacement( + global_["doc"]["paths"][path][method], + "summary", + class_name) + op_status = list() + op_expects = get_parameters(global_, path, method, class_name) + try: + responses = global_["doc"]["paths"][path][method]["responses"] + op_returns = None + for response in responses: + if response != 'default': + op_status.append({"statusCode": int( + response), + "description": responses[response]["description"]}) + try: + op_returns = "vocab:{}".format( + responses[response]["schema"]["$ref"].split('/')[2]) + except KeyError: + pass + if op_returns is None: + try: + op_returns = "vocab:{}".format( + responses[response]["schema"]["items"]["$ref"].split('/')[2]) + except KeyError: + op_returns = try_catch_replacement( + responses[response]["schema"], "type", None) + except KeyError: + op_returns = None + if len(op_status) == 0: + op_status.append( + {"statusCode": 200, "description": "Successful Operation"}) + global_[class_name]["methods"].add(method) + global_[class_name]["op_definition"].append(HydraClassOp( + op_name, op_method.upper(), op_expects, op_returns, op_status)) + else: + print("Method on path {} already present !".format(path)) + + +def get_paths(global_: Dict[str, Any]) -> None: + """ + Parse paths iteratively + :param global_: Global state + """ + paths = global_["doc"]["paths"] + for path in paths: + for method in paths[path]: + class_name = check_for_ref(global_, path, paths[path][method]) + if class_name != "": + # do further processing + get_ops(global_, path, method, class_name) + + +def parse(doc: Dict[str, Any]) -> Dict[str, Any]: + """ + To parse the "info" block and create Hydra Doc + :param doc: the open api documentation + :return: hydra doc created + """ + definitionSet = set() # type: Set[str] + info = try_catch_replacement(doc, "info", "") + global_ = dict() + global_["class_names"] = definitionSet + global_["doc"] = doc + + if info != "": + desc = try_catch_replacement(info, "description", "not defined") + title = try_catch_replacement(info, "title", "not defined") + else: + desc = "not defined" + title = "not defined" + print("Desc and title not present hence exit") + sys.exit() + baseURL = try_catch_replacement(doc, "host", "localhost") + 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)) + get_paths(global_) + for name in global_["class_names"]: + for prop in global_[name]["prop_definition"]: + global_[name]["class_definition"].add_supported_prop(prop) + for op in global_[name]["op_definition"]: + global_[name]["class_definition"].add_supported_op(op) + if global_[name]["collection"] is True: + if global_[name]["class_definition"].endpoint is True: + global_[name]["class_definition"].endpoint = False + + api_doc.add_supported_class( + global_[name]["class_definition"], + global_[name]["collection"], + collection_path=global_[name]["path"]) + + generateEntrypoint(api_doc) + hydra_doc = api_doc.generate() + + return hydra_doc + + +def dump_documentation(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 + + +if __name__ == "__main__": + with open("../samples/petstore_openapi.yaml", 'r') as stream: + try: + doc = yaml.load(stream) + except yaml.YAMLError as exc: + print(exc) + hydra_doc = parse(doc) + + f = open("../samples/hydra_doc_sample.py", "w") + f.write(dump_documentation(hydra_doc)) + f.close() diff --git a/openapi_parser/samples/__init__.py b/openapi_parser/samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_parser/samples/hydra_doc_sample.py b/openapi_parser/samples/hydra_doc_sample.py new file mode 100644 index 0000000..8ce592a --- /dev/null +++ b/openapi_parser/samples/hydra_doc_sample.py @@ -0,0 +1,723 @@ +""" +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" + }, + "expects": { + "@id": "hydra:expects", + "@type": "@id" + }, + "hydra": "http://www.w3.org/ns/hydra/core#", + "label": "rdfs:label", + "method": "hydra:method", + "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#", + "readonly": "hydra:readonly", + "required": "hydra:required", + "returns": { + "@id": "hydra:returns", + "@type": "@id" + }, + "statusCode": "hydra:statusCode", + "statusCodes": "hydra:statusCodes", + "subClassOf": { + "@id": "rdfs:subClassOf", + "@type": "@id" + }, + "supportedClass": "hydra:supportedClass", + "supportedOperation": "hydra:supportedOperation", + "supportedProperty": "hydra:supportedProperty", + "title": "hydra:title", + "vocab": "http://petstore.swagger.io/v2/vocab#", + "writeonly": "hydra:writeonly" + }, + "@id": "http://petstore.swagger.io/v2/vocab", + "@type": "ApiDocumentation", + "description": + "This is a sample server Petstore server." + " You can find out more about Swagger at [http://swagger.io](http://swagger.io)" + " or on [irc.freenode.net, #swagger](http://swagger.io/irc/)." + " For this sample, you can use the api key `special-key` to test the" + " authorization filters.", + "possibleStatus": [], + "supportedClass": [ + { + "@id": "vocab:ApiResponse", + "@type": "hydra:Class", + "description": "ApiResponse", + "supportedOperation": [ + { + "@type": "http://schema.org/UpdateAction", + "expects": "", + "method": "POST", + "possibleStatus": [ + { + "description": "successful operation", + "statusCode": 200 + } + ], + "returns": "vocab:ApiResponse", + "title": "uploads an image" + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "code", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "type", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "message", + "writeonly": "true" + } + ], + "title": "ApiResponse" + }, + { + "@id": "vocab:User", + "@type": "hydra:Class", + "description": "User", + "supportedOperation": [ + { + "@type": "http://schema.org/UpdateAction", + "expects": "vocab:User", + "method": "POST", + "possibleStatus": [ + { + "description": "Successful Operation", + "statusCode": 200 + } + ], + "returns": "null", + "title": "Create user" + }, + { + "@type": "http://schema.org/FindAction", + "expects": "", + "method": "GET", + "possibleStatus": [ + { + "description": "successful operation", + "statusCode": 200 + }, + { + "description": "Invalid username supplied", + "statusCode": 400 + }, + { + "description": "User not found", + "statusCode": 404 + } + ], + "returns": "vocab:User", + "title": "Get user by user name" + }, + { + "@type": "http://schema.org/AddAction", + "expects": "vocab:User", + "method": "PUT", + "possibleStatus": [ + { + "description": "Invalid user supplied", + "statusCode": 400 + } + ], + "returns": "null", + "title": "Updated user" + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "id", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "username", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "firstName", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "lastName", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "email", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "password", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "phone", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "userStatus", + "writeonly": "true" + } + ], + "title": "User" + }, + { + "@id": "vocab:Order", + "@type": "hydra:Class", + "description": "this is def", + "supportedOperation": [ + { + "@type": "http://schema.org/UpdateAction", + "expects": "vocab:Order", + "method": "POST", + "possibleStatus": [ + { + "description": "successful operation", + "statusCode": 200 + }, + { + "description": "Invalid Order", + "statusCode": 400 + } + ], + "returns": "vocab:Order", + "title": "Place an order for a pet" + }, + { + "@type": "http://schema.org/FindAction", + "expects": "", + "method": "GET", + "possibleStatus": [ + { + "description": "successful operation", + "statusCode": 200 + }, + { + "description": "Invalid ID supplied", + "statusCode": 400 + }, + { + "description": "Order not found", + "statusCode": 404 + } + ], + "returns": "vocab:Order", + "title": "Find purchase order by ID" + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "id", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "petId", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "quantity", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "shipDate", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "status", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "complete", + "writeonly": "true" + } + ], + "title": "Order" + }, + { + "@id": "vocab:Pet", + "@type": "hydra:Class", + "description": "Pet", + "supportedOperation": [ + { + "@type": "http://schema.org/UpdateAction", + "expects": "vocab:Pet", + "method": "POST", + "possibleStatus": [ + { + "description": "Invalid input", + "statusCode": 405 + } + ], + "returns": "null", + "title": "Add a new pet to the store" + }, + { + "@type": "http://schema.org/AddAction", + "expects": "vocab:Pet", + "method": "PUT", + "possibleStatus": [ + { + "description": "Invalid ID supplied", + "statusCode": 400 + } + ], + "returns": "null", + "title": "Update an existing pet" + }, + { + "@type": "http://schema.org/FindAction", + "expects": "", + "method": "GET", + "possibleStatus": [ + { + "description": "successful operation", + "statusCode": 200 + } + ], + "returns": "vocab:Pet", + "title": "get all pets" + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "id", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "category", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "true", + "title": "name", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "true", + "title": "photoUrls", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "tags", + "writeonly": "true" + }, + { + "@type": "SupportedProperty", + "property": "", + "readonly": "true", + "required": "false", + "title": "status", + "writeonly": "true" + } + ], + "title": "Pet" + }, + { + "@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", + "readonly": "false", + "required": "null", + "title": "members", + "writeonly": "false" + } + ], + "title": "Collection" + }, + { + "@id": "http://www.w3.org/ns/hydra/core#Resource", + "@type": "hydra:Class", + "description": "null", + "supportedOperation": [], + "supportedProperty": [], + "title": "Resource" + }, + { + "@id": "vocab:UserCollection", + "@type": "hydra:Class", + "description": "A collection of user", + "subClassOf": "http://www.w3.org/ns/hydra/core#Collection", + "supportedOperation": [ + { + "@id": "_:user_collection_retrieve", + "@type": "http://schema.org/FindAction", + "description": "Retrieves all User entities", + "expects": "null", + "method": "GET", + "returns": "vocab:UserCollection", + "statusCodes": [] + }, + { + "@id": "_:user_create", + "@type": "http://schema.org/AddAction", + "description": "Create new User entitity", + "expects": "vocab:User", + "method": "PUT", + "returns": "vocab:User", + "statusCodes": [ + { + "description": "If the User entity was created successfully.", + "statusCode": 201 + } + ] + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "description": "The user", + "property": "http://www.w3.org/ns/hydra/core#member", + "readonly": "false", + "required": "false", + "title": "members", + "writeonly": "false" + } + ], + "title": "UserCollection" + }, + { + "@id": "vocab:PetCollection", + "@type": "hydra:Class", + "description": "A collection of pet", + "subClassOf": "http://www.w3.org/ns/hydra/core#Collection", + "supportedOperation": [ + { + "@id": "_:pet_collection_retrieve", + "@type": "http://schema.org/FindAction", + "description": "Retrieves all Pet entities", + "expects": "null", + "method": "GET", + "returns": "vocab:PetCollection", + "statusCodes": [] + }, + { + "@id": "_:pet_create", + "@type": "http://schema.org/AddAction", + "description": "Create new Pet entitity", + "expects": "vocab:Pet", + "method": "PUT", + "returns": "vocab:Pet", + "statusCodes": [ + { + "description": "If the Pet entity was created successfully.", + "statusCode": 201 + } + ] + } + ], + "supportedProperty": [ + { + "@type": "SupportedProperty", + "description": "The pet", + "property": "http://www.w3.org/ns/hydra/core#member", + "readonly": "false", + "required": "false", + "title": "members", + "writeonly": "false" + } + ], + "title": "PetCollection" + }, + { + "@id": "vocab:EntryPoint", + "@type": "hydra:Class", + "description": "The main entry point or homepage of the API.", + "supportedOperation": [ + { + "@id": "_:entry_point", + "@type": "http://schema.org/FindAction", + "description": "The APIs main entry point.", + "expects": "null", + "method": "GET", + "returns": "null", + "statusCodes": "vocab:EntryPoint" + } + ], + "supportedProperty": [ + { + "hydra:description": "The ApiResponse Class", + "hydra:title": "apiresponse", + "property": { + "@id": "vocab:EntryPoint//pet/uploadImage", + "@type": "hydra:Link", + "description": "ApiResponse", + "domain": "vocab:EntryPoint", + "label": "ApiResponse", + "range": "vocab:ApiResponse", + "supportedOperation": [ + { + "@id": "uploads an image", + "@type": "http://schema.org/UpdateAction", + "description": "null", + "expects": "", + "label": "uploads an image", + "method": "POST", + "returns": "vocab:ApiResponse", + "statusCodes": [ + { + "description": "successful operation", + "statusCode": 200 + } + ] + } + ] + }, + "readonly": "true", + "required": "null", + "writeonly": "false" + }, + { + "hydra:description": "The Order Class", + "hydra:title": "order", + "property": { + "@id": "vocab:EntryPoint//store/order", + "@type": "hydra:Link", + "description": "this is def", + "domain": "vocab:EntryPoint", + "label": "Order", + "range": "vocab:Order", + "supportedOperation": [ + { + "@id": "place an order for a pet", + "@type": "http://schema.org/UpdateAction", + "description": "null", + "expects": "vocab:Order", + "label": "Place an order for a pet", + "method": "POST", + "returns": "vocab:Order", + "statusCodes": [ + { + "description": "successful operation", + "statusCode": 200 + }, + { + "description": "Invalid Order", + "statusCode": 400 + } + ] + }, + { + "@id": "find purchase order by id", + "@type": "http://schema.org/FindAction", + "description": "null", + "expects": "", + "label": "Find purchase order by ID", + "method": "GET", + "returns": "vocab:Order", + "statusCodes": [ + { + "description": "successful operation", + "statusCode": 200 + }, + { + "description": "Invalid ID supplied", + "statusCode": 400 + }, + { + "description": "Order not found", + "statusCode": 404 + } + ] + } + ] + }, + "readonly": "true", + "required": "null", + "writeonly": "false" + }, + { + "hydra:description": "The UserCollection collection", + "hydra:title": "usercollection", + "property": { + "@id": "vocab:EntryPoint//user", + "@type": "hydra:Link", + "description": "The UserCollection collection", + "domain": "vocab:EntryPoint", + "label": "UserCollection", + "range": "vocab:UserCollection", + "supportedOperation": [ + { + "@id": "_:user_collection_retrieve", + "@type": "http://schema.org/FindAction", + "description": "Retrieves all User entities", + "expects": "null", + "method": "GET", + "returns": "vocab:UserCollection", + "statusCodes": [] + }, + { + "@id": "_:user_create", + "@type": "http://schema.org/AddAction", + "description": "Create new User entitity", + "expects": "vocab:User", + "method": "PUT", + "returns": "vocab:User", + "statusCodes": [ + { + "description": "If the User entity was created successfully.", + "statusCode": 201 + } + ] + } + ] + }, + "readonly": "true", + "required": "null", + "writeonly": "false" + }, + { + "hydra:description": "The PetCollection collection", + "hydra:title": "petcollection", + "property": { + "@id": "vocab:EntryPoint//pet", + "@type": "hydra:Link", + "description": "The PetCollection collection", + "domain": "vocab:EntryPoint", + "label": "PetCollection", + "range": "vocab:PetCollection", + "supportedOperation": [ + { + "@id": "_:pet_collection_retrieve", + "@type": "http://schema.org/FindAction", + "description": "Retrieves all Pet entities", + "expects": "null", + "method": "GET", + "returns": "vocab:PetCollection", + "statusCodes": [] + }, + { + "@id": "_:pet_create", + "@type": "http://schema.org/AddAction", + "description": "Create new Pet entitity", + "expects": "vocab:Pet", + "method": "PUT", + "returns": "vocab:Pet", + "statusCodes": [ + { + "description": "If the Pet entity was created successfully.", + "statusCode": 201 + } + ] + } + ] + }, + "readonly": "true", + "required": "null", + "writeonly": "false" + } + ], + "title": "EntryPoint" + } + ], + "title": "Swagger Petstore" +} diff --git a/openapi_parser/samples/petstore_openapi.yaml b/openapi_parser/samples/petstore_openapi.yaml new file mode 100644 index 0000000..7748094 --- /dev/null +++ b/openapi_parser/samples/petstore_openapi.yaml @@ -0,0 +1,724 @@ +swagger: '2.0' +info: + description: 'This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.' + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +host: petstore.swagger.io +basePath: /v2 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +schemes: + - http +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + consumes: + - application/json + - application/xml + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Pet object that needs to be added to the store + required: true + schema: + $ref: '#/definitions/Pet' + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + consumes: + - application/json + - application/xml + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Pet object that needs to be added to the store + required: true + schema: + $ref: '#/definitions/Pet' + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + get: + summary: get all pets + description: get all pets + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + collectionFormat: multi + responses: + '200': + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + produces: + - application/xml + - application/json + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + collectionFormat: multi + responses: + '200': + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: 'Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.' + operationId: findPetsByTags + produces: + - application/xml + - application/json + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + type: array + items: + type: string + collectionFormat: multi + responses: + '200': + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + produces: + - application/xml + - application/json + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + type: integer + format: int64 + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + consumes: + - application/x-www-form-urlencoded + produces: + - application/xml + - application/json + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + type: integer + format: int64 + - name: name + in: formData + description: Updated name of the pet + required: false + type: string + - name: status + in: formData + description: Updated status of the pet + required: false + type: string + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + produces: + - application/xml + - application/json + parameters: + - name: api_key + in: header + required: false + type: string + - name: petId + in: path + description: Pet id to delete + required: true + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + consumes: + - multipart/form-data + produces: + - application/json + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + type: integer + format: int64 + - name: additionalMetadata + in: formData + description: Additional data to pass to server + required: false + type: string + - name: file + in: formData + description: file to upload + required: false + type: file + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + produces: + - application/json + parameters: [] + responses: + '200': + description: successful operation + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: order placed for purchasing the pet + required: true + schema: + $ref: '#/definitions/Order' + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Order' + '400': + description: Invalid Order + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions + operationId: getOrderById + produces: + - application/xml + - application/json + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + type: integer + maximum: 10 + minimum: 1 + format: int64 + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors + operationId: deleteOrder + produces: + - application/xml + - application/json + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + type: integer + minimum: 1 + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Created user object + required: true + schema: + $ref: '#/definitions/User' + responses: + default: + description: successful operation + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: List of user object + required: true + schema: + type: array + items: + $ref: '#/definitions/User' + responses: + default: + description: successful operation + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: List of user object + required: true + schema: + type: array + items: + $ref: '#/definitions/User' + responses: + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + produces: + - application/xml + - application/json + parameters: + - name: username + in: query + description: The user name for login + required: true + type: string + - name: password + in: query + description: The password for login in clear text + required: true + type: string + responses: + '200': + description: successful operation + schema: + type: string + headers: + X-Rate-Limit: + type: integer + format: int32 + description: calls per hour allowed by the user + X-Expires-After: + type: string + format: date-time + description: date in UTC when token expires + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + produces: + - application/xml + - application/json + parameters: [] + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + type: string + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: name that need to be updated + required: true + type: string + - in: body + name: body + description: Updated user object + required: true + schema: + $ref: '#/definitions/User' + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +securityDefinitions: + petstore_auth: + type: oauth2 + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + flow: implicit + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header +definitions: + Order: + type: object + description: "this is def" + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/definitions/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/definitions/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' diff --git a/openapi_parser/tests/__init__.py b/openapi_parser/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_parser/tests/test_parser.py b/openapi_parser/tests/test_parser.py new file mode 100644 index 0000000..312f21a --- /dev/null +++ b/openapi_parser/tests/test_parser.py @@ -0,0 +1,112 @@ +import unittest +import os +import hydrus + +from hydra_python_core.doc_writer import HydraClass +from hydra_openapi_parser import openapi_parser +import yaml + + +def import_doc(): + print("Importing Open Api Documentation ..") + abs_path = os.path.abspath("{}/samples/petstore_openapi.yaml".format(os.path.dirname(openapi_parser.__file__))) + with open(abs_path, 'r') as stream: + try: + return yaml.load(stream) + except yaml.YAMLError as exc: + print(exc) + + +class TestParser(unittest.TestCase): + @classmethod + def setUpClass(self): + doc = import_doc() + self.doc = doc + + @classmethod + def tearDownClass(cls): + pass + + def test_generate_empty_object(self): + """Test if the empty object is being generated correctly """ + object_ = openapi_parser.generate_empty_object() + assert isinstance(object_["prop_definition"], list) + assert isinstance(object_["op_definition"], list) + assert isinstance(object_["class_definition"], type) + assert isinstance(object_["collection"], bool) + + def test_valid_endpoint(self): + """Test if the endpoint is valid and can be parsed """ + path = 'A/B/{id}/C/D' + result = openapi_parser.valid_endpoint(path) + assert result is "False" + assert isinstance(result, str) + path = 'A/B/{id}' + result = openapi_parser.valid_endpoint(path) + assert result is "Collection" + assert isinstance(result, str) + path = 'A/B/id' + result = openapi_parser.valid_endpoint(path) + assert result is "True" + assert isinstance(result, str) + + def test_get_class_name(self): + """Test if the class name is being extracted properly from the path """ + path = "A/B/C/Pet" + path_list = path.split('/') + result = openapi_parser.get_class_name(path_list) + assert result is path_list[3] + assert isinstance(result, str) + + def test_get_data_from_location(self): + """Test if the data from the location given is being fetched correctly""" + path = '#/definitions/Order' + path_list = path.split('/') + result = openapi_parser.get_data_at_location(path_list, self.doc) + response = self.doc["definitions"]["Order"] + assert response is result + + def test_sanitise_path(self): + """Test if the variables can be removed from the path""" + path = "A/B/C/{id}" + result = openapi_parser.sanitise_path(path) + assert isinstance(result, str) + + def test_allow_parameter(self): + """Test if the rules are being followed """ + parameter_block = self.doc["paths"]["/pet"]["post"]["parameters"][0] + result = openapi_parser.allow_parameter(parameter_block) + assert result is True + assert isinstance(result, bool) + parameter_block = self.doc["paths"]["/pet"]["get"]["parameters"][0] + result = openapi_parser.allow_parameter(parameter_block) + assert result is False + assert isinstance(result, bool) + + def test_parse(self): + """Test the hydra documentation """ + result = openapi_parser.parse(self.doc) + assert isinstance(result, dict) + + def test_check_collection(self): + """Test if collections are being identified properly""" + schema_block = { + 'type': 'array', 'items': { + '$ref': '#/definitions/Pet'}} + method = "/Pet" + result = openapi_parser.check_collection(schema_block, method) + assert isinstance(result, bool) + assert result + + def test_check_collection_false(self): + "Test if non collections are identified" + schema = {'$ref': '#/definitions/User'} + method = "/Pet" + result = openapi_parser.check_collection(schema, method) + assert isinstance(result, bool) + assert not result + + +if __name__ == '__main__': + print("Starting tests ..") + unittest.main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6151171 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyYAML==4.2b1 \ No newline at end of file diff --git a/setup.py b/setup.py index e69de29..3909fec 100644 --- a/setup.py +++ b/setup.py @@ -0,0 +1,28 @@ +"""Setup script for Hydra's OpenAPI Parser.""" + +from setuptools import setup, find_packages + +try: # for pip >= 10 + from pip._internal.req import parse_requirements + from pip._internal.download import PipSession +except ImportError: # for pip <= 9.0.3 + from pip.req import parse_requirements + from pip.download import PipSession + + +install_requires = parse_requirements('requirements.txt', session=PipSession()) +dependencies = [str(package.req) for package in install_requires] + +setup(name='hydra-openapi-parser', + include_package_data=True, + version='0.0.1', + description='An OpenAPI parser for W3C HYDRA Draft', + author='W3C HYDRA development group', + author_email='public-hydra@w3.org', + url='https://github.com/HTTP-APIs/hydra-openapi-parser', + python_requires='>=3', + install_requires=dependencies, + packages=find_packages( + exclude=['build', 'dist', 'hydra_openapi_parser.egg-info']), + package_dir={'hydra-openapi-parser':'hydra-openapi-parser'}, + ) \ No newline at end of file