diff --git a/setup.py b/setup.py index 0672eb5..11481e0 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ "jsonschema >= 2.5.1", "m2r >= 0.2", "picobox >= 2.2", + "deepmerge >= 0.1", ], project_urls={ "Documentation": "https://sphinxcontrib-openapi.readthedocs.io/", diff --git a/sphinxcontrib/openapi/renderers/_httpdomain.py b/sphinxcontrib/openapi/renderers/_httpdomain.py index e5eb6ef..4746c47 100644 --- a/sphinxcontrib/openapi/renderers/_httpdomain.py +++ b/sphinxcontrib/openapi/renderers/_httpdomain.py @@ -1,10 +1,12 @@ """OpenAPI spec renderer.""" import collections +import copy import functools import http.client import json +import deepmerge import docutils.parsers.rst.directives as directives import m2r import requests @@ -107,6 +109,77 @@ def _iterexamples(media_types, example_preference, examples_from_schemas): yield content_type, example +def _get_markers_from_object(oas_object, schema): + """Retrieve a bunch of OAS object markers.""" + + markers = [] + + schema_type = _get_schema_type(schema) + if schema_type: + if schema.get("format"): + schema_type = f"{schema_type}:{schema['format']}" + elif schema.get("enum"): + schema_type = f"{schema_type}:enum" + markers.append(schema_type) + elif schema.get("enum"): + markers.append("enum") + + if oas_object.get("required"): + markers.append("required") + + if oas_object.get("deprecated"): + markers.append("deprecated") + + if schema.get("deprecated"): + markers.append("deprecated") + + return markers + + +def _is_json_mimetype(mimetype): + """Returns 'True' if a given mimetype implies JSON data.""" + + return any( + [ + mimetype == "application/json", + mimetype.startswith("application/") and mimetype.endswith("+json"), + ] + ) + + +def _is_2xx_status(status_code): + """Returns 'True' if a given status code is one of successful.""" + + return str(status_code).startswith("2") + + +def _get_schema_type(schema): + """Retrieve schema type either by reading 'type' or guessing.""" + + # There are a lot of OpenAPI specs out there that may lack 'type' property + # in their schemas. I fount no explanations on what is expected behaviour + # in this case neither in OpenAPI nor in JSON Schema specifications. Thus + # let's assume what everyone assumes, and try to guess schema type at least + # for two most popular types: 'object' and 'array'. + if "type" not in schema: + if "properties" in schema: + schema_type = "object" + elif "items" in schema: + schema_type = "array" + else: + schema_type = None + else: + schema_type = schema["type"] + return schema_type + + +_merge_mappings = deepmerge.Merger( + [(collections.Mapping, deepmerge.strategy.dict.DictStrategies("merge"))], + ["override"], + ["override"], +).merge + + class HttpdomainRenderer(abc.RestructuredTextRenderer): """Render OpenAPI v3 using `sphinxcontrib-httpdomain` extension.""" @@ -123,6 +196,7 @@ class HttpdomainRenderer(abc.RestructuredTextRenderer): "request-example-preference": None, "response-example-preference": None, "generate-examples-from-schemas": directives.flag, + "no-json-schema-description": directives.flag, } def __init__(self, state, options): @@ -151,6 +225,7 @@ def __init__(self, state, options): "response-example-preference", self._example_preference ) self._generate_example_from_schema = "generate-examples-from-schemas" in options + self._json_schema_description = "no-json-schema-description" not in options def render_restructuredtext_markup(self, spec): """Spec render entry point.""" @@ -229,7 +304,6 @@ def render_parameter(self, parameter): kinds = CaseInsensitiveDict( {"path": "param", "query": "queryparam", "header": "reqheader"} ) - markers = [] schema = parameter.get("schema", {}) if "content" in parameter: @@ -247,18 +321,6 @@ def render_parameter(self, parameter): ) return - if schema.get("type"): - type_ = schema["type"] - if schema.get("format"): - type_ = f"{type_}:{schema['format']}" - markers.append(type_) - - if parameter.get("required"): - markers.append("required") - - if parameter.get("deprecated"): - markers.append("deprecated") - yield f":{kinds[parameter['in']]} {parameter['name']}:" if parameter.get("description"): @@ -266,6 +328,7 @@ def render_parameter(self, parameter): self._convert_markup(parameter["description"]).strip().splitlines() ) + markers = _get_markers_from_object(parameter, schema) if markers: markers = ", ".join(markers) yield f":{kinds[parameter['in']]}type {parameter['name']}: {markers}" @@ -273,6 +336,15 @@ def render_parameter(self, parameter): def render_request_body(self, request_body, endpoint, method): """Render OAS operation's requestBody.""" + if self._json_schema_description: + for content_type, content in request_body["content"].items(): + if _is_json_mimetype(content_type) and content.get("schema"): + yield from self.render_json_schema_description( + content["schema"], "req" + ) + yield "" + break + yield from self.render_request_body_example(request_body, endpoint, method) yield "" @@ -304,6 +376,18 @@ def render_request_body_example(self, request_body, endpoint, method): def render_responses(self, responses): """Render OAS operation's responses.""" + if self._json_schema_description: + for status_code, response in responses.items(): + if _is_2xx_status(status_code): + for content_type, content in response.get("content", {}).items(): + if _is_json_mimetype(content_type) and content.get("schema"): + yield from self.render_json_schema_description( + content["schema"], "res" + ) + yield "" + break + break + for status_code, response in responses.items(): # Due to the way how YAML spec is parsed, status code may be # infered as integer. In order to spare some cycles on type @@ -321,7 +405,7 @@ def render_response(self, status_code, response): if "content" in response and status_code in self._response_examples_for: yield "" yield from indented( - self.render_response_content(response["content"], status_code) + self.render_response_example(response["content"], status_code) ) if "headers" in response: @@ -342,7 +426,6 @@ def render_response(self, status_code, response): .splitlines() ) - markers = [] schema = header_value.get("schema", {}) if "content" in header_value: # According to OpenAPI v3 spec, 'content' in this case may @@ -350,23 +433,12 @@ def render_response(self, status_code, response): # list is not expensive and should be acceptable. schema = list(header_value["content"].values())[0].get("schema", {}) - if schema.get("type"): - type_ = schema["type"] - if schema.get("format"): - type_ = f"{type_}:{schema['format']}" - markers.append(type_) - - if header_value.get("required"): - markers.append("required") - - if header_value.get("deprecated"): - markers.append("deprecated") - + markers = _get_markers_from_object(header_value, schema) if markers: markers = ", ".join(markers) yield f":resheadertype {header_name}: {markers}" - def render_response_content(self, media_type, status_code): + def render_response_example(self, media_type, status_code): # OpenAPI 3.0 spec may contain more than one response media type, and # each media type may contain more than one example. Rendering all # invariants normally is not an option because the result will be hard @@ -413,3 +485,136 @@ def render_response_content(self, media_type, status_code): yield f" Content-Type: {content_type}" yield f"" yield from indented(example.splitlines()) + + def render_json_schema_description(self, schema, req_or_res): + """Render JSON schema's description.""" + + def _resolve_combining_schema(schema): + if "oneOf" in schema: + # The part with merging is a vague one since I only found a + # single 'oneOf' example where such merging was assumed, and no + # explanations in the spec itself. + merged_schema = schema.copy() + merged_schema.update(merged_schema.pop("oneOf")[0]) + return merged_schema + + elif "anyOf" in schema: + # The part with merging is a vague one since I only found a + # single 'oneOf' example where such merging was assumed, and no + # explanations in the spec itself. + merged_schema = schema.copy() + merged_schema.update(merged_schema.pop("anyOf")[0]) + return merged_schema + + elif "allOf" in schema: + # Since the item is represented by all schemas from the array, + # the best we can do is to render them all at once + # sequentially. Please note, the only way the end result will + # ever make sense is when all schemas from the array are of + # object type. + merged_schema = schema.copy() + for item in merged_schema.pop("allOf"): + merged_schema = _merge_mappings(merged_schema, copy.deepcopy(item)) + return merged_schema + + elif "not" in schema: + # Eh.. do nothing because I have no idea what can we do. + return {} + + return schema + + def _traverse_schema(schema, name, is_required=False): + schema_type = _get_schema_type(schema) + + if {"oneOf", "anyOf", "allOf"} & schema.keys(): + # Since an item can represented by either or any schema from + # the array of schema in case of `oneOf` and `anyOf` + # respectively, the best we can do for them is to render the + # first found variant. In other words, we are going to traverse + # only a single schema variant and leave the rest out. This is + # by design and it was decided so in order to keep produced + # description clear and simple. + yield from _traverse_schema(_resolve_combining_schema(schema), name) + + elif "not" in schema: + yield name, {}, is_required + + elif schema_type == "object": + if name: + yield name, schema, is_required + + required = set(schema.get("required", [])) + + for key, value in schema.get("properties", {}).items(): + # In case of the first recursion call, when 'name' is an + # empty string, we should go with 'key' only in order to + # avoid leading dot at the beginning. + yield from _traverse_schema( + value, + f"{name}.{key}" if name else key, + is_required=key in required, + ) + + elif schema_type == "array": + yield from _traverse_schema(schema["items"], f"{name}[]") + + elif "enum" in schema: + yield name, schema, is_required + + elif schema_type is not None: + yield name, schema, is_required + + schema = _resolve_combining_schema(schema) + schema_type = _get_schema_type(schema) + + # On root level, httpdomain supports only 'object' and 'array' response + # types. If it's something else, let's do not even try to render it. + if schema_type not in {"object", "array"}: + return + + # According to httpdomain's documentation, 'reqjsonobj' is an alias for + # 'reqjson'. However, since the same name is passed as a type directive + # internally, it actually can be used to specify its type. The same + # goes for 'resjsonobj'. + directives_map = { + "req": { + "object": ("reqjson", "reqjsonobj"), + "array": ("reqjsonarr", "reqjsonarrtype"), + }, + "res": { + "object": ("resjson", "resjsonobj"), + "array": ("resjsonarr", "resjsonarrtype"), + }, + } + + # These httpdomain's fields always expect either JSON Object or JSON + # Array. No primitive types are allowed as input. + directive, typedirective = directives_map[req_or_res][schema_type] + + # Since we use JSON array specific httpdomain directives if a schema + # we're about to render is an array, there's no need to render that + # array in the first place. + if schema_type == "array": + schema = schema["items"] + + # Even if a root element is an array, items it contain must not be + # of a primitive types. + if _get_schema_type(schema) not in {"object", "array"}: + return + + for name, schema, is_required in _traverse_schema(schema, ""): + yield f":{directive} {name}:" + + if schema.get("description"): + yield from indented( + self._convert_markup(schema["description"]).strip().splitlines() + ) + + markers = _get_markers_from_object({}, schema) + + if is_required: + markers.append("required") + + if markers: + markers = ", ".join(markers) + yield f":{typedirective} {name}: {markers}" diff --git a/tests/renderers/httpdomain/rendered/v2.0/petstore-expanded.yaml.rst b/tests/renderers/httpdomain/rendered/v2.0/petstore-expanded.yaml.rst index d32d5a1..e9e6384 100644 --- a/tests/renderers/httpdomain/rendered/v2.0/petstore-expanded.yaml.rst +++ b/tests/renderers/httpdomain/rendered/v2.0/petstore-expanded.yaml.rst @@ -8,6 +8,7 @@ :queryparam limit: maximum number of results to return :queryparamtype limit: integer:int32 + :statuscode 200: pet response @@ -18,6 +19,18 @@ Creates a new pet in the store. Duplicates are allowed + :reqjson name: + :reqjsonobj name: string, required + :reqjson tag: + :reqjsonobj tag: string + + + :resjson name: + :resjsonobj name: string + :resjson tag: + :resjsonobj tag: string + :resjson id: + :resjsonobj id: integer:int64, required :statuscode 200: pet response @@ -32,6 +45,13 @@ :param id: ID of pet to fetch :paramtype id: integer:int64, required + :resjson name: + :resjsonobj name: string + :resjson tag: + :resjsonobj tag: string + :resjson id: + :resjsonobj id: integer:int64, required + :statuscode 200: pet response diff --git a/tests/renderers/httpdomain/rendered/v2.0/petstore.yaml.rst b/tests/renderers/httpdomain/rendered/v2.0/petstore.yaml.rst index 899b03a..63a2914 100644 --- a/tests/renderers/httpdomain/rendered/v2.0/petstore.yaml.rst +++ b/tests/renderers/httpdomain/rendered/v2.0/petstore.yaml.rst @@ -5,6 +5,13 @@ :queryparam limit: How many items to return at one time (max 100) :queryparamtype limit: integer:int32 + :resjsonarr id: + :resjsonarrtype id: integer:int64, required + :resjsonarr name: + :resjsonarrtype name: string, required + :resjsonarr tag: + :resjsonarrtype tag: string + :statuscode 200: A paged array of pets @@ -31,6 +38,13 @@ :param petId: The id of the pet to retrieve :paramtype petId: string, required + :resjsonarr id: + :resjsonarrtype id: integer:int64, required + :resjsonarr name: + :resjsonarrtype name: string, required + :resjsonarr tag: + :resjsonarrtype tag: string + :statuscode 200: Expected response to a valid request diff --git a/tests/renderers/httpdomain/rendered/v2.0/uber.json.rst b/tests/renderers/httpdomain/rendered/v2.0/uber.json.rst index 57efd9d..dc27468 100644 --- a/tests/renderers/httpdomain/rendered/v2.0/uber.json.rst +++ b/tests/renderers/httpdomain/rendered/v2.0/uber.json.rst @@ -10,6 +10,22 @@ :queryparam longitude: Longitude component of location. :queryparamtype longitude: number:double, required + :resjsonarr product_id: + Unique identifier representing a specific product for a given latitude & longitude. For example, uberX in San Francisco will have a different product_id than uberX in Los Angeles. + :resjsonarrtype product_id: string + :resjsonarr description: + Description of product. + :resjsonarrtype description: string + :resjsonarr display_name: + Display name of product. + :resjsonarrtype display_name: string + :resjsonarr capacity: + Capacity of product. For example, 4 people. + :resjsonarrtype capacity: string + :resjsonarr image: + Image URL representing the product. + :resjsonarrtype image: string + :statuscode 200: An array of products @@ -38,6 +54,28 @@ :queryparam end_longitude: Longitude component of end location. :queryparamtype end_longitude: number:double, required + :resjsonarr product_id: + Unique identifier representing a specific product for a given latitude & longitude. For example, uberX in San Francisco will have a different product_id than uberX in Los Angeles + :resjsonarrtype product_id: string + :resjsonarr currency_code: + `ISO 4217 `_ currency code. + :resjsonarrtype currency_code: string + :resjsonarr display_name: + Display name of product. + :resjsonarrtype display_name: string + :resjsonarr estimate: + Formatted string of estimate in local currency of the start location. Estimate could be a range, a single number (flat rate) or "Metered" for TAXI. + :resjsonarrtype estimate: string + :resjsonarr low_estimate: + Lower bound of the estimated price. + :resjsonarrtype low_estimate: number + :resjsonarr high_estimate: + Upper bound of the estimated price. + :resjsonarrtype high_estimate: number + :resjsonarr surge_multiplier: + Expected surge multiplier. Surge is active if surge_multiplier is greater than 1. Price estimate already factors in the surge multiplier. + :resjsonarrtype surge_multiplier: number + :statuscode 200: An array of price estimates by product @@ -62,6 +100,22 @@ :queryparam product_id: Unique identifier representing a specific product for a given latitude & longitude. :queryparamtype product_id: string + :resjsonarr product_id: + Unique identifier representing a specific product for a given latitude & longitude. For example, uberX in San Francisco will have a different product_id than uberX in Los Angeles. + :resjsonarrtype product_id: string + :resjsonarr description: + Description of product. + :resjsonarrtype description: string + :resjsonarr display_name: + Display name of product. + :resjsonarrtype display_name: string + :resjsonarr capacity: + Capacity of product. For example, 4 people. + :resjsonarrtype capacity: string + :resjsonarr image: + Image URL representing the product. + :resjsonarrtype image: string + :statuscode 200: An array of products @@ -74,6 +128,22 @@ The User Profile endpoint returns information about the Uber user that has authorized with the application. + :resjson first_name: + First name of the Uber user. + :resjsonobj first_name: string + :resjson last_name: + Last name of the Uber user. + :resjsonobj last_name: string + :resjson email: + Email address of the Uber user + :resjsonobj email: string + :resjson picture: + Image URL of the Uber user. + :resjsonobj picture: string + :resjson promo_code: + Promo code of the Uber user. + :resjsonobj promo_code: string + :statuscode 200: Profile information for a user @@ -96,6 +166,21 @@ :queryparam limit: Number of items to retrieve. Default is 5, maximum is 100. :queryparamtype limit: integer:int32 + :resjson offset: + Position in pagination. + :resjsonobj offset: integer:int32 + :resjson limit: + Number of items to retrieve (100 max). + :resjsonobj limit: integer:int32 + :resjson count: + Total number of items available. + :resjsonobj count: integer:int32 + :resjson history[]: + :resjsonobj history[]: object + :resjson history[].uuid: + Unique identifier for the activity + :resjsonobj history[].uuid: string + :statuscode 200: History information for the given user diff --git a/tests/renderers/httpdomain/rendered/v2.0/uber.yaml.rst b/tests/renderers/httpdomain/rendered/v2.0/uber.yaml.rst index 57efd9d..4b14064 100644 --- a/tests/renderers/httpdomain/rendered/v2.0/uber.yaml.rst +++ b/tests/renderers/httpdomain/rendered/v2.0/uber.yaml.rst @@ -10,6 +10,22 @@ :queryparam longitude: Longitude component of location. :queryparamtype longitude: number:double, required + :resjsonarr product_id: + Unique identifier representing a specific product for a given latitude & longitude. For example, uberX in San Francisco will have a different product_id than uberX in Los Angeles. + :resjsonarrtype product_id: string + :resjsonarr description: + Description of product. + :resjsonarrtype description: string + :resjsonarr display_name: + Display name of product. + :resjsonarrtype display_name: string + :resjsonarr capacity: + Capacity of product. For example, 4 people. + :resjsonarrtype capacity: integer + :resjsonarr image: + Image URL representing the product. + :resjsonarrtype image: string + :statuscode 200: An array of products @@ -38,6 +54,28 @@ :queryparam end_longitude: Longitude component of end location. :queryparamtype end_longitude: number:double, required + :resjsonarr product_id: + Unique identifier representing a specific product for a given latitude & longitude. For example, uberX in San Francisco will have a different product_id than uberX in Los Angeles + :resjsonarrtype product_id: string + :resjsonarr currency_code: + `ISO 4217 `_ currency code. + :resjsonarrtype currency_code: string + :resjsonarr display_name: + Display name of product. + :resjsonarrtype display_name: string + :resjsonarr estimate: + Formatted string of estimate in local currency of the start location. Estimate could be a range, a single number (flat rate) or "Metered" for TAXI. + :resjsonarrtype estimate: string + :resjsonarr low_estimate: + Lower bound of the estimated price. + :resjsonarrtype low_estimate: number + :resjsonarr high_estimate: + Upper bound of the estimated price. + :resjsonarrtype high_estimate: number + :resjsonarr surge_multiplier: + Expected surge multiplier. Surge is active if surge_multiplier is greater than 1. Price estimate already factors in the surge multiplier. + :resjsonarrtype surge_multiplier: number + :statuscode 200: An array of price estimates by product @@ -62,6 +100,22 @@ :queryparam product_id: Unique identifier representing a specific product for a given latitude & longitude. :queryparamtype product_id: string + :resjsonarr product_id: + Unique identifier representing a specific product for a given latitude & longitude. For example, uberX in San Francisco will have a different product_id than uberX in Los Angeles. + :resjsonarrtype product_id: string + :resjsonarr description: + Description of product. + :resjsonarrtype description: string + :resjsonarr display_name: + Display name of product. + :resjsonarrtype display_name: string + :resjsonarr capacity: + Capacity of product. For example, 4 people. + :resjsonarrtype capacity: integer + :resjsonarr image: + Image URL representing the product. + :resjsonarrtype image: string + :statuscode 200: An array of products @@ -74,6 +128,22 @@ The User Profile endpoint returns information about the Uber user that has authorized with the application. + :resjson first_name: + First name of the Uber user. + :resjsonobj first_name: string + :resjson last_name: + Last name of the Uber user. + :resjsonobj last_name: string + :resjson email: + Email address of the Uber user + :resjsonobj email: string + :resjson picture: + Image URL of the Uber user. + :resjsonobj picture: string + :resjson promo_code: + Promo code of the Uber user. + :resjsonobj promo_code: string + :statuscode 200: Profile information for a user @@ -96,6 +166,21 @@ :queryparam limit: Number of items to retrieve. Default is 5, maximum is 100. :queryparamtype limit: integer:int32 + :resjson offset: + Position in pagination. + :resjsonobj offset: integer:int32 + :resjson limit: + Number of items to retrieve (100 max). + :resjsonobj limit: integer:int32 + :resjson count: + Total number of items available. + :resjsonobj count: integer:int32 + :resjson history[]: + :resjsonobj history[]: object + :resjson history[].uuid: + Unique identifier for the activity + :resjsonobj history[].uuid: string + :statuscode 200: History information for the given user diff --git a/tests/renderers/httpdomain/rendered/v3.0/petstore-expanded.yaml.rst b/tests/renderers/httpdomain/rendered/v3.0/petstore-expanded.yaml.rst index 533e567..69adbda 100644 --- a/tests/renderers/httpdomain/rendered/v3.0/petstore-expanded.yaml.rst +++ b/tests/renderers/httpdomain/rendered/v3.0/petstore-expanded.yaml.rst @@ -11,6 +11,7 @@ :queryparam limit: maximum number of results to return :queryparamtype limit: integer:int32 + :statuscode 200: pet response @@ -21,6 +22,18 @@ Creates a new pet in the store. Duplicates are allowed + :reqjson name: + :reqjsonobj name: string, required + :reqjson tag: + :reqjsonobj tag: string + + + :resjson name: + :resjsonobj name: string + :resjson tag: + :resjsonobj tag: string + :resjson id: + :resjsonobj id: integer:int64, required :statuscode 200: pet response @@ -35,6 +48,13 @@ :param id: ID of pet to fetch :paramtype id: integer:int64, required + :resjson name: + :resjsonobj name: string + :resjson tag: + :resjsonobj tag: string + :resjson id: + :resjsonobj id: integer:int64, required + :statuscode 200: pet response diff --git a/tests/renderers/httpdomain/rendered/v3.0/petstore.yaml.rst b/tests/renderers/httpdomain/rendered/v3.0/petstore.yaml.rst index 899b03a..63a2914 100644 --- a/tests/renderers/httpdomain/rendered/v3.0/petstore.yaml.rst +++ b/tests/renderers/httpdomain/rendered/v3.0/petstore.yaml.rst @@ -5,6 +5,13 @@ :queryparam limit: How many items to return at one time (max 100) :queryparamtype limit: integer:int32 + :resjsonarr id: + :resjsonarrtype id: integer:int64, required + :resjsonarr name: + :resjsonarrtype name: string, required + :resjsonarr tag: + :resjsonarrtype tag: string + :statuscode 200: A paged array of pets @@ -31,6 +38,13 @@ :param petId: The id of the pet to retrieve :paramtype petId: string, required + :resjsonarr id: + :resjsonarrtype id: integer:int64, required + :resjsonarr name: + :resjsonarrtype name: string, required + :resjsonarr tag: + :resjsonarrtype tag: string + :statuscode 200: Expected response to a valid request diff --git a/tests/renderers/httpdomain/rendered/v3.0/uspto.yaml.rst b/tests/renderers/httpdomain/rendered/v3.0/uspto.yaml.rst index 827f5cd..6cd0c0a 100644 --- a/tests/renderers/httpdomain/rendered/v3.0/uspto.yaml.rst +++ b/tests/renderers/httpdomain/rendered/v3.0/uspto.yaml.rst @@ -2,6 +2,23 @@ **List available data sets** + :resjson total: + :resjsonobj total: integer + :resjson apis[]: + :resjsonobj apis[]: object + :resjson apis[].apiKey: + To be used as a dataset parameter value + :resjsonobj apis[].apiKey: string + :resjson apis[].apiVersionNumber: + To be used as a version parameter value + :resjsonobj apis[].apiVersionNumber: string + :resjson apis[].apiUrl: + The URL describing the dataset's fields + :resjsonobj apis[].apiUrl: string:uriref + :resjson apis[].apiDocumentationUrl: + A URL to the API console for each API + :resjsonobj apis[].apiDocumentationUrl: string:uriref + :statuscode 200: Returns a list of data sets @@ -40,6 +57,7 @@ :param version: Version of the dataset. :paramtype version: string, required + :statuscode 200: The dataset API for the given version is found and it is accessible to consume. @@ -59,6 +77,7 @@ Name of the dataset. In this case, the default value is oa_citations :paramtype dataset: string, required + :statuscode 200: successful operation diff --git a/tests/renderers/httpdomain/test_render_json_schema_description.py b/tests/renderers/httpdomain/test_render_json_schema_description.py new file mode 100644 index 0000000..e25ca53 --- /dev/null +++ b/tests/renderers/httpdomain/test_render_json_schema_description.py @@ -0,0 +1,1366 @@ +"""OpenAPI spec renderer: render_json_schema_description.""" + +import textwrap + +import pytest + +from sphinxcontrib.openapi import renderers + + +def textify(generator): + return "\n".join(generator) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_root_object( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON object in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + prop_a: + type: string + prop_b: + type: object + properties: + eggs: + type: boolean + prop_c: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} prop_a: + :{typedirective} prop_a: string + :{directive} prop_b: + :{typedirective} prop_b: object + :{directive} prop_b.eggs: + :{typedirective} prop_b.eggs: boolean + :{directive} prop_c: + :{typedirective} prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjsonarr", "reqjsonarrtype", id="req"), + pytest.param("res", "resjsonarr", "resjsonarrtype", id="res"), + ], +) +def test_render_json_schema_description_root_array( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON array in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: array + items: + type: object + properties: + prop: + type: string + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} prop: + :{typedirective} prop: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +@pytest.mark.parametrize( + ["schema_type"], + [ + pytest.param("null"), + pytest.param("boolean"), + pytest.param("number"), + pytest.param("string"), + pytest.param("integer"), + ], +) +def test_render_json_schema_description_root_unsupported( + testrenderer, oas_fragment, schema_type, req_or_res, directive, typedirective +): + """JSON schema description is not generated for unsupported type in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + f""" + type: {schema_type} + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + """\ + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_root_any_of_object( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for anyOf JSON object in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + anyOf: + - type: object + properties: + prop_a: + type: string + prop_b: + type: number + - type: object + properties: + prop_c: + type: object + properties: + eggs: + type: boolean + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} prop_a: + :{typedirective} prop_a: string + :{directive} prop_b: + :{typedirective} prop_b: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjsonarr", "reqjsonarrtype", id="req"), + pytest.param("res", "resjsonarr", "resjsonarrtype", id="res"), + ], +) +def test_render_json_schema_description_root_any_of_array( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for anyOf JSON array in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + anyOf: + - type: array + items: + type: object + properties: + prop: + type: string + - type: array + items: + type: object + properties: + prop: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} prop: + :{typedirective} prop: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +@pytest.mark.parametrize( + ["schema_type"], + [ + pytest.param("null"), + pytest.param("boolean"), + pytest.param("number"), + pytest.param("string"), + pytest.param("integer"), + ], +) +def test_render_json_schema_description_root_any_of_unsupported( + testrenderer, oas_fragment, schema_type, req_or_res, directive, typedirective +): + """JSON schema description is not generated for anyOf unsupported type in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + f""" + anyOf: + - type: {schema_type} + - type: object + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + """\ + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_root_one_of_object( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for oneOf JSON object in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + oneOf: + - type: object + properties: + prop_a: + type: string + prop_b: + type: number + - type: object + properties: + prop_c: + type: object + properties: + eggs: + type: boolean + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} prop_a: + :{typedirective} prop_a: string + :{directive} prop_b: + :{typedirective} prop_b: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjsonarr", "reqjsonarrtype", id="req"), + pytest.param("res", "resjsonarr", "resjsonarrtype", id="res"), + ], +) +def test_render_json_schema_description_root_one_of_array( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for oneOf JSON array in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + oneOf: + - type: array + items: + type: object + properties: + prop: + type: string + - type: array + items: + type: object + properties: + prop: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} prop: + :{typedirective} prop: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +@pytest.mark.parametrize( + ["schema_type"], + [ + pytest.param("null"), + pytest.param("boolean"), + pytest.param("number"), + pytest.param("string"), + pytest.param("integer"), + ], +) +def test_render_json_schema_description_root_one_of_unsupported( + testrenderer, oas_fragment, schema_type, req_or_res, directive, typedirective +): + """JSON schema description is not generated for oneOf unsupported type in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + f""" + oneOf: + - type: {schema_type} + - type: object + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + """\ + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_root_all_of_object( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for allOf in root.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + allOf: + - properties: + name: + properties: + first: + type: string + age: + type: integer + - properties: + name: + properties: + last: + type: string + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} name: + :{typedirective} name: object + :{directive} name.first: + :{typedirective} name.first: string + :{directive} name.last: + :{typedirective} name.last: string + :{directive} age: + :{typedirective} age: integer + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +@pytest.mark.parametrize( + ["schema_type"], + [ + pytest.param("null"), + pytest.param("boolean"), + pytest.param("number"), + pytest.param("string"), + pytest.param("integer"), + ], +) +def test_render_json_schema_description_primitive( + testrenderer, oas_fragment, schema_type, req_or_res, directive, typedirective +): + """JSON schema description is generated for primitive types.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + f""" + type: object + properties: + some_key: + type: "{schema_type}" + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: {schema_type} + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_object( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON object.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + type: object + properties: + prop_a: + type: string + prop_b: + type: object + properties: + eggs: + type: boolean + prop_c: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + :{typedirective} root: object + :{directive} root.prop_a: + :{typedirective} root.prop_a: string + :{directive} root.prop_b: + :{typedirective} root.prop_b: object + :{directive} root.prop_b.eggs: + :{typedirective} root.prop_b.eggs: boolean + :{directive} root.prop_c: + :{typedirective} root.prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_object_implicit( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for implicit JSON object.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + properties: + prop_a: + type: string + prop_b: + properties: + eggs: + type: boolean + prop_c: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + :{typedirective} root: object + :{directive} root.prop_a: + :{typedirective} root.prop_a: string + :{directive} root.prop_b: + :{typedirective} root.prop_b: object + :{directive} root.prop_b.eggs: + :{typedirective} root.prop_b.eggs: boolean + :{directive} root.prop_c: + :{typedirective} root.prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_array( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON array.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + type: array + items: + type: object + properties: + prop_a: + type: string + prop_b: + type: array + items: + type: number + prop_c: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root[]: + :{typedirective} root[]: object + :{directive} root[].prop_a: + :{typedirective} root[].prop_a: string + :{directive} root[].prop_b[]: + :{typedirective} root[].prop_b[]: number + :{directive} root[].prop_c: + :{typedirective} root[].prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_array_implicit( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for implicit JSON array.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + items: + type: object + properties: + prop_a: + type: string + prop_b: + items: + type: number + prop_c: + type: number + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root[]: + :{typedirective} root[]: object + :{directive} root[].prop_a: + :{typedirective} root[].prop_a: string + :{directive} root[].prop_b[]: + :{typedirective} root[].prop_b[]: number + :{directive} root[].prop_c: + :{typedirective} root[].prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_format( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for formatted types.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + created_at: + type: string + format: date-time + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} created_at: + :{typedirective} created_at: string:date-time + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_deprecated( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated with deprecated marker.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + created_at: + type: string + deprecated: true + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} created_at: + :{typedirective} created_at: string, deprecated + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_required( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON object w/ required marker.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + type: object + properties: + prop_a: + type: string + prop_b: + type: object + properties: + eggs: + type: boolean + required: [eggs] + prop_c: + type: number + required: [prop_a] + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + :{typedirective} root: object + :{directive} root.prop_a: + :{typedirective} root.prop_a: string, required + :{directive} root.prop_b: + :{typedirective} root.prop_b: object + :{directive} root.prop_b.eggs: + :{typedirective} root.prop_b.eggs: boolean, required + :{directive} root.prop_c: + :{typedirective} root.prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_deprecated_and_required( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON object w/ deprecated & required markers.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + type: object + properties: + prop_a: + type: string + prop_b: + type: object + properties: + eggs: + type: boolean + deprecated: true + required: [eggs] + prop_c: + type: number + required: [prop_a] + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + :{typedirective} root: object + :{directive} root.prop_a: + :{typedirective} root.prop_a: string, required + :{directive} root.prop_b: + :{typedirective} root.prop_b: object + :{directive} root.prop_b.eggs: + :{typedirective} root.prop_b.eggs: boolean, deprecated, required + :{directive} root.prop_c: + :{typedirective} root.prop_c: number + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_description( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated with description.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + description: a resource representation + properties: + created_at: + type: string + description: a resource creation time + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} created_at: + a resource creation time + :{typedirective} created_at: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_description_commonmark_default( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated with CommonMark description by default.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + description: a resource representation + properties: + created_at: + type: string + description: a `resource` creation __time__ + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} created_at: + a ``resource`` creation **time** + :{typedirective} created_at: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_description_commonmark( + fakestate, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated with CommonMark description.""" + + testrenderer = renderers.HttpdomainRenderer(fakestate, {"markup": "commonmark"}) + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + description: a resource representation + properties: + created_at: + type: string + description: a `resource` creation __time__ + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} created_at: + a ``resource`` creation **time** + :{typedirective} created_at: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_description_restructuredtext( + fakestate, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated with reStructuredText description.""" + + testrenderer = renderers.HttpdomainRenderer( + fakestate, {"markup": "restructuredtext"} + ) + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + description: a resource representation + properties: + created_at: + type: string + description: a `resource` creation __time__ + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} created_at: + a `resource` creation __time__ + :{typedirective} created_at: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_any_of( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for anyOf.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + f""" + type: object + properties: + some_key: + anyOf: + - type: integer + - type: string + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: integer + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_one_of( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for oneOf.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + some_key: + oneOf: + - type: integer + - type: string + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: integer + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_all_of( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for allOf.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + person: + allOf: + - properties: + name: + properties: + first: + type: string + age: + type: integer + - properties: + name: + properties: + last: + type: string + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} person: + :{typedirective} person: object + :{directive} person.name: + :{typedirective} person.name: object + :{directive} person.name.first: + :{typedirective} person.name.first: string + :{directive} person.name.last: + :{typedirective} person.name.last: string + :{directive} person.age: + :{typedirective} person.age: integer + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_all_of_logical_impossible( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for allOf that is logical impossible.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + some_key: + allOf: + - type: integer + - type: string + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_any_of_shared_type( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for anyOf w/ shared 'type'.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + some_key: + type: string + anyOf: + - minLength: 3 + - maxLength: 5 + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_one_of_shared_type( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for oneOf w/ shared 'type'.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + some_key: + type: string + oneOf: + - minLength: 3 + - maxLength: 5 + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_all_of_shared_type( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for allOf w/ shared 'type'.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + some_key: + type: string + alOf: + - minLength: 3 + - maxLength: 5 + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} some_key: + :{typedirective} some_key: string + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_not( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON *not*.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + not: + type: boolean + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_enum( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON enum.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + type: string + enum: + - foo + - bar + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + :{typedirective} root: string:enum + """.rstrip() + ) + + +@pytest.mark.parametrize( + ["req_or_res", "directive", "typedirective"], + [ + pytest.param("req", "reqjson", "reqjsonobj", id="req"), + pytest.param("res", "resjson", "resjsonobj", id="res"), + ], +) +def test_render_json_schema_description_enum_wo_type( + testrenderer, oas_fragment, req_or_res, directive, typedirective +): + """JSON schema description is generated for JSON enum wo/ type.""" + + markup = textify( + testrenderer.render_json_schema_description( + oas_fragment( + """ + type: object + properties: + root: + enum: + - foo + - bar + """ + ), + req_or_res, + ) + ) + assert markup == textwrap.dedent( + f"""\ + :{directive} root: + :{typedirective} root: enum + """.rstrip() + ) diff --git a/tests/renderers/httpdomain/test_render_request_body.py b/tests/renderers/httpdomain/test_render_request_body.py index e69de29..2794cbb 100644 --- a/tests/renderers/httpdomain/test_render_request_body.py +++ b/tests/renderers/httpdomain/test_render_request_body.py @@ -0,0 +1,106 @@ +"""OpenAPI spec renderer: render_request_body.""" + +import textwrap + +import pytest + +from sphinxcontrib.openapi import renderers + + +def textify(generator): + return "\n".join(generator) + + +@pytest.mark.parametrize( + "content_type", ["application/json", "application/foobar+json"] +) +def test_render_request_body_schema_description( + testrenderer, oas_fragment, content_type +): + """JSON schema description is rendered.""" + + markup = textify( + testrenderer.render_request_body( + oas_fragment( + f""" + content: + {content_type}: + schema: + properties: + foo: + type: string + bar: + type: integer + """ + ), + "/evidences/{evidenceId}", + "POST", + ) + ) + assert markup == textwrap.dedent( + """\ + :reqjson foo: + :reqjsonobj foo: string + :reqjson bar: + :reqjsonobj bar: integer + + """ + ) + + +def test_render_request_body_schema_description_non_json(testrenderer, oas_fragment): + """JSON schema is not rendered for non JSON mimetype.""" + + markup = textify( + testrenderer.render_request_body( + oas_fragment( + """ + content: + text/csv: + schema: + properties: + foo: + type: string + bar: + type: integer + """ + ), + "/evidences/{evidenceId}", + "POST", + ) + ) + assert markup == textwrap.dedent( + """\ + """ + ) + + +def test_render_request_body_schema_description_turned_off(fakestate, oas_fragment): + """JSON schema description is not rendered b/c feature is off.""" + + testrenderer = renderers.HttpdomainRenderer( + fakestate, {"no-json-schema-description": True}, + ) + + markup = textify( + testrenderer.render_request_body( + oas_fragment( + """ + content: + application/json: + schema: + properties: + foo: + type: string + bar: + type: integer + """ + ), + "/evidences/{evidenceId}", + "POST", + ) + ) + assert markup == textwrap.dedent( + """\ + """ + ) diff --git a/tests/renderers/httpdomain/test_render_response_content.py b/tests/renderers/httpdomain/test_render_response_example.py similarity index 88% rename from tests/renderers/httpdomain/test_render_response_content.py rename to tests/renderers/httpdomain/test_render_response_example.py index dee34b6..09e4f55 100644 --- a/tests/renderers/httpdomain/test_render_response_content.py +++ b/tests/renderers/httpdomain/test_render_response_example.py @@ -1,4 +1,4 @@ -"""OpenAPI spec renderer: render_response_content.""" +"""OpenAPI spec renderer: render_response_example.""" import textwrap @@ -109,11 +109,11 @@ def textify(generator): ), ], ) -def test_render_response_content_example(testrenderer, oas_fragment, media_type): +def test_render_response_example(testrenderer, oas_fragment, media_type): """Path response's example is rendered.""" markup = textify( - testrenderer.render_response_content(oas_fragment(media_type), "200") + testrenderer.render_response_example(oas_fragment(media_type), "200") ) assert markup == textwrap.dedent( """\ @@ -130,11 +130,11 @@ def test_render_response_content_example(testrenderer, oas_fragment, media_type) ) -def test_render_response_content_example_1st_from_examples(testrenderer, oas_fragment): +def test_render_response_example_1st_from_examples(testrenderer, oas_fragment): """Path response's first example is rendered.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -166,13 +166,11 @@ def test_render_response_content_example_1st_from_examples(testrenderer, oas_fra ) -def test_render_response_content_example_1st_from_media_type( - testrenderer, oas_fragment -): +def test_render_response_example_1st_from_media_type(testrenderer, oas_fragment): """Path response's example from first media type is rendered.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/plain: @@ -205,7 +203,7 @@ def test_render_response_content_example_1st_from_media_type( ["example_preference_key"], [pytest.param("response-example-preference"), pytest.param("example-preference")], ) -def test_render_response_content_example_preference( +def test_render_response_example_preference( fakestate, example_preference_key, oas_fragment ): """Path response's example from preferred media type is rendered.""" @@ -215,7 +213,7 @@ def test_render_response_content_example_preference( ) markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -249,7 +247,7 @@ def test_render_response_content_example_preference( ["example_preference_key"], [pytest.param("response-example-preference"), pytest.param("example-preference")], ) -def test_render_response_content_example_preference_complex( +def test_render_response_example_preference_complex( fakestate, example_preference_key, oas_fragment ): """Path response's example from preferred media type is rendered.""" @@ -259,7 +257,7 @@ def test_render_response_content_example_preference_complex( ) markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/csv: @@ -292,7 +290,7 @@ def test_render_response_content_example_preference_complex( ) -def test_render_response_content_example_preference_priority(fakestate, oas_fragment): +def test_render_response_example_preference_priority(fakestate, oas_fragment): """Path response's example from preferred media type is rendered.""" testrenderer = renderers.HttpdomainRenderer( @@ -304,7 +302,7 @@ def test_render_response_content_example_preference_priority(fakestate, oas_frag ) markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -335,7 +333,7 @@ def test_render_response_content_example_preference_priority(fakestate, oas_frag @responses.activate -def test_render_response_content_example_external(testrenderer, oas_fragment): +def test_render_response_example_external(testrenderer, oas_fragment): """Path response's example can be retrieved from external location.""" responses.add( @@ -346,7 +344,7 @@ def test_render_response_content_example_external(testrenderer, oas_fragment): ) markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -371,7 +369,7 @@ def test_render_response_content_example_external(testrenderer, oas_fragment): @responses.activate -def test_render_response_content_example_external_errored_next_example( +def test_render_response_example_external_errored_next_example( testrenderer, caplog, oas_fragment ): """Path response's example fallbacks on next when external cannot be retrieved.""" @@ -381,7 +379,7 @@ def test_render_response_content_example_external_errored_next_example( ) markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -408,7 +406,7 @@ def test_render_response_content_example_external_errored_next_example( @responses.activate -def test_render_response_content_example_external_errored_next_media_type( +def test_render_response_example_external_errored_next_media_type( testrenderer, oas_fragment, caplog ): """Path response's example fallbacks on next when external cannot be retrieved.""" @@ -418,7 +416,7 @@ def test_render_response_content_example_external_errored_next_media_type( ) markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -444,11 +442,11 @@ def test_render_response_content_example_external_errored_next_media_type( ) -def test_render_response_content_example_content_type(testrenderer, oas_fragment): +def test_render_response_example_content_type(testrenderer, oas_fragment): """Path response's example can render something other than application/json.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/csv: @@ -473,11 +471,11 @@ def test_render_response_content_example_content_type(testrenderer, oas_fragment ) -def test_render_response_content_example_noop(testrenderer, oas_fragment): +def test_render_response_example_noop(testrenderer, oas_fragment): """Path response's example is not rendered if there's nothing to render.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ application/json: @@ -500,13 +498,13 @@ def test_render_response_content_example_noop(testrenderer, oas_fragment): pytest.param("422", "Unprocessable Entity", id="422"), ], ) -def test_render_response_content_status_code( +def test_render_response_status_code( testrenderer, oas_fragment, status_code, status_text ): """Path response's example is rendered with proper status code.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/csv: @@ -539,13 +537,13 @@ def test_render_response_content_status_code( pytest.param("4XX", "400", "Bad Request", id="4XX"), ], ) -def test_render_response_content_status_code_range( +def test_render_response_status_code_range( testrenderer, oas_fragment, status_range, status_code, status_text ): """Path response's example is rendered with proper status range.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/csv: @@ -578,13 +576,13 @@ def test_render_response_content_status_code_range( pytest.param("422", "Unprocessable Entity", id="422"), ], ) -def test_render_response_content_status_code_int( +def test_render_response_status_code_int( testrenderer, oas_fragment, status_code, status_text ): """Path response's example is rendered with proper status code.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/csv: @@ -609,11 +607,11 @@ def test_render_response_content_status_code_int( ) -def test_render_response_content_status_code_default(testrenderer, oas_fragment): +def test_render_response_status_code_default(testrenderer, oas_fragment): """Path response's example is rendered when default is passed.""" markup = textify( - testrenderer.render_response_content( + testrenderer.render_response_example( oas_fragment( """ text/csv: diff --git a/tests/renderers/httpdomain/test_render_responses.py b/tests/renderers/httpdomain/test_render_responses.py index c071aed..f80437d 100644 --- a/tests/renderers/httpdomain/test_render_responses.py +++ b/tests/renderers/httpdomain/test_render_responses.py @@ -2,6 +2,8 @@ import textwrap +from sphinxcontrib.openapi import renderers + def textify(generator): return "\n".join(generator) @@ -87,3 +89,154 @@ def test_render_responses_many_items(testrenderer, oas_fragment): An evidence not found. """.rstrip() ) + + +def test_render_responses_json_schema_description(testrenderer, oas_fragment): + """JSON schema description is rendered.""" + + markup = textify( + testrenderer.render_responses( + oas_fragment( + """ + '200': + description: An evidence. + content: + application/json: + schema: + properties: + foo: + type: string + bar: + type: integer + """ + ) + ) + ) + assert markup == textwrap.dedent( + """\ + :resjson foo: + :resjsonobj foo: string + :resjson bar: + :resjsonobj bar: integer + + :statuscode 200: + An evidence. + """ + ) + + +def test_render_responses_json_schema_description_4xx(testrenderer, oas_fragment): + """JSON schema description is rendered.""" + + markup = textify( + testrenderer.render_responses( + oas_fragment( + """ + '400': + description: An evidence. + content: + application/json: + schema: + properties: + foo: + type: string + bar: + type: integer + """ + ) + ) + ) + assert markup == textwrap.dedent( + """\ + :statuscode 400: + An evidence. + """.rstrip() + ) + + +def test_render_responses_json_schema_description_first_2xx(testrenderer, oas_fragment): + """JSON schema description is rendered.""" + + markup = textify( + testrenderer.render_responses( + oas_fragment( + """ + '400': + description: An error. + content: + application/json: + schema: + properties: + aaa: + type: string + '200': + description: An evidence. + content: + application/json: + schema: + properties: + foo: + type: string + bar: + type: integer + '201': + description: An evidence created. + content: + application/json: + schema: + properties: + bbb: + type: string + """ + ) + ) + ) + assert markup == textwrap.dedent( + """\ + :resjson foo: + :resjsonobj foo: string + :resjson bar: + :resjsonobj bar: integer + + :statuscode 400: + An error. + :statuscode 200: + An evidence. + + :statuscode 201: + An evidence created. + """ + ) + + +def test_render_responses_json_schema_description_turned_off(fakestate, oas_fragment): + """JSON schema description is not rendered b/c feature is off.""" + + testrenderer = renderers.HttpdomainRenderer( + fakestate, {"no-json-schema-description": True}, + ) + + markup = textify( + testrenderer.render_responses( + oas_fragment( + """ + '200': + description: An evidence. + content: + application/json: + schema: + properties: + foo: + type: string + bar: + type: integer + """ + ) + ) + ) + assert markup == textwrap.dedent( + """\ + :statuscode 200: + An evidence. + """ + ) diff --git a/tests/renderers/httpdomain/test_render_restructuredtext_markup.py b/tests/renderers/httpdomain/test_render_restructuredtext_markup.py index 4ec37c9..0df437f 100644 --- a/tests/renderers/httpdomain/test_render_restructuredtext_markup.py +++ b/tests/renderers/httpdomain/test_render_restructuredtext_markup.py @@ -113,6 +113,7 @@ def test_oas2_complete(testrenderer, oas_fragment): :paramtype username: string, required :queryparam id: :queryparamtype id: string + :statuscode 200: a response description @@ -162,6 +163,7 @@ def test_oas2_schema_example(testrenderer, oas_fragment): an operation description + :statuscode 200: a response description @@ -216,6 +218,7 @@ def test_oas2_complete_generate_examples_from_schema(fakestate, oas_fragment): an operation description + :statuscode 200: a response description @@ -339,6 +342,7 @@ def test_oas3_complete(testrenderer, oas_fragment): :paramtype username: string, required :queryparam id: :queryparamtype id: string + :statuscode 200: a response description @@ -388,6 +392,7 @@ def test_oas3_schema_example(testrenderer, oas_fragment): an operation description + :statuscode 200: a response description @@ -442,6 +447,7 @@ def test_oas3_generate_examples_from_schema(fakestate, oas_fragment): an operation description + :statuscode 200: a response description