diff --git a/apistar/__init__.py b/apistar/__init__.py index b9237a96..8fca20b2 100644 --- a/apistar/__init__.py +++ b/apistar/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa """ _ ____ ___ ____ _ __/\__ / \ | _ \_ _| / ___|| |_ __ _ _ __ __/\__ diff --git a/apistar/document.py b/apistar/document.py index ae26bf6f..c3765ac2 100644 --- a/apistar/document.py +++ b/apistar/document.py @@ -9,11 +9,11 @@ class Document: def __init__(self, - content: typing.Sequence[typing.Union['Section', 'Link']]=None, - url: str='', - title: str='', - description: str='', - version: str=''): + content: typing.Sequence[typing.Union['Section', 'Link']] = None, + url: str = '', + title: str = '', + description: str = '', + version: str = ''): content = [] if (content is None) else list(content) # Ensure all names within a document are unique. @@ -55,9 +55,9 @@ def walk_links(self): class Section: def __init__(self, name: str, - content: typing.Sequence[typing.Union['Section', 'Link']]=None, - title: str='', - description: str=''): + content: typing.Sequence[typing.Union['Section', 'Link']] = None, + title: str = '', + description: str = ''): content = [] if (content is None) else list(content) # Ensure all names within a section are unique. @@ -104,13 +104,13 @@ class Link: def __init__(self, url: str, method: str, - handler: typing.Callable=None, - name: str='', - encoding: str='', - response: 'Response'=None, - title: str='', - description: str='', - fields: typing.Sequence['Field']=None): + handler: typing.Callable = None, + name: str = '', + encoding: str = '', + response: 'Response' = None, + title: str = '', + description: str = '', + fields: typing.Sequence['Field'] = None): method = method.upper() fields = [] if (fields is None) else list(fields) @@ -173,11 +173,11 @@ class Field: def __init__(self, name: str, location: str, - title: str='', - description: str='', - required: bool=None, - schema: Validator=None, - example: typing.Any=None): + title: str = '', + description: str = '', + required: bool = None, + schema: Validator = None, + example: typing.Any = None): assert location in ('path', 'query', 'body', 'cookie', 'header', 'formData') if required is None: required = True if location in ('path', 'body') else False @@ -194,7 +194,7 @@ def __init__(self, class Response: - def __init__(self, encoding: str, status_code: int=200, schema: Validator=None): + def __init__(self, encoding: str, status_code: int = 200, schema: Validator = None): self.encoding = encoding self.status_code = status_code self.schema = schema diff --git a/apistar/schemas/openapi.py b/apistar/schemas/openapi.py index 6dda2173..f3a57935 100644 --- a/apistar/schemas/openapi.py +++ b/apistar/schemas/openapi.py @@ -5,6 +5,7 @@ from apistar.compat import dict_type from apistar.document import Document, Field, Link, Section from apistar.schemas.jsonschema import JSON_SCHEMA, JSONSchema +from apistar.validators import Validator SCHEMA_REF = validators.Object( properties={'$ref': validators.String(pattern='^#/components/schemas/')} @@ -15,6 +16,9 @@ RESPONSE_REF = validators.Object( properties={'$ref': validators.String(pattern='^#/components/responses/')} ) +PARAMETERS_REF = validators.Object( + properties={'$ref': validators.String(pattern='^#/components/parameters/')} +) OPEN_API = validators.Object( def_name='OpenAPI', @@ -116,7 +120,7 @@ ('patch', validators.Ref('Operation')), ('trace', validators.Ref('Operation')), ('servers', validators.Array(items=validators.Ref('Server'))), - ('parameters', validators.Array(items=validators.Ref('Parameter'))), # TODO: | ReferenceObject + ('parameters', validators.Array(items=validators.Ref('Parameter') | PARAMETERS_REF)), ], pattern_properties={ '^x-': validators.Any(), @@ -130,7 +134,7 @@ ('description', validators.String(format='textarea')), ('externalDocs', validators.Ref('ExternalDocumentation')), ('operationId', validators.String()), - ('parameters', validators.Array(items=validators.Ref('Parameter'))), # TODO: | ReferenceObject + ('parameters', validators.Array(items=validators.Ref('Parameter') | PARAMETERS_REF)), ('requestBody', REQUESTBODY_REF | validators.Ref('RequestBody')), # TODO: RequestBody | ReferenceObject ('responses', validators.Ref('Responses')), # TODO: 'callbacks' @@ -314,7 +318,8 @@ def load(self, data): version = lookup(data, ['info', 'version']) base_url = lookup(data, ['servers', 0, 'url']) schema_definitions = self.get_schema_definitions(data) - content = self.get_content(data, base_url, schema_definitions) + parameter_definitions = self.get_parameter_definitions(data, schema_definitions) + content = self.get_content(data, base_url, schema_definitions, parameter_definitions) return Document(title=title, description=description, version=version, url=base_url, content=content) @@ -326,7 +331,26 @@ def get_schema_definitions(self, data): definitions[key].def_name = key return definitions - def get_content(self, data, base_url, schema_definitions): + def get_validator(self, schema, schema_definitions): + if schema is None or isinstance(schema, Validator): + return schema + + if '$ref' in schema: + ref = schema['$ref'][len('#/components/schemas/'):] + return schema_definitions.get(ref) + else: + return JSONSchema().decode_from_data_structure(schema) + + def get_parameter_definitions(self, data, schema_definitions): + definitions = {} + parameters = lookup(data, ['components', 'parameters'], {}) + for key, value in parameters.items(): + value["schema"] = self.get_validator(value.get("schema"), schema_definitions) + value["schema"].def_name = key + definitions[key] = value + return definitions + + def get_content(self, data, base_url, schema_definitions, parameter_definitions): """ Return all the links in the document, layed out by tag and operationId. """ @@ -340,7 +364,8 @@ def get_content(self, data, base_url, schema_definitions): } for operation, operation_info in operations.items(): tag = lookup(operation_info, ['tags', 0]) - link = self.get_link(base_url, path, path_info, operation, operation_info, schema_definitions) + link = self.get_link( + base_url, path, path_info, operation, operation_info, schema_definitions, parameter_definitions) if link is None: continue @@ -357,7 +382,7 @@ def get_content(self, data, base_url, schema_definitions): ] return links + sections - def get_link(self, base_url, path, path_info, operation, operation_info, schema_definitions): + def get_link(self, base_url, path, path_info, operation, operation_info, schema_definitions, parameter_definitions): """ Return a single link in the document. """ @@ -379,7 +404,7 @@ def get_link(self, base_url, path, path_info, operation, operation_info, schema_ parameters += operation_info.get('parameters', []) fields = [ - self.get_field(parameter, schema_definitions) + self.get_field(parameter, schema_definitions, parameter_definitions) for parameter in parameters ] @@ -409,24 +434,21 @@ def get_link(self, base_url, path, path_info, operation, operation_info, schema_ encoding=encoding ) - def get_field(self, parameter, schema_definitions): + def get_field(self, parameter, schema_definitions, parameter_definitions): """ Return a single field in a link. """ + if '$ref' in parameter: + ref = parameter['$ref'][len('#/components/parameters/'):] + parameter = parameter_definitions.get(ref) + name = parameter.get('name') location = parameter.get('in') description = parameter.get('description') required = parameter.get('required', False) - schema = parameter.get('schema') + schema = self.get_validator(parameter.get('schema'), schema_definitions) example = parameter.get('example') - if schema is not None: - if '$ref' in schema: - ref = schema['$ref'][len('#/components/schemas/'):] - schema = schema_definitions.get(ref) - else: - schema = JSONSchema().decode_from_data_structure(schema) - return Field( name=name, location=location, diff --git a/apistar/tokenize/tokens.py b/apistar/tokenize/tokens.py index e2370aa8..47f1e9cb 100644 --- a/apistar/tokenize/tokens.py +++ b/apistar/tokenize/tokens.py @@ -9,7 +9,7 @@ def __init__(self, value, start_index: int, end_index: int, content=None): self._content = content def get_value(self): - raise NotImplemented() # pragma: nocover + raise NotImplementedError() # pragma: nocover @property def start(self): diff --git a/testcases/openapi/parameters-example.yaml b/testcases/openapi/parameters-example.yaml new file mode 100644 index 00000000..ce14d3e2 --- /dev/null +++ b/testcases/openapi/parameters-example.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.2 +info: + title: Parameters Example + version: 1.0.0 +paths: + /parameters: + get: + operationId: getParameters + parameters: + - $ref: '#/components/parameters/Simple' + - $ref: '#/components/parameters/SimpleSchemaReference' + - name: inline + in: query + description: 'Query parameter defined inline' + required: false + schema: + type: string + - name: inline_schema_reference + in: query + description: 'Query parameter defined inline but with referenced schema' + required: false + schema: + $ref: '#/components/schemas/SchemaReference' + responses: + '200': + description: success + content: + application/json: + schema: + type: object + properties: + status: + type: string +components: + parameters: + Simple: + name: simple + in: query + description: 'Simple query parameters defined as component' + required: false + schema: + type: string + SimpleSchemaReference: + name: simple_schema_reference + in: query + description: 'Simple query parameters defined as component with schema referenced' + required: false + schema: + $ref: '#/components/schemas/SchemaReference' + schemas: + SchemaReference: + type: string \ No newline at end of file diff --git a/tests/client/test_client.py b/tests/client/test_client.py index cc7dce7d..05cf6d94 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -10,7 +10,8 @@ @app.route('/path-param/{value}') -def path_param(request, value): +def path_param(request): + value = request.path_params['value'] return JSONResponse({'value': value}) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index a48a39ba..ac2a1b54 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -9,6 +9,7 @@ # 'testcases/openapi/callback-example.yaml', # 'testcases/openapi/link-example.yaml', 'testcases/openapi/petstore-expanded.yaml', + 'testcases/openapi/parameters-example.yaml', 'testcases/openapi/petstore.yaml', 'testcases/openapi/uspto.yaml', ]