Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/openapi_parser/builders/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,46 +18,57 @@ 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]:
"""Build a list of content objects from a dict of media types."""
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()
]

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}]")

ContentTypeCls: ContentTypeType = (
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,
)
57 changes: 57 additions & 0 deletions src/openapi_parser/builders/encoding.py
Original file line number Diff line number Diff line change
@@ -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)
51 changes: 51 additions & 0 deletions src/openapi_parser/builders/link.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions src/openapi_parser/builders/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/openapi_parser/builders/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions src/openapi_parser/builders/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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."""
Expand All @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions src/openapi_parser/builders/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
12 changes: 10 additions & 2 deletions src/openapi_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 30 additions & 4 deletions src/openapi_parser/specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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."""
Expand All @@ -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
Expand All @@ -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."""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading