diff --git a/.flake8 b/.flake8 index 508fa68..2fa2ca9 100644 --- a/.flake8 +++ b/.flake8 @@ -6,4 +6,5 @@ max-complexity = 18 per-file-ignores = tests/test_v2.py: E501 tests/test_v3.py: E501 + tests/test_v31.py: E501 select = B,C,E,F,W,T4,B9 diff --git a/openapidocs/v3.py b/openapidocs/v3.py index 93cb53a..8429768 100644 --- a/openapidocs/v3.py +++ b/openapidocs/v3.py @@ -1,7 +1,7 @@ """ This module defines classes that can be used to generate OpenAPI Documentation version 3. -https://swagger.io/specification/ +https://swagger.io/specification/v3/ """ from abc import ABC from dataclasses import dataclass diff --git a/openapidocs/v31.py b/openapidocs/v31.py new file mode 100644 index 0000000..859e204 --- /dev/null +++ b/openapidocs/v31.py @@ -0,0 +1,392 @@ +""" +This module defines classes that can be used to generate OpenAPI Documentation +version 3.1. +https://swagger.io/specification/ +""" +from abc import ABC +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, List, Optional, Union + +from openapidocs.common import OpenAPIRoot, normalize_dict + +from .common import OpenAPIElement + + +class ParameterLocation(Enum): + QUERY = "query" + HEADER = "header" + PATH = "path" + COOKIE = "cookie" + + +class ValueType(Enum): + ARRAY = "array" + BOOLEAN = "boolean" + INTEGER = "integer" + NUMBER = "number" + OBJECT = "object" + STRING = "string" + NULL = "null" + + +class ValueFormat(Enum): + BASE64 = "base64" + BINARY = "binary" + BYTE = "byte" + DATE = "date" + DATETIME = "date-time" + TIME = "time" + DURATION = "duration" + DOUBLE = "double" + FLOAT = "float" + INT32 = "int32" + INT64 = "int64" + PASSWORD = "password" + EMAIL = "email" + IDNEMAIL = "idn-email" + UUID = "uuid" + PARTIALTIME = "partial-time" + HOSTNAME = "hostname" + IDNHOSTNAME = "idn-hostname" + IPV4 = "ipv4" + IPV6 = "ipv6" + URI = "uri" + URIREFERENCE = "uri-reference" + IRI = "iri" + IRIREFERENCE = "iri-reference" + URITEMPLATE = "uri-template" + JSONPOINTER = "json-pointer" + RELATIVEJSONPOINTER = "relative-json-pointer" + REGEX = "regex" + + +class SecuritySchemeType(Enum): + APIKEY = "apiKey" + HTTP = "http" + OAUTH = "oauth2" + OAUTH2 = "oauth2" + OIDC = "openIdConnect" + OPENIDCONNECT = "openIdConnect" + MUTUALTLS = "mutualTLS" + + +@dataclass +class Contact(OpenAPIElement): + name: Optional[str] = None + url: Optional[str] = None + email: Optional[str] = None + + +@dataclass +class ExternalDocs(OpenAPIElement): + url: str + description: Optional[str] = None + + +@dataclass +class License(OpenAPIElement): + name: str + identifier: Optional[str] = None + url: Optional[str] = None + + +@dataclass +class Info(OpenAPIElement): + title: str + version: str + summary: Optional[str] = None + description: Optional[str] = None + terms_of_service: Optional[str] = None + contact: Optional[Contact] = None + license: Optional[License] = None + + +@dataclass +class ServerVariable(OpenAPIElement): + default: str + description: Optional[str] = None + enum: Optional[List[str]] = None + + +@dataclass +class Server(OpenAPIElement): + url: str + description: Optional[str] = None + variables: Optional[Dict[str, ServerVariable]] = None + + +@dataclass +class XML(OpenAPIElement): + name: Optional[str] = None + namespace: Optional[str] = None + prefix: Optional[str] = None + attribute: Optional[bool] = None + wrapped: Optional[bool] = None + + +@dataclass +class Discriminator(OpenAPIElement): + property_name: str + mapping: Optional[Dict[str, str]] = None + + +@dataclass +class Schema(OpenAPIElement): + type: Union[None, str, ValueType, List[Union[None, str, ValueType]]] = None + format: Union[None, str, ValueFormat] = None + required: Optional[List[str]] = None + properties: Optional[Dict[str, Union["Schema", "Reference"]]] = None + default: Optional[Any] = None + deprecated: Optional[bool] = None + example: Any = None + external_docs: Optional[ExternalDocs] = None + ref: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + content_encoding: Optional[str] = None + content_media_type: Optional[str] = None + max_length: Optional[float] = None + min_length: Optional[float] = None + maximum: Optional[float] = None + minimum: Optional[float] = None + xml: Optional[XML] = None + items: Union[None, "Schema", "Reference"] = None + enum: Optional[List[str]] = None + discriminator: Optional[Discriminator] = None + all_of: Optional[List[Union["Schema", "Reference"]]] = None + any_of: Optional[List[Union["Schema", "Reference"]]] = None + one_of: Optional[List[Union["Schema", "Reference"]]] = None + not_: Optional[List[Union["Schema", "Reference"]]] = None + + +@dataclass +class Header(OpenAPIElement): + description: Optional[str] = None + schema: Union[None, Schema, "Reference"] = None + + +@dataclass +class Example(OpenAPIElement): + summary: Optional[str] = None + description: Optional[str] = None + value: Any = None + external_value: Optional[str] = None + + +@dataclass +class Reference(OpenAPIElement): + ref: str + summary: Optional[str] = None + description: Optional[str] = None + + def to_obj(self) -> Dict[str, str]: + obj = {"$ref": self.ref} + if self.summary: + obj["summary"] = self.summary + if self.description: + obj["description"] = self.description + return obj + + +@dataclass +class Encoding(OpenAPIElement): + content_type: Optional[str] = None + headers: Optional[Dict[str, Union[Header, Reference]]] = None + style: Optional[str] = None + explode: Optional[bool] = None + allow_reserved: Optional[bool] = None + + +@dataclass +class Link(OpenAPIElement): + operation_ref: Optional[str] = None + operation_id: Optional[str] = None + parameters: Optional[Dict[str, Any]] = None + request_body: Any = None + description: Optional[str] = None + server: Optional[Server] = None + + +@dataclass +class MediaType(OpenAPIElement): + schema: Union[None, Schema, Reference] = None + examples: Optional[Dict[str, Union[Example, Reference]]] = None + encoding: Optional[Dict[str, Encoding]] = None + + +@dataclass +class Response(OpenAPIElement): + description: Optional[str] = None + headers: Optional[Dict[str, Union[Header, Reference]]] = None + content: Optional[Dict[str, Union[MediaType, Reference]]] = None + links: Optional[Dict[str, Union[Link, Reference]]] = None + + +@dataclass +class Parameter(OpenAPIElement): + name: str + in_: ParameterLocation + schema: Union[None, Schema, Reference] = None + content: Optional[Dict[str, MediaType]] = None + description: Optional[str] = None + required: Optional[bool] = None + deprecated: Optional[bool] = None + allow_empty_value: Optional[bool] = None + example: Optional[Any] = None + examples: Optional[Dict[str, Union[Example, Reference]]] = None + + +@dataclass +class RequestBody(OpenAPIElement): + content: Dict[str, MediaType] + description: Optional[str] = None + required: Optional[bool] = None + + +@dataclass +class SecurityRequirement(OpenAPIElement): + name: str + value: List[str] + + def to_obj(self): + return {self.name: self.value} + + +@dataclass +class Operation(OpenAPIElement): + responses: Dict[str, Response] + tags: Optional[List[str]] = None + summary: Optional[str] = None + description: Optional[str] = None + external_docs: Optional[ExternalDocs] = None + operation_id: Optional[str] = None + parameters: Optional[List[Union[Parameter, Reference]]] = None + request_body: Union[None, RequestBody, Reference] = None + callbacks: Optional[Dict[str, Union["Callback", Reference]]] = None + deprecated: Optional[bool] = None + security: Optional[List[SecurityRequirement]] = None + servers: Optional[List[Server]] = None + + +@dataclass +class PathItem(OpenAPIElement): + summary: Optional[str] = None + ref: Optional[str] = None + description: Optional[str] = None + get: Optional[Operation] = None + put: Optional[Operation] = None + post: Optional[Operation] = None + delete: Optional[Operation] = None + options: Optional[Operation] = None + head: Optional[Operation] = None + patch: Optional[Operation] = None + trace: Optional[Operation] = None + servers: Optional[List[Server]] = None + parameters: Optional[List[Union[Parameter, Reference]]] = None + + +@dataclass +class Callback(OpenAPIElement): + expression: str + path: Union[PathItem, Reference] + + def to_obj(self): + return {self.expression: normalize_dict(self.path)} + + +@dataclass +class OAuthFlow(OpenAPIElement): + scopes: Dict[str, str] + authorization_url: Optional[str] = None + token_url: Optional[str] = None + refresh_url: Optional[str] = None + + +@dataclass +class OAuthFlows(OpenAPIElement): + implicit: Optional[OAuthFlow] = None + password: Optional[OAuthFlow] = None + client_credentials: Optional[OAuthFlow] = None + authorization_code: Optional[OAuthFlow] = None + + +class SecurityScheme(OpenAPIElement, ABC): + """Abstract security scheme""" + + +@dataclass +class HTTPSecurity(SecurityScheme): + scheme: str + type: SecuritySchemeType = SecuritySchemeType.HTTP + description: Optional[str] = None + bearer_format: Optional[str] = None + + +@dataclass +class APIKeySecurity(SecurityScheme): + name: str + in_: ParameterLocation + type: SecuritySchemeType = SecuritySchemeType.APIKEY + description: Optional[str] = None + + +@dataclass +class OAuth2Security(SecurityScheme): + flows: OAuthFlows + type: SecuritySchemeType = SecuritySchemeType.OAUTH2 + description: Optional[str] = None + + +@dataclass +class OpenIdConnectSecurity(SecurityScheme): + open_id_connect_url: str + type: SecuritySchemeType = SecuritySchemeType.OPENIDCONNECT + description: Optional[str] = None + + +@dataclass +class Components(OpenAPIElement): + schemas: Optional[Dict[str, Union[Schema, Reference]]] = None + responses: Optional[Dict[str, Union[Response, Reference]]] = None + parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None + examples: Optional[Dict[str, Union[Example, Reference]]] = None + request_bodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None + headers: Optional[Dict[str, Union[Header, Reference]]] = None + security_schemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None + links: Optional[Dict[str, Union[Link, Reference]]] = None + callbacks: Optional[Dict[str, Union[Callback, Reference]]] = None + path_items: Optional[Dict[str, Union[PathItem, Reference]]] = None + + +@dataclass +class Tag(OpenAPIElement): + name: str + description: Optional[str] = None + external_docs: Optional[ExternalDocs] = None + + +@dataclass +class Security(OpenAPIElement): + requirements: List[SecurityRequirement] + optional: bool = False + + def to_obj(self): + items = [normalize_dict(item) for item in self.requirements] + if self.optional: + items.insert(0, {}) + return items + + +@dataclass +class OpenAPI(OpenAPIRoot): + openapi: str = "3.1.0" + info: Optional[Info] = None + json_schema_dialect: str = "https://json-schema.org/draft/2020-12/schema" + paths: Optional[Dict[str, PathItem]] = None + servers: Optional[List[Server]] = None + components: Optional[Components] = None + tags: Optional[List[Tag]] = None + security: Optional[Security] = None + external_docs: Optional[ExternalDocs] = None + webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None diff --git a/tests/test_v31.py b/tests/test_v31.py new file mode 100644 index 0000000..9177abd --- /dev/null +++ b/tests/test_v31.py @@ -0,0 +1,2429 @@ +import os +from abc import abstractmethod +from dataclasses import dataclass +from datetime import date, datetime, time +from enum import Enum +from textwrap import dedent +from typing import Any, Optional, Type +from uuid import UUID + +import pytest +import yaml +from pydantic import BaseModel + +from openapidocs.common import Format, Serializer +from openapidocs.mk.contents import JSONContentWriter +from openapidocs.v31 import ( + APIKeySecurity, + Callback, + Components, + Contact, + Example, + HTTPSecurity, + Info, + License, + MediaType, + OAuth2Security, + OAuthFlow, + OAuthFlows, + OpenAPI, + OpenIdConnectSecurity, + Operation, + Parameter, + ParameterLocation, + PathItem, + Reference, + RequestBody, + Response, + Schema, + Security, + SecurityRequirement, + Server, + ServerVariable, + ValueFormat, + ValueType, +) +from tests.common import debug_result + + +@dataclass +class ExampleOne: + snake_case: str + ner_label: str + + +class ExampleOneAlt(BaseModel): + snake_case: str + ner_label: str + + +class CatType(Enum): + EUROPEAN = "european" + PERSIAN = "persian" + + +@dataclass +class Cat: + id: int + name: str + type: CatType + + +@dataclass +class Foo: + id: int + hello: Optional[str] + foo: Optional[float] + + +@dataclass +class BuiltInsExample: + one: time + two: date + three: datetime + four: bytes + + +@dataclass +class FooParent: + id: UUID + foo: Foo + + +class TestItem: + @abstractmethod + def get_instance(self) -> Any: + ... + + def expected_yaml(self) -> str: + return dedent(self.yaml()).strip() + + def expected_json(self) -> str: + return dedent(self.json()).strip() + + @abstractmethod + def json(self) -> str: + ... + + @abstractmethod + def yaml(self) -> str: + ... + + +class ParameterExample1(TestItem): + def get_instance(self) -> Parameter: + return Parameter("search", ParameterLocation.QUERY) + + def yaml(self) -> str: + return """ + name: search + in: query + """ + + def json(self) -> str: + return """ + { + "name": "search", + "in": "query" + } + """ + + +class OpenAPIExample1(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Cats API", version="1.0.0"), + paths={ + "/": PathItem("summary"), + "/api/v1/cats": PathItem( + "", + get=Operation( + responses={"200": Response("Gets a list of cats")}, + parameters=[ + Parameter( + "page", + ParameterLocation.QUERY, + schema=Schema(type=["integer", "null"]), + ), + Parameter( + "size", + ParameterLocation.QUERY, + schema=Schema(type="integer"), + ), + Parameter( + "search", + ParameterLocation.QUERY, + schema=Schema(type="string"), + ), + ], + ), + ), + }, + servers=[ + Server("https://dev.foo.com", "Development server"), + Server("https://test.foo.com", "Test server"), + Server("https://foo.com", "Production server"), + ], + ) + + def yaml(self) -> str: + return """ + openapi: 3.1.0 + info: + title: Cats API + version: 1.0.0 + jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema + paths: + /: + summary: summary + /api/v1/cats: + summary: '' + get: + responses: + '200': + description: Gets a list of cats + parameters: + - name: page + in: query + schema: + type: + - integer + - 'null' + - name: size + in: query + schema: + type: integer + - name: search + in: query + schema: + type: string + servers: + - url: https://dev.foo.com + description: Development server + - url: https://test.foo.com + description: Test server + - url: https://foo.com + description: Production server + """ + + def json(self) -> str: + return """ + { + "openapi": "3.1.0", + "info": { + "title": "Cats API", + "version": "1.0.0" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "summary": "summary" + }, + "/api/v1/cats": { + "summary": "", + "get": { + "responses": { + "200": { + "description": "Gets a list of cats" + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": [ + "integer", + "null" + ] + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + } + ] + } + } + }, + "servers": [ + { + "url": "https://dev.foo.com", + "description": "Development server" + }, + { + "url": "https://test.foo.com", + "description": "Test server" + }, + { + "url": "https://foo.com", + "description": "Production server" + } + ] + } + """ + + +class OpenAPIExample2(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Cats API", version="1.0.0"), + json_schema_dialect="https://json-schema.org/draft/2020-12/schema", + paths={ + "/": PathItem("summary"), + "/api/v1/cats": PathItem( + "", + get=Operation( + responses={"200": Response("Gets a list of cats")}, + parameters=[ + Parameter( + "page", + ParameterLocation.QUERY, + schema=Schema(type="integer"), + ), + Parameter( + "size", + ParameterLocation.QUERY, + schema=Schema(type="integer"), + ), + Parameter( + "search", + ParameterLocation.QUERY, + schema=Schema(type="string"), + ), + ], + ), + ), + }, + servers=[ + Server("https://dev.foo.com", "Development server"), + Server("https://test.foo.com", "Test server"), + Server("https://foo.com", "Production server"), + ], + components=Components( + security_schemes={ + "BasicAuth": HTTPSecurity(scheme="basic"), + "BearerAuth": HTTPSecurity(scheme="bearer"), + "ApiKeyAuth": APIKeySecurity( + in_=ParameterLocation.HEADER, + name="X-API-Key", + ), + "OpenID": OpenIdConnectSecurity( + open_id_connect_url="https://example.com/.well-known/openid-configuration" + ), + "OAuth2": OAuth2Security( + flows=OAuthFlows( + authorization_code=OAuthFlow( + { + "read": "Grants read access", + "write": "Grants write access", + "admin": "Grants access to admin operations", + }, + authorization_url="https://example.com/oauth/authorize", + token_url="https://example.com/oauth/token", + ) + ) + ), + } + ), + security=Security( + [ + SecurityRequirement("ApiKeyAuth", []), + SecurityRequirement("OAuth2", ["read", "write"]), + ], + optional=True, + ), + ) + + def yaml(self) -> str: + return """ + openapi: 3.1.0 + info: + title: Cats API + version: 1.0.0 + jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema + paths: + /: + summary: summary + /api/v1/cats: + summary: '' + get: + responses: + '200': + description: Gets a list of cats + parameters: + - name: page + in: query + schema: + type: integer + - name: size + in: query + schema: + type: integer + - name: search + in: query + schema: + type: string + servers: + - url: https://dev.foo.com + description: Development server + - url: https://test.foo.com + description: Test server + - url: https://foo.com + description: Production server + components: + securitySchemes: + BasicAuth: + scheme: basic + type: http + BearerAuth: + scheme: bearer + type: http + ApiKeyAuth: + name: X-API-Key + in: header + type: apiKey + OpenID: + openIdConnectUrl: https://example.com/.well-known/openid-configuration + type: openIdConnect + OAuth2: + flows: + authorizationCode: + scopes: + read: Grants read access + write: Grants write access + admin: Grants access to admin operations + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + type: oauth2 + security: + - {} + - ApiKeyAuth: [] + - OAuth2: + - read + - write + """ + + def json(self) -> str: + return """ + { + "openapi": "3.1.0", + "info": { + "title": "Cats API", + "version": "1.0.0" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "summary": "summary" + }, + "/api/v1/cats": { + "summary": "", + "get": { + "responses": { + "200": { + "description": "Gets a list of cats" + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "size", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + } + ] + } + } + }, + "servers": [ + { + "url": "https://dev.foo.com", + "description": "Development server" + }, + { + "url": "https://test.foo.com", + "description": "Test server" + }, + { + "url": "https://foo.com", + "description": "Production server" + } + ], + "components": { + "securitySchemes": { + "BasicAuth": { + "scheme": "basic", + "type": "http" + }, + "BearerAuth": { + "scheme": "bearer", + "type": "http" + }, + "ApiKeyAuth": { + "name": "X-API-Key", + "in": "header", + "type": "apiKey" + }, + "OpenID": { + "openIdConnectUrl": "https://example.com/.well-known/openid-configuration", + "type": "openIdConnect" + }, + "OAuth2": { + "flows": { + "authorizationCode": { + "scopes": { + "read": "Grants read access", + "write": "Grants write access", + "admin": "Grants access to admin operations" + }, + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token" + } + }, + "type": "oauth2" + } + } + }, + "security": [ + {}, + { + "ApiKeyAuth": [] + }, + { + "OAuth2": [ + "read", + "write" + ] + } + ] + } + """ + + +class OpenAPIExample3(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Weather API", version="0.0.0-alpha"), + paths={ + "/weather": PathItem( + summary="Call current weather data for one location", + description="""Access current weather data for any location on Earth.""", + get=Operation( + tags=["Current Weather Data"], + operation_id="CurrentWeatherData", + parameters=[ + Reference("#/components/parameters/q"), + Reference("#/components/parameters/id"), + Reference("#/components/parameters/lat"), + Reference("#/components/parameters/lon"), + ], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + properties={ + "placeholder": Schema( + ValueType.STRING, + "Placeholder description", + ) + }, + ) + ) + }, + ), + "404": Response( + "Not found response", + content={ + "text/plain": MediaType( + schema=Schema( + ValueType.STRING, + title="Weather not found", + example="Not found", + ) + ) + }, + ), + }, + ), + ) + }, + components=Components( + parameters={ + "q": Parameter( + "q", + in_=ParameterLocation.QUERY, + description="Example", + schema=Schema(type=ValueType.STRING), + ), + "id": Parameter( + "id", + in_=ParameterLocation.QUERY, + description="Example", + schema=Schema(type=ValueType.STRING), + ), + "lat": Parameter( + "lat", + in_=ParameterLocation.QUERY, + description="Example", + schema=Schema(type=ValueType.STRING), + ), + "lon": Parameter( + "lon", + in_=ParameterLocation.QUERY, + description="Example", + schema=Schema(type=ValueType.STRING), + ), + } + ), + ) + + def yaml(self) -> str: + return """ + openapi: 3.1.0 + info: + title: Weather API + version: 0.0.0-alpha + jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema + paths: + /weather: + summary: Call current weather data for one location + description: Access current weather data for any location on Earth. + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + placeholder: + type: string + format: Placeholder description + title: sample + '404': + description: Not found response + content: + text/plain: + schema: + type: string + example: Not found + title: Weather not found + tags: + - Current Weather Data + operationId: CurrentWeatherData + parameters: + - $ref: '#/components/parameters/q' + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/lat' + - $ref: '#/components/parameters/lon' + components: + parameters: + q: + name: q + in: query + schema: + type: string + description: Example + id: + name: id + in: query + schema: + type: string + description: Example + lat: + name: lat + in: query + schema: + type: string + description: Example + lon: + name: lon + in: query + schema: + type: string + description: Example + """ + + def json(self) -> str: + return """ + { + "openapi": "3.1.0", + "info": { + "title": "Weather API", + "version": "0.0.0-alpha" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/weather": { + "summary": "Call current weather data for one location", + "description": "Access current weather data for any location on Earth.", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "placeholder": { + "type": "string", + "format": "Placeholder description" + } + }, + "title": "sample" + } + } + } + }, + "404": { + "description": "Not found response", + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "Not found", + "title": "Weather not found" + } + } + } + } + }, + "tags": [ + "Current Weather Data" + ], + "operationId": "CurrentWeatherData", + "parameters": [ + { + "$ref": "#/components/parameters/q" + }, + { + "$ref": "#/components/parameters/id" + }, + { + "$ref": "#/components/parameters/lat" + }, + { + "$ref": "#/components/parameters/lon" + } + ] + } + } + }, + "components": { + "parameters": { + "q": { + "name": "q", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Example" + }, + "id": { + "name": "id", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Example" + }, + "lat": { + "name": "lat", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Example" + }, + "lon": { + "name": "lon", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Example" + } + } + } + } + """ + + +class OpenAPIExample4(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Example API", version="0.0.0-alpha"), + paths={ + "/": PathItem( + summary="Test the example snake_case properness", + description="Lorem ipsum dolor sit amet", + get=Operation( + tags=["Example"], + operation_id="example", + parameters=[], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + properties={ + "snake_case": Schema( + ValueType.STRING, + "Placeholder description", + ), + "ner_label": Schema( + ValueType.STRING, + "Placeholder description", + ), + }, + example=ExampleOne( + snake_case="Foo", + ner_label="Lorem Ipsum", + ), + ) + ) + }, + ), + }, + ), + ) + }, + ) + + def yaml(self) -> str: + return """ +openapi: 3.1.0 +info: + title: Example API + version: 0.0.0-alpha +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +paths: + /: + summary: Test the example snake_case properness + description: Lorem ipsum dolor sit amet + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + snake_case: + type: string + format: Placeholder description + ner_label: + type: string + format: Placeholder description + example: + snake_case: Foo + ner_label: Lorem Ipsum + title: sample + tags: + - Example + operationId: example + parameters: [] + """ + + def json(self) -> str: + return """ +{ + "openapi": "3.1.0", + "info": { + "title": "Example API", + "version": "0.0.0-alpha" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "summary": "Test the example snake_case properness", + "description": "Lorem ipsum dolor sit amet", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "snake_case": { + "type": "string", + "format": "Placeholder description" + }, + "ner_label": { + "type": "string", + "format": "Placeholder description" + } + }, + "example": { + "snake_case": "Foo", + "ner_label": "Lorem Ipsum" + }, + "title": "sample" + } + } + } + } + }, + "tags": [ + "Example" + ], + "operationId": "example", + "parameters": [] + } + } + } +} + """ + + +class OpenAPIExample5(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Example API", version="0.0.0-alpha"), + paths={ + "/": PathItem( + summary="Test the example snake_case properness", + description="Lorem ipsum dolor sit amet", + get=Operation( + tags=["Example"], + operation_id="example", + parameters=[], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + properties={ + "snake_case": Schema( + ValueType.STRING, + "Placeholder description", + ), + "ner_label": Schema( + ValueType.STRING, + "Placeholder description", + ), + }, + ), + examples={ + "one": Example( + summary="First example", + value=ExampleOne( + snake_case="ABC", + ner_label="Lorem Ipsum 1", + ), + ), + "two": Example( + summary="Second example", + value=ExampleOne( + snake_case="DEF", + ner_label="Lorem Ipsum 2", + ), + ), + }, + ) + }, + ), + }, + ), + ) + }, + ) + + def yaml(self) -> str: + return """ +openapi: 3.1.0 +info: + title: Example API + version: 0.0.0-alpha +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +paths: + /: + summary: Test the example snake_case properness + description: Lorem ipsum dolor sit amet + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + snake_case: + type: string + format: Placeholder description + ner_label: + type: string + format: Placeholder description + title: sample + examples: + one: + summary: First example + value: + snake_case: ABC + ner_label: Lorem Ipsum 1 + two: + summary: Second example + value: + snake_case: DEF + ner_label: Lorem Ipsum 2 + tags: + - Example + operationId: example + parameters: [] + """ + + def json(self) -> str: + return """ +{ + "openapi": "3.1.0", + "info": { + "title": "Example API", + "version": "0.0.0-alpha" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "summary": "Test the example snake_case properness", + "description": "Lorem ipsum dolor sit amet", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "snake_case": { + "type": "string", + "format": "Placeholder description" + }, + "ner_label": { + "type": "string", + "format": "Placeholder description" + } + }, + "title": "sample" + }, + "examples": { + "one": { + "summary": "First example", + "value": { + "snake_case": "ABC", + "ner_label": "Lorem Ipsum 1" + } + }, + "two": { + "summary": "Second example", + "value": { + "snake_case": "DEF", + "ner_label": "Lorem Ipsum 2" + } + } + } + } + } + } + }, + "tags": [ + "Example" + ], + "operationId": "example", + "parameters": [] + } + } + } +} + """ + + +class OpenAPIExample6(TestItem): + """ + This tests proper handling of Python Enums in dataclasses. + """ + + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Example API", version="0.0.0-alpha"), + paths={ + "/": PathItem( + summary="Test the example snake_case properness", + description="Lorem ipsum dolor sit amet", + get=Operation( + tags=["Example"], + operation_id="example", + parameters=[], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + required=["id", "name", "type"], + properties={ + "id": Schema( + ValueType.INTEGER, + "Placeholder description", + ), + "name": Schema( + ValueType.STRING, + "Placeholder description", + ), + "type": Schema( + ValueType.STRING, + enum=[ + CatType.EUROPEAN.value, + CatType.PERSIAN.value, + ], + ), + }, + ), + examples={ + "fat_cat": Example( + value=Cat( + id=1, + name="Fatty", + type=CatType.EUROPEAN, + ) + ), + "thin_cat": Example( + value=Cat( + id=2, + name="Thinny", + type=CatType.PERSIAN, + ) + ), + }, + ) + }, + ), + }, + ), + ) + }, + ) + + def yaml(self) -> str: + return """ +openapi: 3.1.0 +info: + title: Example API + version: 0.0.0-alpha +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +paths: + /: + summary: Test the example snake_case properness + description: Lorem ipsum dolor sit amet + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + required: + - id + - name + - type + properties: + id: + type: integer + format: Placeholder description + name: + type: string + format: Placeholder description + type: + type: string + enum: + - european + - persian + title: sample + examples: + fat_cat: + value: + id: 1 + name: Fatty + type: european + thin_cat: + value: + id: 2 + name: Thinny + type: persian + tags: + - Example + operationId: example + parameters: [] + """ + + def json(self) -> str: + return """ +{ + "openapi": "3.1.0", + "info": { + "title": "Example API", + "version": "0.0.0-alpha" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "summary": "Test the example snake_case properness", + "description": "Lorem ipsum dolor sit amet", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id", + "name", + "type" + ], + "properties": { + "id": { + "type": "integer", + "format": "Placeholder description" + }, + "name": { + "type": "string", + "format": "Placeholder description" + }, + "type": { + "type": "string", + "enum": [ + "european", + "persian" + ] + } + }, + "title": "sample" + }, + "examples": { + "fat_cat": { + "value": { + "id": 1, + "name": "Fatty", + "type": "european" + } + }, + "thin_cat": { + "value": { + "id": 2, + "name": "Thinny", + "type": "persian" + } + } + } + } + } + } + }, + "tags": [ + "Example" + ], + "operationId": "example", + "parameters": [] + } + } + } +} + """ + + +class OpenAPIExample7(TestItem): + """ + This tests support for UUIDs, times and dates. + """ + + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Example API", version="0.0.0-alpha"), + paths={ + "/": PathItem( + description="Lorem ipsum dolor sit amet", + get=Operation( + tags=["Example"], + operation_id="example", + parameters=[], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + required=["id", "name", "type"], + properties={ + "id": Schema( + ValueType.STRING, + format=ValueFormat.UUID, + title="FooParent ID", + ), + "foo": Reference( + "#/components/schemas/Foo" + ), + }, + ), + examples={ + "one": Example( + value=FooParent( + id=UUID( + "a475c8d9-232f-4b65-ab2a-f3ccfdd3e26a" + ), + foo=Foo(1, "World", 0.6), + ) + ), + "two": Example( + value=FooParent( + id=UUID( + "5a68f798-0492-4a54-8bd8-aa3c0026b341" + ), + foo=Foo(2, None, None), + ) + ), + }, + ) + }, + ), + }, + ), + ), + "/times": PathItem( + description="Lorem ipsum dolor sit amet", + get=Operation( + tags=["Example"], + operation_id="example2", + parameters=[], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + properties={ + "one": Schema( + ValueType.STRING, + ValueFormat.PARTIALTIME, + ), + "two": Schema( + ValueType.STRING, ValueFormat.DATE + ), + "three": Schema( + ValueType.STRING, + ValueFormat.DATETIME, + ), + }, + example=BuiltInsExample( + one=time(10, 30, 15), + two=date(2016, 3, 26), + three=datetime(2016, 3, 26, 3, 0, 0), + four=b"Lorem ipsum dolor", + ), + ), + ) + }, + ), + }, + ), + ), + }, + components=Components( + schemas={ + "Foo": Schema( + ValueType.OBJECT, + required=["id"], + properties={ + "id": Schema(ValueType.INTEGER), + "hello": Schema(ValueType.STRING), + "foo": Schema(ValueType.NUMBER, ValueFormat.FLOAT), + "four": Schema(ValueType.STRING, ValueFormat.BASE64), + }, + ) + } + ), + ) + + def yaml(self) -> str: + return """ +openapi: 3.1.0 +info: + title: Example API + version: 0.0.0-alpha +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +paths: + /: + description: Lorem ipsum dolor sit amet + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + required: + - id + - name + - type + properties: + id: + type: string + format: uuid + title: FooParent ID + foo: + $ref: '#/components/schemas/Foo' + title: sample + examples: + one: + value: + id: a475c8d9-232f-4b65-ab2a-f3ccfdd3e26a + foo: + id: 1 + hello: World + foo: 0.6 + two: + value: + id: 5a68f798-0492-4a54-8bd8-aa3c0026b341 + foo: + id: 2 + hello: null + foo: null + tags: + - Example + operationId: example + parameters: [] + /times: + description: Lorem ipsum dolor sit amet + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + one: + type: string + format: partial-time + two: + type: string + format: date + three: + type: string + format: date-time + example: + one: '10:30:15' + two: '2016-03-26' + three: '2016-03-26T03:00:00' + four: TG9yZW0gaXBzdW0gZG9sb3I= + title: sample + tags: + - Example + operationId: example2 + parameters: [] +components: + schemas: + Foo: + type: object + required: + - id + properties: + id: + type: integer + hello: + type: string + foo: + type: number + format: float + four: + type: string + format: base64 + """ + + def json(self) -> str: + return """ +{ + "openapi": "3.1.0", + "info": { + "title": "Example API", + "version": "0.0.0-alpha" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "description": "Lorem ipsum dolor sit amet", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id", + "name", + "type" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "FooParent ID" + }, + "foo": { + "$ref": "#/components/schemas/Foo" + } + }, + "title": "sample" + }, + "examples": { + "one": { + "value": { + "id": "a475c8d9-232f-4b65-ab2a-f3ccfdd3e26a", + "foo": { + "id": 1, + "hello": "World", + "foo": 0.6 + } + } + }, + "two": { + "value": { + "id": "5a68f798-0492-4a54-8bd8-aa3c0026b341", + "foo": { + "id": 2, + "hello": null, + "foo": null + } + } + } + } + } + } + } + }, + "tags": [ + "Example" + ], + "operationId": "example", + "parameters": [] + } + }, + "/times": { + "description": "Lorem ipsum dolor sit amet", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "one": { + "type": "string", + "format": "partial-time" + }, + "two": { + "type": "string", + "format": "date" + }, + "three": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "one": "10:30:15", + "two": "2016-03-26", + "three": "2016-03-26T03:00:00", + "four": "TG9yZW0gaXBzdW0gZG9sb3I=" + }, + "title": "sample" + } + } + } + } + }, + "tags": [ + "Example" + ], + "operationId": "example2", + "parameters": [] + } + } + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "hello": { + "type": "string" + }, + "foo": { + "type": "number", + "format": "float" + }, + "four": { + "type": "string", + "format": "base64" + } + } + } + } + } +} + """ + + +class OpenAPIExamplesDefinedWithPydantic(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + info=Info("Example API", version="0.0.0-alpha"), + paths={ + "/": PathItem( + summary="Test the example snake_case properness", + description="Lorem ipsum dolor sit amet", + get=Operation( + tags=["Example"], + operation_id="example", + parameters=[], + responses={ + "200": Response( + "Successful response", + content={ + "application/json": MediaType( + schema=Schema( + ValueType.OBJECT, + title="sample", + properties={ + "snake_case": Schema( + ValueType.STRING, + "Placeholder description", + ), + "ner_label": Schema( + ValueType.STRING, + "Placeholder description", + ), + }, + ), + examples={ + "one": Example( + summary="First example", + value=ExampleOneAlt( + snake_case="ABC", + ner_label="Lorem Ipsum 1", + ), + ), + "two": Example( + summary="Second example", + value=ExampleOneAlt( + snake_case="DEF", + ner_label="Lorem Ipsum 2", + ), + ), + }, + ) + }, + ), + }, + ), + ) + }, + ) + + def yaml(self) -> str: + return """ +openapi: 3.1.0 +info: + title: Example API + version: 0.0.0-alpha +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +paths: + /: + summary: Test the example snake_case properness + description: Lorem ipsum dolor sit amet + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + snake_case: + type: string + format: Placeholder description + ner_label: + type: string + format: Placeholder description + title: sample + examples: + one: + summary: First example + value: + snake_case: ABC + ner_label: Lorem Ipsum 1 + two: + summary: Second example + value: + snake_case: DEF + ner_label: Lorem Ipsum 2 + tags: + - Example + operationId: example + parameters: [] + """ + + def json(self) -> str: + return """ +{ + "openapi": "3.1.0", + "info": { + "title": "Example API", + "version": "0.0.0-alpha" + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "paths": { + "/": { + "summary": "Test the example snake_case properness", + "description": "Lorem ipsum dolor sit amet", + "get": { + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "snake_case": { + "type": "string", + "format": "Placeholder description" + }, + "ner_label": { + "type": "string", + "format": "Placeholder description" + } + }, + "title": "sample" + }, + "examples": { + "one": { + "summary": "First example", + "value": { + "snake_case": "ABC", + "ner_label": "Lorem Ipsum 1" + } + }, + "two": { + "summary": "Second example", + "value": { + "snake_case": "DEF", + "ner_label": "Lorem Ipsum 2" + } + } + } + } + } + } + }, + "tags": [ + "Example" + ], + "operationId": "example", + "parameters": [] + } + } + } +} + """ + + +class SchemaExample1(TestItem): + def get_instance(self) -> Any: + return Schema( + type="object", + required=["name"], + properties={ + "name": Schema(type="string"), + "address": Schema(ref="#/components/schemas/Address"), + "age": Schema(type="integer", format="int32", minimum=0), + }, + ) + + def yaml(self) -> str: + return """ + type: object + required: + - name + properties: + name: + type: string + address: + $ref: '#/components/schemas/Address' + age: + type: integer + format: int32 + minimum: 0 + """ + + def json(self) -> str: + return """ + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "address": { + "$ref": "#/components/schemas/Address" + }, + "age": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + } + """ + + +class ServerExample1(TestItem): + def get_instance(self) -> Any: + return OpenAPI( + servers=[ + Server( + url="https://{username}.gigantic-server.com:{port}/{basePath}", + description="The production API server", + variables={ + "username": ServerVariable( + default="demo", + description="this value is assigned by the service " + + " provider, in this example `gigantic-server.com`", + ), + "port": ServerVariable( + enum=["8443", "443"], + default="8443", + ), + "basePath": ServerVariable( + default="v2", + ), + }, + ) + ] + ) + + def yaml(self) -> str: + return """ + openapi: 3.1.0 + jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema + servers: + - url: https://{username}.gigantic-server.com:{port}/{basePath} + description: The production API server + variables: + username: + default: demo + description: this value is assigned by the service provider, in this + example `gigantic-server.com` + port: + default: '8443' + enum: + - '8443' + - '443' + basePath: + default: v2 + """ + + def json(self) -> str: + return """ + { + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "servers": [ + { + "url": "https://{username}.gigantic-server.com:{port}/{basePath}", + "description": "The production API server", + "variables": { + "username": { + "default": "demo", + "description": "this value is assigned by the service provider, in this example `gigantic-server.com`" + }, + "port": { + "default": "8443", + "enum": [ + "8443", + "443" + ] + }, + "basePath": { + "default": "v2" + } + } + } + ] + } + """ + + +class InfoExample1(TestItem): + def get_instance(self) -> Any: + return Info( + title="Sample Pet Store App", + description="This is a sample server for a pet store.", + terms_of_service="http://example.com/terms/", + contact=Contact( + name="API Support", + url="http://www.example.com/support", + email="support@example.com", + ), + license=License( + name="Apache 2.0", + url="https://www.apache.org/licenses/LICENSE-2.0.html", + ), + version="1.0.1", + ) + + def yaml(self) -> str: + return """ + title: Sample Pet Store App + version: 1.0.1 + description: This is a sample server for a pet store. + termsOfService: http://example.com/terms/ + contact: + name: API Support + url: http://www.example.com/support + email: support@example.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + """ + + def json(self) -> str: + return """ + { + "title": "Sample Pet Store App", + "version": "1.0.1", + "description": "This is a sample server for a pet store.", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "API Support", + "url": "http://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + } + """ + + +class InfoExample2(TestItem): + def get_instance(self) -> Any: + return Info( + title="Sample Pet Store App", + description="This is a sample server for a pet store.", + license=License( + name="Apache 2.0", + url="https://www.apache.org/licenses/LICENSE-2.0.html", + ), + version="1.0.1", + ) + + def yaml(self) -> str: + return """ + title: Sample Pet Store App + version: 1.0.1 + description: This is a sample server for a pet store. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + """ + + def json(self) -> str: + return """ + { + "title": "Sample Pet Store App", + "version": "1.0.1", + "description": "This is a sample server for a pet store.", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + } + """ + + +class InfoExample3(TestItem): + def get_instance(self) -> Any: + return Info( + title="ØØ Void", + description="Great album.", + version="1.0.1", + ) + + def yaml(self) -> str: + return """ + title: ØØ Void + version: 1.0.1 + description: Great album. + """ + + def json(self) -> str: + return """ + { + "title": "ØØ Void", + "version": "1.0.1", + "description": "Great album." + } + """ + + +class ResponseExample1(TestItem): + def get_instance(self) -> Any: + return Response( + description="A simple string response", + content={"text/plain": MediaType(schema=Schema(type="string"))}, + ) + + def yaml(self) -> str: + return """ + description: A simple string response + content: + text/plain: + schema: + type: string + """ + + def json(self) -> str: + return """ + { + "description": "A simple string response", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + """ + + +class RequestBodyExample1(TestItem): + def get_instance(self) -> Any: + return RequestBody( + description="Some request body", + content={ + "text/plain": MediaType(schema=Schema(type=ValueType.STRING)), + "application/json": MediaType( + schema=Schema( + type=ValueType.OBJECT, + required=["id", "name", "foo"], + properties={ + "id": Schema(type=ValueType.STRING), + "name": Schema(type=ValueType.STRING), + "foo": Schema(type=ValueType.BOOLEAN), + }, + ) + ), + }, + ) + + def yaml(self) -> str: + return """ + content: + text/plain: + schema: + type: string + application/json: + schema: + type: object + required: + - id + - name + - foo + properties: + id: + type: string + name: + type: string + foo: + type: boolean + description: Some request body + """ + + def json(self) -> str: + return """ + { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "object", + "required": [ + "id", + "name", + "foo" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "foo": { + "type": "boolean" + } + } + } + } + }, + "description": "Some request body" + } + """ + + +class SecuritySchemeExample1(TestItem): + def get_instance(self) -> Any: + return OAuth2Security( + flows=OAuthFlows( + implicit=OAuthFlow( + authorization_url="https://example.com/api/oauth/dialog", + scopes={ + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + ), + authorization_code=OAuthFlow( + authorization_url="https://example.com/api/oauth/dialog", + token_url="https://example.com/api/oauth/token", + scopes={ + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + ), + ), + ) + + def yaml(self) -> str: + return """ + flows: + implicit: + scopes: + write:pets: modify pets in your account + read:pets: read your pets + authorizationUrl: https://example.com/api/oauth/dialog + authorizationCode: + scopes: + write:pets: modify pets in your account + read:pets: read your pets + authorizationUrl: https://example.com/api/oauth/dialog + tokenUrl: https://example.com/api/oauth/token + type: oauth2 + """ + + def json(self) -> str: + return """ + { + "flows": { + "implicit": { + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + }, + "authorizationUrl": "https://example.com/api/oauth/dialog" + }, + "authorizationCode": { + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + }, + "authorizationUrl": "https://example.com/api/oauth/dialog", + "tokenUrl": "https://example.com/api/oauth/token" + } + }, + "type": "oauth2" + } + """ + + +class CallbackExample1(TestItem): + def get_instance(self) -> Any: + return Callback( + expression="{$request.query.queryUrl}", + path=PathItem( + post=Operation( + request_body=RequestBody( + description="Callback payload", + content={ + "application/json": MediaType( + Schema(ref="#/components/schemas/SomePayload") + ) + }, + ), + responses={ + "200": Response(description="callback successfully processed") + }, + ) + ), + ) + + def yaml(self) -> str: + return """ + '{$request.query.queryUrl}': + post: + responses: + '200': + description: callback successfully processed + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SomePayload' + description: Callback payload + """ + + def json(self) -> str: + return """ + { + "{$request.query.queryUrl}": { + "post": { + "responses": { + "200": { + "description": "callback successfully processed" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SomePayload" + } + } + }, + "description": "Callback payload" + } + } + } + } + """ + + +@pytest.fixture(scope="module") +def serializer() -> Serializer: + return Serializer() + + +@pytest.mark.parametrize("example_type", TestItem.__subclasses__()) +def test_yaml_serialization( + example_type: Type[TestItem], serializer: Serializer +) -> None: + example = example_type() + expected_yaml = example.expected_yaml() + instance = example.get_instance() + result = serializer.to_yaml(instance) + try: + assert result.strip() == expected_yaml + except AssertionError as ae: + debug_result("v3", example, result, Format.YAML) + raise ae + + +@pytest.mark.parametrize("example_type", TestItem.__subclasses__()) +def test_json_serialization( + example_type: Type[TestItem], serializer: Serializer +) -> None: + example = example_type() + expected_json = example.expected_json() + instance = example.get_instance() + result = serializer.to_json(instance) + try: + assert result.strip() == expected_json + except AssertionError as ae: + debug_result("v3", example, result, Format.JSON) + raise ae + + +@pytest.mark.parametrize("example_type", TestItem.__subclasses__()) +def test_equality(example_type: Type[TestItem]) -> None: + example = example_type() + one = example.get_instance() + two = example.get_instance() + assert one == two + + +def test_serialize_datetimes_examples(): + """ + Tests serialization using the default formatter for datetime. + """ + writer = JSONContentWriter() + + yaml_text = """ +description: Start time stamp of the returned data interval +example: 2022-08-17T18:00:00Z +in: query +name: fromInstant +required: false +schema: + format: date-time + type: string + """ + data = yaml.safe_load(yaml_text) + json_text = writer.write(data) + + expected_json = """ +{ + "description": "Start time stamp of the returned data interval", + "example": "2022-08-17T18:00:00+00:00", + "in": "query", + "name": "fromInstant", + "required": false, + "schema": { + "format": "date-time", + "type": "string" + } +} + """.strip() + + assert json_text == expected_json + + +def test_serialize_datetimes_examples_exact_format(): + writer = JSONContentWriter() + + yaml_text = """ +description: Start time stamp of the returned data interval +example: '2022-08-17T18:00:00Z' +in: query +name: fromInstant +required: false +schema: + format: date-time + type: string + """ + data = yaml.safe_load(yaml_text) + json_text = writer.write(data) + + expected_json = """ +{ + "description": "Start time stamp of the returned data interval", + "example": "2022-08-17T18:00:00Z", + "in": "query", + "name": "fromInstant", + "required": false, + "schema": { + "format": "date-time", + "type": "string" + } +} + """.strip() + + assert json_text == expected_json + + +def test_serialize_datetimes_examples_exact_format_env(): + os.environ["OPENAPI_DATETIME_FORMAT"] = "%Y-%m-%dT%H:%M:%SZ" + + try: + writer = JSONContentWriter() + + yaml_text = """ + description: Start time stamp of the returned data interval + example: 2022-08-17T18:00:00Z + in: query + name: fromInstant + required: false + schema: + format: date-time + type: string + """ + data = yaml.safe_load(yaml_text) + json_text = writer.write(data) + + expected_json = """ +{ + "description": "Start time stamp of the returned data interval", + "example": "2022-08-17T18:00:00Z", + "in": "query", + "name": "fromInstant", + "required": false, + "schema": { + "format": "date-time", + "type": "string" + } +} + """.strip() + + assert json_text == expected_json + finally: + os.environ["OPENAPI_DATETIME_FORMAT"] = ""