Skip to content

Commit 9deae91

Browse files
authored
feat: implemented additional fields support (#106)
1 parent 6b1a8e1 commit 9deae91

18 files changed

Lines changed: 647 additions & 26 deletions

src/openapi_parser/builders/content.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from typing import Any
55

6+
from openapi_parser.builders.encoding import EncodingBuilder
67
from openapi_parser.builders.schema import SchemaFactory
78
from openapi_parser.enumeration import ContentType
89
from openapi_parser.loose_types import LooseContentType
@@ -17,46 +18,57 @@ class ContentBuilder:
1718
"""Builds content objects for request/response bodies."""
1819

1920
_schema_factory: SchemaFactory
21+
_encoding_builder: EncodingBuilder
2022
_strict_enum: bool
2123

22-
def __init__(self, schema_factory: SchemaFactory, strict_enum: bool = True) -> None:
24+
def __init__(
25+
self,
26+
schema_factory: SchemaFactory,
27+
encoding_builder: EncodingBuilder,
28+
strict_enum: bool = True,
29+
) -> None:
2330
"""Initialize content builder.
2431
2532
Args:
2633
schema_factory: Factory for creating schema objects
34+
encoding_builder: Builder for encoding objects
2735
strict_enum: Whether to validate enums strictly
2836
"""
2937
self._schema_factory = schema_factory
38+
self._encoding_builder = encoding_builder
3039
self._strict_enum = strict_enum
3140

3241
def build_list(self, data: dict[str, Any]) -> list[Content]:
3342
"""Build a list of content objects from a dict of media types."""
3443
return [
3544
self._create_content(
3645
content_type,
37-
content_value.get("schema", {}),
38-
content_value.get("example", None),
39-
content_value.get("examples", {}),
46+
content_value,
4047
)
4148
for content_type, content_value in data.items()
4249
]
4350

4451
def _create_content(
4552
self,
4653
content_type: str,
47-
schema: dict[str, Any],
48-
example: Any,
49-
examples: dict[str, Any],
54+
content_value: dict[str, Any],
5055
) -> Content:
5156
logger.debug(f"Content building [type={content_type}]")
5257

5358
ContentTypeCls: ContentTypeType = (
5459
ContentType if self._strict_enum else LooseContentType
5560
)
5661

62+
encoding = (
63+
self._encoding_builder.build_dict(content_value["encoding"])
64+
if content_value.get("encoding")
65+
else None
66+
)
67+
5768
return Content(
5869
type=ContentTypeCls(content_type),
59-
schema=self._schema_factory.create(schema),
60-
example=example,
61-
examples=examples,
70+
schema=self._schema_factory.create(content_value.get("schema", {})),
71+
example=content_value.get("example"),
72+
examples=content_value.get("examples", {}),
73+
encoding=encoding,
6274
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Encoding builder for request body property encodings."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from openapi_parser.builders.common import (
7+
PropertyMeta,
8+
extract_extension_attributes,
9+
extract_typed_props,
10+
)
11+
from openapi_parser.builders.header import HeaderBuilder
12+
from openapi_parser.specification import Encoding
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class EncodingBuilder:
18+
"""Builds encoding objects from raw specification data."""
19+
20+
_header_builder: HeaderBuilder
21+
22+
def __init__(self, header_builder: HeaderBuilder) -> None:
23+
"""Initialize encoding builder.
24+
25+
Args:
26+
header_builder: Builder for header objects
27+
"""
28+
self._header_builder = header_builder
29+
30+
def build_dict(self, data: dict[str, dict[str, Any]]) -> dict[str, Encoding]:
31+
"""Build a dict of encodings from a dict of raw encoding definitions."""
32+
return {
33+
property_name: self._build(encoding_data)
34+
for property_name, encoding_data in data.items()
35+
}
36+
37+
def _build(self, data: dict[str, Any]) -> Encoding:
38+
logger.debug("Encoding building")
39+
40+
attrs_map = {
41+
"content_type": PropertyMeta(name="contentType", cast=str),
42+
"headers": PropertyMeta(
43+
name="headers",
44+
cast=self._header_builder.build_list,
45+
),
46+
"style": PropertyMeta(name="style", cast=str),
47+
"explode": PropertyMeta(name="explode", cast=bool),
48+
"allow_reserved": PropertyMeta(name="allowReserved", cast=bool),
49+
}
50+
51+
attrs = extract_typed_props(data, attrs_map)
52+
attrs["extensions"] = extract_extension_attributes(data)
53+
54+
if attrs["extensions"]:
55+
logger.debug(f"Extracted custom properties [{attrs['extensions'].keys()}]")
56+
57+
return Encoding(**attrs)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Link builder for response links."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from openapi_parser.builders.common import (
7+
PropertyMeta,
8+
extract_extension_attributes,
9+
extract_typed_props,
10+
)
11+
from openapi_parser.specification import Link, Server
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def build_server(value: dict[str, Any]) -> Server:
17+
"""Build a Server object from raw data."""
18+
return Server(
19+
url=value["url"],
20+
description=value.get("description"),
21+
)
22+
23+
24+
class LinkBuilder:
25+
"""Builds link objects from raw specification data."""
26+
27+
def build_dict(self, data: dict[str, dict[str, Any]]) -> dict[str, Link]:
28+
"""Build a dict of links from a dict of raw link definitions."""
29+
return {
30+
link_name: self._build(link_data) for link_name, link_data in data.items()
31+
}
32+
33+
def _build(self, data: dict[str, Any]) -> Link:
34+
logger.debug("Link building")
35+
36+
attrs_map = {
37+
"operation_ref": PropertyMeta(name="operationRef", cast=str),
38+
"operation_id": PropertyMeta(name="operationId", cast=str),
39+
"parameters": PropertyMeta(name="parameters", cast=dict),
40+
"request_body": PropertyMeta(name="requestBody", cast=None),
41+
"description": PropertyMeta(name="description", cast=str),
42+
"server": PropertyMeta(name="server", cast=build_server),
43+
}
44+
45+
attrs = extract_typed_props(data, attrs_map)
46+
attrs["extensions"] = extract_extension_attributes(data)
47+
48+
if attrs["extensions"]:
49+
logger.debug(f"Extracted custom properties [{attrs['extensions'].keys()}]")
50+
51+
return Link(**attrs)

src/openapi_parser/builders/operation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def build(self, method: OperationMethod, data: dict[str, Any]) -> Operation:
7272
),
7373
"tags": PropertyMeta(name="tags", cast=list),
7474
"security": PropertyMeta(name="security", cast=None),
75+
"callbacks": PropertyMeta(name="callbacks", cast=None),
7576
}
7677

7778
attrs = extract_typed_props(data, attrs_map)

src/openapi_parser/builders/parameter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def build(self, data: dict[str, Any]) -> Parameter:
8484
"examples": PropertyMeta(name="examples", cast=dict),
8585
"deprecated": PropertyMeta(name="deprecated", cast=bool),
8686
"explode": PropertyMeta(name="explode", cast=bool),
87+
"allow_reserved": PropertyMeta(name="allowReserved", cast=bool),
8788
}
8889

8990
attrs = extract_typed_props(data, attrs_map)

src/openapi_parser/builders/response.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from openapi_parser.builders.common import PropertyMeta, extract_typed_props
77
from openapi_parser.builders.content import ContentBuilder
88
from openapi_parser.builders.header import HeaderBuilder
9+
from openapi_parser.builders.link import LinkBuilder
910
from openapi_parser.specification import Response
1011

1112
logger = logging.getLogger(__name__)
@@ -16,20 +17,24 @@ class ResponseBuilder:
1617

1718
_content_builder: ContentBuilder
1819
_header_builder: HeaderBuilder
20+
_link_builder: LinkBuilder
1921

2022
def __init__(
2123
self,
2224
content_builder: ContentBuilder,
2325
header_builder: HeaderBuilder,
26+
link_builder: LinkBuilder,
2427
) -> None:
2528
"""Initialize response builder.
2629
2730
Args:
2831
content_builder: Builder for content objects
2932
header_builder: Builder for header objects
33+
link_builder: Builder for link objects
3034
"""
3135
self._content_builder = content_builder
3236
self._header_builder = header_builder
37+
self._link_builder = link_builder
3338

