From 51a75d7588b9009144407f37419b5b2af9564b72 Mon Sep 17 00:00:00 2001 From: manchenkoff Date: Fri, 22 May 2026 23:40:01 +0100 Subject: [PATCH 1/2] feat: implemented additional fields support --- src/openapi_parser/builders/content.py | 32 ++++--- src/openapi_parser/builders/encoding.py | 57 ++++++++++++ src/openapi_parser/builders/link.py | 51 +++++++++++ src/openapi_parser/builders/operation.py | 1 + src/openapi_parser/builders/parameter.py | 1 + src/openapi_parser/builders/response.py | 9 ++ src/openapi_parser/builders/schema.py | 11 +++ src/openapi_parser/parser.py | 12 ++- src/openapi_parser/specification.py | 34 ++++++- tests/builders/schema/test_object.py | 30 +++++++ tests/builders/test_content_builder.py | 75 ++++++++++++++-- tests/builders/test_encoding_builder.py | 109 +++++++++++++++++++++++ tests/builders/test_link_builder.py | 92 +++++++++++++++++++ tests/builders/test_operation_builder.py | 50 +++++++++++ tests/builders/test_parameter_builder.py | 22 +++++ tests/builders/test_response_builder.py | 18 +++- 16 files changed, 579 insertions(+), 25 deletions(-) create mode 100644 src/openapi_parser/builders/encoding.py create mode 100644 src/openapi_parser/builders/link.py create mode 100644 tests/builders/test_encoding_builder.py create mode 100644 tests/builders/test_link_builder.py diff --git a/src/openapi_parser/builders/content.py b/src/openapi_parser/builders/content.py index 3f6632e..4c0d92d 100644 --- a/src/openapi_parser/builders/content.py +++ b/src/openapi_parser/builders/content.py @@ -3,6 +3,7 @@ import logging from typing import Any +from openapi_parser.builders.encoding import EncodingBuilder from openapi_parser.builders.schema import SchemaFactory from openapi_parser.enumeration import ContentType from openapi_parser.loose_types import LooseContentType @@ -17,16 +18,24 @@ class ContentBuilder: """Builds content objects for request/response bodies.""" _schema_factory: SchemaFactory + _encoding_builder: EncodingBuilder _strict_enum: bool - def __init__(self, schema_factory: SchemaFactory, strict_enum: bool = True) -> None: + def __init__( + self, + schema_factory: SchemaFactory, + encoding_builder: EncodingBuilder, + strict_enum: bool = True, + ) -> None: """Initialize content builder. Args: schema_factory: Factory for creating schema objects + encoding_builder: Builder for encoding objects strict_enum: Whether to validate enums strictly """ self._schema_factory = schema_factory + self._encoding_builder = encoding_builder self._strict_enum = strict_enum def build_list(self, data: dict[str, Any]) -> list[Content]: @@ -34,9 +43,7 @@ def build_list(self, data: dict[str, Any]) -> list[Content]: return [ self._create_content( content_type, - content_value.get("schema", {}), - content_value.get("example", None), - content_value.get("examples", {}), + content_value, ) for content_type, content_value in data.items() ] @@ -44,9 +51,7 @@ def build_list(self, data: dict[str, Any]) -> list[Content]: def _create_content( self, content_type: str, - schema: dict[str, Any], - example: Any, - examples: dict[str, Any], + content_value: dict[str, Any], ) -> Content: logger.debug(f"Content building [type={content_type}]") @@ -54,9 +59,16 @@ def _create_content( ContentType if self._strict_enum else LooseContentType ) + encoding = ( + self._encoding_builder.build_dict(content_value["encoding"]) + if content_value.get("encoding") + else None + ) + return Content( type=ContentTypeCls(content_type), - schema=self._schema_factory.create(schema), - example=example, - examples=examples, + schema=self._schema_factory.create(content_value.get("schema", {})), + example=content_value.get("example"), + examples=content_value.get("examples", {}), + encoding=encoding, ) diff --git a/src/openapi_parser/builders/encoding.py b/src/openapi_parser/builders/encoding.py new file mode 100644 index 0000000..9a7a36b --- /dev/null +++ b/src/openapi_parser/builders/encoding.py @@ -0,0 +1,57 @@ +"""Encoding builder for request body property encodings.""" + +import logging +from typing import Any + +from openapi_parser.builders.common import ( + PropertyMeta, + extract_extension_attributes, + extract_typed_props, +) +from openapi_parser.builders.header import HeaderBuilder +from openapi_parser.specification import Encoding + +logger = logging.getLogger(__name__) + + +class EncodingBuilder: + """Builds encoding objects from raw specification data.""" + + _header_builder: HeaderBuilder + + def __init__(self, header_builder: HeaderBuilder) -> None: + """Initialize encoding builder. + + Args: + header_builder: Builder for header objects + """ + self._header_builder = header_builder + + def build_dict(self, data: dict[str, dict[str, Any]]) -> dict[str, Encoding]: + """Build a dict of encodings from a dict of raw encoding definitions.""" + return { + property_name: self._build(encoding_data) + for property_name, encoding_data in data.items() + } + + def _build(self, data: dict[str, Any]) -> Encoding: + logger.debug("Encoding building") + + attrs_map = { + "content_type": PropertyMeta(name="contentType", cast=str), + "headers": PropertyMeta( + name="headers", + cast=self._header_builder.build_list, + ), + "style": PropertyMeta(name="style", cast=str), + "explode": PropertyMeta(name="explode", cast=bool), + "allow_reserved": PropertyMeta(name="allowReserved", cast=bool), + } + + attrs = extract_typed_props(data, attrs_map) + attrs["extensions"] = extract_extension_attributes(data) + + if attrs["extensions"]: + logger.debug(f"Extracted custom properties [{attrs['extensions'].keys()}]") + + return Encoding(**attrs) diff --git a/src/openapi_parser/builders/link.py b/src/openapi_parser/builders/link.py new file mode 100644 index 0000000..bd54c0a --- /dev/null +++ b/src/openapi_parser/builders/link.py @@ -0,0 +1,51 @@ +"""Link builder for response links.""" + +import logging +from typing import Any + +from openapi_parser.builders.common import ( + PropertyMeta, + extract_extension_attributes, + extract_typed_props, +) +from openapi_parser.specification import Link, Server + +logger = logging.getLogger(__name__) + + +def build_server(value: dict[str, Any]) -> Server: + """Build a Server object from raw data.""" + return Server( + url=value["url"], + description=value.get("description"), + ) + + +class LinkBuilder: + """Builds link objects from raw specification data.""" + + def build_dict(self, data: dict[str, dict[str, Any]]) -> dict[str, Link]: + """Build a dict of links from a dict of raw link definitions.""" + return { + link_name: self._build(link_data) for link_name, link_data in data.items() + } + + def _build(self, data: dict[str, Any]) -> Link: + logger.debug("Link building") + + attrs_map = { + "operation_ref": PropertyMeta(name="operationRef", cast=str), + "operation_id": PropertyMeta(name="operationId", cast=str), + "parameters": PropertyMeta(name="parameters", cast=dict), + "request_body": PropertyMeta(name="requestBody", cast=None), + "description": PropertyMeta(name="description", cast=str), + "server": PropertyMeta(name="server", cast=build_server), + } + + attrs = extract_typed_props(data, attrs_map) + attrs["extensions"] = extract_extension_attributes(data) + + if attrs["extensions"]: + logger.debug(f"Extracted custom properties [{attrs['extensions'].keys()}]") + + return Link(**attrs) diff --git a/src/openapi_parser/builders/operation.py b/src/openapi_parser/builders/operation.py index 5380fc1..737a43e 100644 --- a/src/openapi_parser/builders/operation.py +++ b/src/openapi_parser/builders/operation.py @@ -72,6 +72,7 @@ def build(self, method: OperationMethod, data: dict[str, Any]) -> Operation: ), "tags": PropertyMeta(name="tags", cast=list), "security": PropertyMeta(name="security", cast=None), + "callbacks": PropertyMeta(name="callbacks", cast=None), } attrs = extract_typed_props(data, attrs_map) diff --git a/src/openapi_parser/builders/parameter.py b/src/openapi_parser/builders/parameter.py index 3ddfbc4..c60c69f 100644 --- a/src/openapi_parser/builders/parameter.py +++ b/src/openapi_parser/builders/parameter.py @@ -84,6 +84,7 @@ def build(self, data: dict[str, Any]) -> Parameter: "examples": PropertyMeta(name="examples", cast=dict), "deprecated": PropertyMeta(name="deprecated", cast=bool), "explode": PropertyMeta(name="explode", cast=bool), + "allow_reserved": PropertyMeta(name="allowReserved", cast=bool), } attrs = extract_typed_props(data, attrs_map) diff --git a/src/openapi_parser/builders/response.py b/src/openapi_parser/builders/response.py index 2a86e53..12dd6fc 100644 --- a/src/openapi_parser/builders/response.py +++ b/src/openapi_parser/builders/response.py @@ -6,6 +6,7 @@ from openapi_parser.builders.common import PropertyMeta, extract_typed_props from openapi_parser.builders.content import ContentBuilder from openapi_parser.builders.header import HeaderBuilder +from openapi_parser.builders.link import LinkBuilder from openapi_parser.specification import Response logger = logging.getLogger(__name__) @@ -16,20 +17,24 @@ class ResponseBuilder: _content_builder: ContentBuilder _header_builder: HeaderBuilder + _link_builder: LinkBuilder def __init__( self, content_builder: ContentBuilder, header_builder: HeaderBuilder, + link_builder: LinkBuilder, ) -> None: """Initialize response builder. Args: content_builder: Builder for content objects header_builder: Builder for header objects + link_builder: Builder for link objects """ self._content_builder = content_builder self._header_builder = header_builder + self._link_builder = link_builder def build(self, code: int | str, data: dict[str, Any]) -> Response: """Build a Response from a status code and raw data dict.""" @@ -45,6 +50,10 @@ def build(self, code: int | str, data: dict[str, Any]) -> Response: name="headers", cast=self._header_builder.build_list, ), + "links": PropertyMeta( + name="links", + cast=self._link_builder.build_dict, + ), } attrs = extract_typed_props(data, attrs_map) diff --git a/src/openapi_parser/builders/schema.py b/src/openapi_parser/builders/schema.py index d137432..efc5dbf 100644 --- a/src/openapi_parser/builders/schema.py +++ b/src/openapi_parser/builders/schema.py @@ -232,11 +232,22 @@ def build_properties(object_attrs: dict[str, Any]) -> list[Property]: for name, schema in object_attrs.items() ] + def build_additional_properties( + value: bool | dict[str, Any], + ) -> bool | Schema: + if isinstance(value, bool): + return value + return self.create(value) + attrs_map = { "max_properties": PropertyMeta(name="maxProperties", cast=int), "min_properties": PropertyMeta(name="minProperties", cast=int), "required": PropertyMeta(name="required", cast=list), "properties": PropertyMeta(name="properties", cast=build_properties), + "additional_properties": PropertyMeta( + name="additionalProperties", + cast=build_additional_properties, + ), } return Object(**extract_attrs(data, attrs_map)) diff --git a/src/openapi_parser/parser.py b/src/openapi_parser/parser.py index 75966c4..d28f33d 100644 --- a/src/openapi_parser/parser.py +++ b/src/openapi_parser/parser.py @@ -5,9 +5,11 @@ from openapi_parser.builders.common import PropertyMeta, extract_typed_props from openapi_parser.builders.content import ContentBuilder +from openapi_parser.builders.encoding import EncodingBuilder from openapi_parser.builders.external_doc import ExternalDocBuilder from openapi_parser.builders.header import HeaderBuilder from openapi_parser.builders.info import InfoBuilder +from openapi_parser.builders.link import LinkBuilder from openapi_parser.builders.oauth_flow import OAuthFlowBuilder from openapi_parser.builders.operation import OperationBuilder from openapi_parser.builders.parameter import ParameterBuilder @@ -143,11 +145,17 @@ def _create_parser(strict_enum: bool = True) -> Parser: external_doc_builder = ExternalDocBuilder() tag_builder = TagBuilder(external_doc_builder) schema_factory = SchemaFactory(strict_enum=strict_enum) - content_builder = ContentBuilder(schema_factory, strict_enum=strict_enum) header_builder = HeaderBuilder(schema_factory) + encoding_builder = EncodingBuilder(header_builder) + content_builder = ContentBuilder( + schema_factory, + encoding_builder, + strict_enum=strict_enum, + ) parameter_builder = ParameterBuilder(schema_factory, content_builder) schemas_builder = SchemasBuilder(schema_factory) - response_builder = ResponseBuilder(content_builder, header_builder) + link_builder = LinkBuilder() + response_builder = ResponseBuilder(content_builder, header_builder, link_builder) request_builder = RequestBuilder(content_builder) operation_builder = OperationBuilder( response_builder, diff --git a/src/openapi_parser/specification.py b/src/openapi_parser/specification.py index 1271381..f277dc1 100644 --- a/src/openapi_parser/specification.py +++ b/src/openapi_parser/specification.py @@ -199,7 +199,7 @@ class Object(Schema): min_properties: int | None = None required: list[str] = field(default_factory=list) properties: list[Property] = field(default_factory=list) - # additional_properties: Optional[Union[bool, Schema]] = field(default=True) # TODO + additional_properties: bool | Schema | None = None @dataclass @@ -214,7 +214,7 @@ class Parameter: description: str | None = None example: Any | None = None examples: dict[str, Any] = field(default_factory=dict) - # allow_reserved: bool # TODO + allow_reserved: bool | None = None deprecated: bool | None = field(default=False) style: ( str @@ -228,6 +228,18 @@ class Parameter: extensions: dict[str, Any] | None = field(default_factory=dict) +@dataclass +class Encoding: + """Encoding definition for request body properties.""" + + content_type: str | None = None + headers: list[Header] = field(default_factory=list) + style: str | None = None + explode: bool | None = None + allow_reserved: bool | None = None + extensions: dict[str, Any] | None = field(default_factory=dict) + + @dataclass class Content: """Request/response content definition.""" @@ -236,7 +248,7 @@ class Content: schema: Schema example: Any | None = None examples: dict[str, Any] = field(default_factory=dict) - # encoding: dict[str, Encoding] # TODO + encoding: dict[str, Encoding] | None = None @dataclass @@ -260,6 +272,19 @@ class Header: extensions: dict[str, Any] | None = field(default_factory=dict) +@dataclass +class Link: + """Link definition for response links.""" + + operation_ref: str | None = None + operation_id: str | None = None + parameters: dict[str, Any] = field(default_factory=dict) + request_body: Any | None = None + description: str | None = None + server: Server | None = None + extensions: dict[str, Any] | None = field(default_factory=dict) + + @dataclass class Response: """API response definition.""" @@ -269,6 +294,7 @@ class Response: code: int | None = None content: list[Content] | None = None headers: list[Header] = field(default_factory=list) + links: dict[str, Link] | None = None @dataclass @@ -313,7 +339,7 @@ class Operation: tags: list[str] = field(default_factory=list) security: list[dict[str, Any]] = field(default_factory=list) extensions: dict[str, Any] | None = field(default_factory=dict) - # callbacks: dict[str, Callback] = field(default_factory=dict) # TODO + callbacks: dict[str, Any] = field(default_factory=dict) @dataclass diff --git a/tests/builders/schema/test_object.py b/tests/builders/schema/test_object.py index a687035..9906f68 100644 --- a/tests/builders/schema/test_object.py +++ b/tests/builders/schema/test_object.py @@ -31,6 +31,36 @@ properties=[Property("name", string_schema)], ), ), + ( + { + "type": "object", + "additionalProperties": True, + }, + Object( + type=DataType.OBJECT, + additional_properties=True, + ), + ), + ( + { + "type": "object", + "additionalProperties": False, + }, + Object( + type=DataType.OBJECT, + additional_properties=False, + ), + ), + ( + { + "type": "object", + "additionalProperties": {"type": "string"}, + }, + Object( + type=DataType.OBJECT, + additional_properties=string_schema, + ), + ), ) diff --git a/tests/builders/test_content_builder.py b/tests/builders/test_content_builder.py index 28c3e3b..9b82a8e 100644 --- a/tests/builders/test_content_builder.py +++ b/tests/builders/test_content_builder.py @@ -4,9 +4,10 @@ import pytest from openapi_parser.builders.content import ContentBuilder +from openapi_parser.builders.encoding import EncodingBuilder from openapi_parser.builders.schema import SchemaFactory from openapi_parser.enumeration import ContentType, DataType -from openapi_parser.specification import Content, Schema, String +from openapi_parser.specification import Content, Encoding, Schema, String def _get_schema_factory_mock(expected_value: Schema) -> SchemaFactory: @@ -16,6 +17,13 @@ def _get_schema_factory_mock(expected_value: Schema) -> SchemaFactory: return mock_object +def _get_encoding_builder_mock() -> EncodingBuilder: + mock_object = MagicMock() + mock_object.build_dict.return_value = None + + return mock_object + + string_schema = String(type=DataType.STRING) collection_data_provider = ( @@ -41,20 +49,23 @@ def test_build( expected: list[Content], schema_factory: SchemaFactory, ) -> None: - builder = ContentBuilder(schema_factory) + builder = ContentBuilder(schema_factory, _get_encoding_builder_mock()) assert expected == builder.build_list(data) def test_build_empty_dict() -> None: - builder = ContentBuilder(_get_schema_factory_mock(string_schema)) + builder = ContentBuilder( + _get_schema_factory_mock(string_schema), + _get_encoding_builder_mock(), + ) assert builder.build_list({}) == [] def test_build_with_example() -> None: schema_factory = _get_schema_factory_mock(string_schema) - builder = ContentBuilder(schema_factory) + builder = ContentBuilder(schema_factory, _get_encoding_builder_mock()) result = builder.build_list( { @@ -71,7 +82,7 @@ def test_build_with_example() -> None: def test_build_with_examples() -> None: schema_factory = _get_schema_factory_mock(string_schema) - builder = ContentBuilder(schema_factory) + builder = ContentBuilder(schema_factory, _get_encoding_builder_mock()) examples = {"test": {"value": "hello"}} result = builder.build_list( @@ -89,7 +100,7 @@ def test_build_with_examples() -> None: def test_build_missing_schema() -> None: schema_factory_mock = MagicMock() - builder = ContentBuilder(schema_factory_mock) + builder = ContentBuilder(schema_factory_mock, _get_encoding_builder_mock()) builder.build_list({"application/json": {}}) @@ -99,7 +110,7 @@ def test_build_missing_schema() -> None: def test_build_multiple_content_types() -> None: schema_factory = MagicMock() schema_factory.create.side_effect = [string_schema, string_schema] - builder = ContentBuilder(schema_factory) + builder = ContentBuilder(schema_factory, _get_encoding_builder_mock()) result = builder.build_list( { @@ -115,7 +126,11 @@ def test_build_multiple_content_types() -> None: def test_build_non_strict_enum() -> None: schema_factory = _get_schema_factory_mock(string_schema) - builder = ContentBuilder(schema_factory, strict_enum=False) + builder = ContentBuilder( + schema_factory, + _get_encoding_builder_mock(), + strict_enum=False, + ) result = builder.build_list( { @@ -125,3 +140,47 @@ def test_build_non_strict_enum() -> None: assert len(result) == 1 assert result[0].type.value == "application/vnd.api+json" + + +def test_build_with_encoding() -> None: + schema_factory = _get_schema_factory_mock(string_schema) + encoding_builder = MagicMock() + encoding_builder.build_dict.return_value = { + "name": Encoding(content_type="text/plain"), + } + builder = ContentBuilder(schema_factory, encoding_builder) + + result = builder.build_list( + { + "application/json": { + "schema": {"type": "string"}, + "encoding": { + "name": { + "contentType": "text/plain", + } + }, + } + } + ) + + assert len(result) == 1 + assert result[0].encoding == { + "name": Encoding(content_type="text/plain"), + } + encoding_builder.build_dict.assert_called_once_with( + {"name": {"contentType": "text/plain"}} + ) + + +def test_build_without_encoding() -> None: + schema_factory = _get_schema_factory_mock(string_schema) + builder = ContentBuilder(schema_factory, _get_encoding_builder_mock()) + + result = builder.build_list( + { + "application/json": {"schema": {"type": "string"}}, + } + ) + + assert len(result) == 1 + assert result[0].encoding is None diff --git a/tests/builders/test_encoding_builder.py b/tests/builders/test_encoding_builder.py new file mode 100644 index 0000000..642cc46 --- /dev/null +++ b/tests/builders/test_encoding_builder.py @@ -0,0 +1,109 @@ +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from openapi_parser.builders.encoding import EncodingBuilder +from openapi_parser.builders.header import HeaderBuilder +from openapi_parser.enumeration import DataType +from openapi_parser.specification import Encoding, Header, Integer + + +def _get_header_builder_mock(expected_value: list[Header]) -> HeaderBuilder: + mock_object = MagicMock() + mock_object.build_list.return_value = expected_value + + return mock_object + + +data_provider = ( + ( + { + "contentType": "text/plain", + }, + Encoding(content_type="text/plain"), + ), + ( + { + "contentType": "application/json", + "style": "form", + "explode": True, + "allowReserved": False, + }, + Encoding( + content_type="application/json", + style="form", + explode=True, + allow_reserved=False, + ), + ), +) + + +@pytest.mark.parametrize(["data", "expected"], data_provider) +def test_build(data: dict[str, Any], expected: Encoding) -> None: + builder = EncodingBuilder(_get_header_builder_mock([])) + + result = builder._build(data) + + assert expected == result + + +def test_build_with_headers() -> None: + headers = [ + Header( + name="X-Rate-Limit-Limit", + description="The number of allowed requests in the current period", + schema=Integer(type=DataType.INTEGER), + ) + ] + builder = EncodingBuilder(_get_header_builder_mock(headers)) + + result = builder._build( + { + "contentType": "application/json", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the current period", + "schema": {"type": "integer"}, + } + }, + } + ) + + assert result.content_type == "application/json" + assert result.headers == headers + + +def test_build_with_extensions() -> None: + builder = EncodingBuilder(_get_header_builder_mock([])) + + result = builder._build( + { + "contentType": "text/plain", + "x-custom-encoding": "value", + } + ) + + assert result.content_type == "text/plain" + assert result.extensions == {"custom_encoding": "value"} + + +def test_build_dict() -> None: + builder = EncodingBuilder(_get_header_builder_mock([])) + + result = builder.build_dict( + { + "name": { + "contentType": "text/plain", + }, + "email": { + "contentType": "application/json", + }, + } + ) + + assert result == { + "name": Encoding(content_type="text/plain"), + "email": Encoding(content_type="application/json"), + } diff --git a/tests/builders/test_link_builder.py b/tests/builders/test_link_builder.py new file mode 100644 index 0000000..09da922 --- /dev/null +++ b/tests/builders/test_link_builder.py @@ -0,0 +1,92 @@ +from typing import Any + +import pytest + +from openapi_parser.builders.link import LinkBuilder +from openapi_parser.specification import Link, Server + +data_provider = ( + ( + { + "operationId": "getUser", + "parameters": { + "userId": "{$request.body#/id}", + }, + }, + Link( + operation_id="getUser", + parameters={"userId": "{$request.body#/id}"}, + ), + ), + ( + { + "operationRef": "#/paths/~1users~1{userId}/get", + "description": "Get user details", + }, + Link( + operation_ref="#/paths/~1users~1{userId}/get", + description="Get user details", + ), + ), + ( + { + "operationId": "getUser", + "requestBody": "{$request.body#/id}", + "server": { + "url": "https://example.com/api", + }, + }, + Link( + operation_id="getUser", + request_body="{$request.body#/id}", + server=Server(url="https://example.com/api"), + ), + ), +) + + +@pytest.mark.parametrize(["data", "expected"], data_provider) +def test_build(data: dict[str, Any], expected: Link) -> None: + builder = LinkBuilder() + + result = builder._build(data) + + assert expected == result + + +def test_build_with_extensions() -> None: + builder = LinkBuilder() + + result = builder._build( + { + "operationId": "getUser", + "x-custom-link": "value", + } + ) + + assert result.operation_id == "getUser" + assert result.extensions == {"custom_link": "value"} + + +def test_build_dict() -> None: + builder = LinkBuilder() + + result = builder.build_dict( + { + "getUserById": { + "operationId": "getUser", + "parameters": {"userId": "{$request.body#/id}"}, + }, + "getUserByEmail": { + "operationId": "getUserByEmail", + }, + } + ) + + assert result == { + "getUserById": Link( + operation_id="getUser", + parameters={"userId": "{$request.body#/id}"}, + ), + "getUserByEmail": Link(operation_id="getUserByEmail"), + } diff --git a/tests/builders/test_operation_builder.py b/tests/builders/test_operation_builder.py index cb5dfd7..4e3a35c 100644 --- a/tests/builders/test_operation_builder.py +++ b/tests/builders/test_operation_builder.py @@ -209,6 +209,56 @@ def _get_list_builder_mock(expected: Any) -> MagicMock: _get_builder_mock(request_body), _get_list_builder_mock(parameter_list), ), + ( + { + "responses": { + "200": { + "description": "Pet updated.", + "content": { + "application/json": { + "schema": { + "type": "object", + } + }, + }, + }, + }, + "callbacks": { + "myCallback": { + "{$request.body#/callbackUrl}": { + "get": { + "responses": { + "200": { + "description": "Callback response", + } + } + } + } + } + }, + }, + Operation( + responses=[response_schema], + method=OperationMethod.GET, + callbacks={ + "myCallback": { + "{$request.body#/callbackUrl}": { + "get": { + "responses": { + "200": { + "description": "Callback response", + } + } + } + } + } + }, + ), + _get_builder_mock(response_schema), + _get_builder_mock(None), + _get_builder_mock(None), + _get_list_builder_mock(None), + ), ) diff --git a/tests/builders/test_parameter_builder.py b/tests/builders/test_parameter_builder.py index c784f5c..4fc0abd 100644 --- a/tests/builders/test_parameter_builder.py +++ b/tests/builders/test_parameter_builder.py @@ -151,6 +151,28 @@ def _get_content_builder_mock( _get_schema_factory_mock(string_schema), _get_content_builder_mock(None), ), + ( + { + "name": "q", + "in": "query", + "required": True, + "allowReserved": True, + "schema": { + "type": "string", + }, + }, + Parameter( + name="q", + location=ParameterLocation.QUERY, + required=True, + allow_reserved=True, + schema=string_schema, + style=QueryParameterStyle.FORM, + explode=True, + ), + _get_schema_factory_mock(string_schema), + _get_content_builder_mock(None), + ), ( { "name": "filter", diff --git a/tests/builders/test_response_builder.py b/tests/builders/test_response_builder.py index 210263f..05b932a 100644 --- a/tests/builders/test_response_builder.py +++ b/tests/builders/test_response_builder.py @@ -5,6 +5,7 @@ from openapi_parser.builders.content import ContentBuilder from openapi_parser.builders.header import HeaderBuilder +from openapi_parser.builders.link import LinkBuilder from openapi_parser.builders.response import ResponseBuilder from openapi_parser.enumeration import ContentType, DataType from openapi_parser.specification import ( @@ -32,6 +33,13 @@ def _get_header_builder_mock(expected_value: Any) -> HeaderBuilder: return mock_object +def _get_link_builder_mock() -> LinkBuilder: + mock_object = MagicMock() + mock_object.build_dict.return_value = None + + return mock_object + + content_schema = [ Content( type=ContentType.JSON, @@ -92,7 +100,11 @@ def test_build( content_builder: ContentBuilder, header_builder: HeaderBuilder, ) -> None: - builder = ResponseBuilder(content_builder, header_builder) + builder = ResponseBuilder( + content_builder, + header_builder, + _get_link_builder_mock(), + ) assert expected.code is not None assert expected == builder.build(expected.code, data) @@ -102,6 +114,7 @@ def test_build_default_response() -> None: builder = ResponseBuilder( _get_content_builder_mock(None), _get_header_builder_mock(None), + _get_link_builder_mock(), ) response_data = {"description": "A string response"} @@ -115,6 +128,7 @@ def test_build_no_content_or_headers() -> None: builder = ResponseBuilder( _get_content_builder_mock(None), _get_header_builder_mock(None), + _get_link_builder_mock(), ) actual = builder.build(200, {"description": "No content response"}) @@ -130,6 +144,7 @@ def test_build_with_code_as_string() -> None: builder = ResponseBuilder( _get_content_builder_mock([]), _get_header_builder_mock([]), + _get_link_builder_mock(), ) actual = builder.build("404", {"description": "Not found"}) @@ -143,6 +158,7 @@ def test_build_with_various_codes(code: int) -> None: builder = ResponseBuilder( _get_content_builder_mock(None), _get_header_builder_mock(None), + _get_link_builder_mock(), ) actual = builder.build(code, {"description": f"Response {code}"}) From 0c4fcf4fc7c14fc26061b2b0b3f32dfe6e3322ca Mon Sep 17 00:00:00 2001 From: manchenkoff Date: Fri, 22 May 2026 23:43:27 +0100 Subject: [PATCH 2/2] chore: reflected additional fields in yaml spec example --- tests/data/swagger.yml | 22 +++++++++++++++++++ tests/openapi_fixture.py | 47 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/tests/data/swagger.yml b/tests/data/swagger.yml index f35f2ef..c72775f 100644 --- a/tests/data/swagger.yml +++ b/tests/data/swagger.yml @@ -56,6 +56,14 @@ paths: - Users requestBody: $ref: '#/components/requestBodies/AddUserRequest' + callbacks: + onAdd: + '{$request.body#/email}': + post: + summary: 'Callback after user creation' + responses: + '200': + description: 'Callback processed successfully' responses: 201: $ref: '#/components/responses/AddUserResponse' @@ -109,6 +117,7 @@ components: allowEmptyValue: false example: 10 required: true + allowReserved: true schema: type: integer @@ -155,6 +164,12 @@ components: application/json: schema: $ref: '#/components/schemas/User' + encoding: + login: + contentType: text/plain + style: form + email: + contentType: text/plain responses: BadRequest: @@ -215,6 +230,12 @@ components: properties: user: $ref: '#/components/schemas/User' + links: + UpdateUser: + operationId: UpdateUser + parameters: + uuid: '$response.body#/user/uuid' + description: Updates the user schemas: BadRequestError: @@ -249,6 +270,7 @@ components: UUIDObject: type: object + additionalProperties: false required: - uuid properties: diff --git a/tests/openapi_fixture.py b/tests/openapi_fixture.py index a2599e3..961f1ad 100644 --- a/tests/openapi_fixture.py +++ b/tests/openapi_fixture.py @@ -15,9 +15,11 @@ Array, Contact, Content, + Encoding, Info, Integer, License, + Link, Object, Operation, Parameter, @@ -35,6 +37,7 @@ schema_user = Object( type=DataType.OBJECT, + additional_properties=False, required=["uuid", "login", "email", "avatar"], properties=[ Property( @@ -242,6 +245,7 @@ def create_specification() -> Specification: ), "UUIDObject": Object( type=DataType.OBJECT, + additional_properties=False, required=["uuid"], properties=[ Property( @@ -257,6 +261,7 @@ def create_specification() -> Specification: ), "User": Object( type=DataType.OBJECT, + additional_properties=False, required=["uuid", "login", "email", "avatar"], properties=[ Property( @@ -334,6 +339,7 @@ def create_specification() -> Specification: required=True, explode=True, style=QueryParameterStyle.FORM, + allow_reserved=True, example=10, schema=Integer(type=DataType.INTEGER), ), @@ -389,8 +395,38 @@ def create_specification() -> Specification: security=[{"Basic": []}], request_body=RequestBody( description="New user model request", - content=[Content(type=ContentType.JSON, schema=schema_user)], + content=[ + Content( + type=ContentType.JSON, + schema=schema_user, + encoding={ + "login": Encoding( + content_type="text/plain", + style="form", + ), + "email": Encoding( + content_type="text/plain", + ), + }, + ) + ], ), + callbacks={ + "onAdd": { + "{$request.body#/email}": { + "post": { + "summary": "Callback after user creation", + "responses": { + "200": { + "description": ( + "Callback processed successfully" + ), + }, + }, + }, + }, + }, + }, responses=[ Response( code=201, @@ -449,6 +485,15 @@ def create_specification() -> Specification: ), ), ], + links={ + "UpdateUser": Link( + operation_id="UpdateUser", + parameters={ + "uuid": ("$response.body#/user/uuid"), + }, + description="Updates the user", + ), + }, ), bad_request_response, internal_error_response,