Skip to content

Commit

Permalink
Fixes manchenkoff#40 -- allow non_strict enum usage during parsing
Browse files Browse the repository at this point in the history
Certain schema properties (content types, type formats...) are not
limited to a known set of values in the OpenAPI 3.x specification.

Users can opt-in for non-strict enum evaluation, making it possible to
parse specifications with custom content types and/or formats used in
schema definitions.
  • Loading branch information
sergei-maertens committed Sep 4, 2022
1 parent 7ded792 commit 994f9e6
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 23 deletions.
12 changes: 9 additions & 3 deletions src/openapi_parser/builders/content.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import logging
from typing import Type, Union

from . import SchemaFactory
from ..enumeration import ContentType
from ..specification import Content
from ..loose_types import LooseContentType

logger = logging.getLogger(__name__)

ContentTypeType = Union[Type[ContentType], Type[LooseContentType]]


class ContentBuilder:
schema_factory: SchemaFactory
strict_enum: bool

def __init__(self, schema_factory: SchemaFactory) -> None:
def __init__(self, schema_factory: SchemaFactory, strict_enum: bool = True) -> None:
self.schema_factory = schema_factory
self.strict_enum = strict_enum

def build_list(self, data: dict) -> list[Content]:
return [
Expand All @@ -22,8 +28,8 @@ def build_list(self, data: dict) -> list[Content]:

def _create_content(self, content_type: str, content_value: dict) -> Content:
logger.debug(f"Content building [type={content_type}]")

ContentTypeCls: ContentTypeType = ContentType if self.strict_enum else LooseContentType
return Content(
type=ContentType(content_type),
type=ContentTypeCls(content_type),
schema=self.schema_factory.create(content_value)
)
27 changes: 17 additions & 10 deletions src/openapi_parser/builders/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from ..enumeration import DataType, IntegerFormat, NumberFormat, StringFormat
from ..errors import ParserError
from ..specification import Array, Boolean, Discriminator, Integer, Number, Object, OneOf, Property, Schema, String
from ..loose_types import (
LooseIntegerFormat,
LooseNumberFormat,
LooseStringFormat,
)

SchemaBuilderMethod = Callable[[dict], Schema]

Expand Down Expand Up @@ -79,8 +84,10 @@ def merge_all_of_schemas(original_data: dict) -> dict:

class SchemaFactory:
_builders: Dict[DataType, SchemaBuilderMethod]
strict_enum: bool

def __init__(self) -> None:
def __init__(self, strict_enum: bool = True) -> None:
self.strict_enum = strict_enum
self._builders = {
DataType.INTEGER: self._integer,
DataType.NUMBER: self._number,
Expand Down Expand Up @@ -116,39 +123,39 @@ def create(self, data: dict) -> Schema:

return builder_func(data)

@staticmethod
def _integer(data: dict) -> Integer:
def _integer(self, data: dict) -> Integer:
format_cast = IntegerFormat if self.strict_enum else LooseIntegerFormat
attrs_map = {
"multiple_of": PropertyMeta(name="multipleOf", cast=int),
"maximum": PropertyMeta(name="maximum", cast=int),
"exclusive_maximum": PropertyMeta(name="exclusiveMaximum", cast=int),
"minimum": PropertyMeta(name="minimum", cast=int),
"exclusive_minimum": PropertyMeta(name="exclusiveMinimum", cast=int),
"format": PropertyMeta(name="format", cast=IntegerFormat),
"format": PropertyMeta(name="format", cast=format_cast),
}

return Integer(**extract_attrs(data, attrs_map))

@staticmethod
def _number(data: dict) -> Number:
def _number(self, data: dict) -> Number:
format_cast = NumberFormat if self.strict_enum else LooseNumberFormat
attrs_map = {
"multiple_of": PropertyMeta(name="multipleOf", cast=float),
"maximum": PropertyMeta(name="maximum", cast=float),
"exclusive_maximum": PropertyMeta(name="exclusiveMaximum", cast=float),
"minimum": PropertyMeta(name="minimum", cast=float),
"exclusive_minimum": PropertyMeta(name="exclusiveMinimum", cast=float),
"format": PropertyMeta(name="format", cast=NumberFormat),
"format": PropertyMeta(name="format", cast=format_cast),
}

return Number(**extract_attrs(data, attrs_map))

@staticmethod
def _string(data: dict) -> String:
def _string(self, data: dict) -> String:
format_cast = StringFormat if self.strict_enum else LooseStringFormat
attrs_map = {
"max_length": PropertyMeta(name="maxLength", cast=int),
"min_length": PropertyMeta(name="minLength", cast=int),
"pattern": PropertyMeta(name="pattern", cast=None),
"format": PropertyMeta(name="format", cast=StringFormat),
"format": PropertyMeta(name="format", cast=format_cast),
}

return String(**extract_attrs(data, attrs_map))
Expand Down
21 changes: 21 additions & 0 deletions src/openapi_parser/loose_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dataclasses import dataclass


@dataclass
class LooseContentType:
value: str


@dataclass
class LooseIntegerFormat:
value: str


@dataclass
class LooseNumberFormat:
value: str


@dataclass
class LooseStringFormat:
value: str
13 changes: 8 additions & 5 deletions src/openapi_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ def load_specification(self, data: dict) -> Specification:
return Specification(**attrs)


def _create_parser() -> Parser:
def _create_parser(strict_enum: bool = True) -> Parser:
logger.info("Initializing parser")

info_builder = InfoBuilder()
server_builder = ServerBuilder()
external_doc_builder = ExternalDocBuilder()
tag_builder = TagBuilder(external_doc_builder)
schema_factory = SchemaFactory()
content_builder = ContentBuilder(schema_factory)
schema_factory = SchemaFactory(strict_enum=strict_enum)
content_builder = ContentBuilder(schema_factory, strict_enum=strict_enum)
header_builder = HeaderBuilder(schema_factory)
parameter_builder = ParameterBuilder(schema_factory)
schemas_builder = SchemasBuilder(schema_factory)
Expand All @@ -109,15 +109,18 @@ def _create_parser() -> Parser:
schemas_builder)


def parse(uri: str) -> Specification:
def parse(uri: str, strict_enum: bool = True) -> Specification:
"""Parse specification document by URL or filepath
Args:
uri (str): Path or URL to OpenAPI file
strict_enum (bool): Validate content types and string formats against the
enums defined in openapi-parser. Note that the OpenAPI specification allows
for custom values in these properties.
"""
resolver = OpenAPIResolver(uri)
specification = resolver.resolve()

parser = _create_parser()
parser = _create_parser(strict_enum=strict_enum)

return parser.load_specification(specification)
16 changes: 11 additions & 5 deletions src/openapi_parser/specification.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from dataclasses import dataclass, field
from typing import Any, Optional
from typing import Any, Optional, Union

from .enumeration import *
from .loose_types import (
LooseContentType,
LooseIntegerFormat,
LooseNumberFormat,
LooseStringFormat,
)


@dataclass
Expand Down Expand Up @@ -80,7 +86,7 @@ class Integer(Schema):
exclusive_maximum: Optional[int] = None
minimum: Optional[int] = None
exclusive_minimum: Optional[int] = None
format: Optional[IntegerFormat] = None
format: Optional[Union[IntegerFormat, LooseIntegerFormat]] = None


@dataclass
Expand All @@ -90,15 +96,15 @@ class Number(Schema):
exclusive_maximum: Optional[float] = None
minimum: Optional[float] = None
exclusive_minimum: Optional[float] = None
format: Optional[NumberFormat] = None
format: Optional[Union[NumberFormat, LooseNumberFormat]] = None


@dataclass
class String(Schema):
max_length: Optional[int] = None
min_length: Optional[int] = None
pattern: Optional[str] = None
format: Optional[StringFormat] = None
format: Optional[Union[StringFormat, LooseStringFormat]] = None


@dataclass
Expand Down Expand Up @@ -158,7 +164,7 @@ class Parameter:

@dataclass
class Content:
type: ContentType
type: Union[ContentType, LooseContentType]
schema: Schema
# example: Optional[Any] # TODO
# examples: list[Any] = field(default_factory=list) # TODO
Expand Down

0 comments on commit 994f9e6

Please sign in to comment.