3439
def build(self, code: int | str, data: dict[str, Any]) -> Response:
3540
"""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:
4550
name="headers",
4651
cast=self._header_builder.build_list,
4752
),
53+
"links": PropertyMeta(
54+
name="links",
55+
cast=self._link_builder.build_dict,
56+
),
4857
}
4958

5059
attrs = extract_typed_props(data, attrs_map)

src/openapi_parser/builders/schema.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,22 @@ def build_properties(object_attrs: dict[str, Any]) -> list[Property]:
232232
for name, schema in object_attrs.items()
233233
]
234234

235+
def build_additional_properties(
236+
value: bool | dict[str, Any],
237+
) -> bool | Schema:
238+
if isinstance(value, bool):
239+
return value
240+
return self.create(value)
241+
235242
attrs_map = {
236243
"max_properties": PropertyMeta(name="maxProperties", cast=int),
237244
"min_properties": PropertyMeta(name="minProperties", cast=int),
238245
"required": PropertyMeta(name="required", cast=list),
239246
"properties": PropertyMeta(name="properties", cast=build_properties),
247+
"additional_properties": PropertyMeta(
248+
name="additionalProperties",
249+
cast=build_additional_properties,
250+
),
240251
}
241252

242253
return Object(**extract_attrs(data, attrs_map))

src/openapi_parser/parser.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
from openapi_parser.builders.common import PropertyMeta, extract_typed_props
77
from openapi_parser.builders.content import ContentBuilder
8+
from openapi_parser.builders.encoding import EncodingBuilder
89
from openapi_parser.builders.external_doc import ExternalDocBuilder
910
from openapi_parser.builders.header import HeaderBuilder
1011
from openapi_parser.builders.info import InfoBuilder
12+
from openapi_parser.builders.link import LinkBuilder
1113
from openapi_parser.builders.oauth_flow import OAuthFlowBuilder
1214
from openapi_parser.builders.operation import OperationBuilder
1315
from openapi_parser.builders.parameter import ParameterBuilder
@@ -143,11 +145,17 @@ def _create_parser(strict_enum: bool = True) -> Parser:
143145
external_doc_builder = ExternalDocBuilder()
144146
tag_builder = TagBuilder(external_doc_builder)
145147
schema_factory = SchemaFactory(strict_enum=strict_enum)
146-
content_builder = ContentBuilder(schema_factory, strict_enum=strict_enum)
147148
header_builder = HeaderBuilder(schema_factory)
149+
encoding_builder = EncodingBuilder(header_builder)
150+
content_builder = ContentBuilder(
151+
schema_factory,
152+
encoding_builder,
153+
strict_enum=strict_enum,
154+
)
148155
parameter_builder = ParameterBuilder(schema_factory, content_builder)
149156
schemas_builder = SchemasBuilder(schema_factory)
150-
response_builder = ResponseBuilder(content_builder, header_builder)
157+
link_builder = LinkBuilder()
158+
response_builder = ResponseBuilder(content_builder, header_builder, link_builder)
151159
request_builder = RequestBuilder(content_builder)
152160
operation_builder = OperationBuilder(
153161
response_builder,

src/openapi_parser/specification.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class Object(Schema):
199199
min_properties: int | None = None
200200
required: list[str] = field(default_factory=list)
201201
properties: list[Property] = field(default_factory=list)
202-
# additional_properties: Optional[Union[bool, Schema]] = field(default=True) # TODO
202+
additional_properties: bool | Schema | None = None
203203

204204

205205
@dataclass
@@ -214,7 +214,7 @@ class Parameter:
214214
description: str | None = None
215215
example: Any | None = None
216216
examples: dict[str, Any] = field(default_factory=dict)
217-
# allow_reserved: bool # TODO
217+
allow_reserved: bool | None = None
218218
deprecated: bool | None = field(default=False)
219219
style: (
220220
str
@@ -228,6 +228,18 @@ class Parameter:
228228
extensions: dict[str, Any] | None = field(default_factory=dict)
229229

230230

231+
@dataclass
232+
class Encoding:
233+
"""Encoding definition for request body properties."""
234+
235+
content_type: str | None = None
236+
headers: list[Header] = field(default_factory=list)
237+
style: str | None = None
238+
explode: bool | None = None
239+
allow_reserved: bool | None = None
240+
extensions: dict[str, Any] | None = field(default_factory=dict)
241+
242+
231243
@dataclass
232244
class Content:
233245
"""Request/response content definition."""
@@ -236,7 +248,7 @@ class Content:
236248
schema: Schema
237249
example: Any | None = None
238250
examples: dict[str, Any] = field(default_factory=dict)
239-
# encoding: dict[str, Encoding] # TODO
251+
encoding: dict[str, Encoding] | None = None
240252

241253

242254
@dataclass
@@ -260,6 +272,19 @@ class Header:
260272
extensions: dict[str, Any] | None = field(default_factory=dict)
261273

262274

275+
@dataclass
276+
class Link:
277+
"""Link definition for response links."""
278+
279+
operation_ref: str | None = None
280+
operation_id: str | None = None
281+
parameters: dict[str, Any] = field(default_factory=dict)
282+
request_body: Any | None = None
283+
description: str | None = None
284+
server: Server | None = None
285+
extensions: dict[str, Any] | None = field(default_factory=dict)
286+
287+
263288
@dataclass
264289
class Response:
265290
"""API response definition."""
@@ -269,6 +294,7 @@ class Response:
269294
code: int | None = None
270295
content: list[Content] | None = None
271296
headers: list[Header] = field(default_factory=list)
297+
links: dict[str, Link] | None = None
272298

273299

274300
@dataclass
@@ -313,7 +339,7 @@ class Operation:
313339
tags: list[str] = field(default_factory=list)
314340
security: list[dict[str, Any]] = field(default_factory=list)
315341
extensions: dict[str, Any] | None = field(default_factory=dict)
316-
# callbacks: dict[str, Callback] = field(default_factory=dict) # TODO
342+
callbacks: dict[str, Any] = field(default_factory=dict)
317343

318344

319345
@dataclass

0 commit comments

Comments
 (0)