Skip to content

Commit f33cb15

Browse files
Fixes #40 -- allow non_strict enum usage during parsing
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.
1 parent 62672b9 commit f33cb15

File tree

5 files changed

+57
-23
lines changed

5 files changed

+57
-23
lines changed
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import logging
2+
from typing import Type, Union
23

34
from . import SchemaFactory
45
from ..enumeration import ContentType
56
from ..specification import Content
7+
from ..loose_types import LooseContentType
68

79
logger = logging.getLogger(__name__)
810

11+
ContentTypeType = Union[Type[ContentType], Type[LooseContentType]]
12+
913

1014
class ContentBuilder:
1115
schema_factory: SchemaFactory
16+
strict_enum: bool
1217

13-
def __init__(self, schema_factory: SchemaFactory) -> None:
18+
def __init__(self, schema_factory: SchemaFactory, strict_enum: bool = True) -> None:
1419
self.schema_factory = schema_factory
20+
self.strict_enum = strict_enum
1521

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

2329
def _create_content(self, content_type: str, content_value: dict) -> Content:
2430
logger.debug(f"Content building [type={content_type}]")
25-
31+
ContentTypeCls: ContentTypeType = ContentType if self.strict_enum else LooseContentType
2632
return Content(
27-
type=ContentType(content_type),
33+
type=ContentTypeCls(content_type),
2834
schema=self.schema_factory.create(content_value)
2935
)

src/openapi_parser/builders/schema.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from ..enumeration import DataType, IntegerFormat, NumberFormat, StringFormat
66
from ..errors import ParserError
77
from ..specification import Array, Boolean, Discriminator, Integer, Number, Object, OneOf, Property, Schema, String
8+
from ..loose_types import (
9+
LooseIntegerFormat,
10+
LooseNumberFormat,
11+
LooseStringFormat,
12+
)
813

914
SchemaBuilderMethod = Callable[[dict], Schema]
1015

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

8085
class SchemaFactory:
8186
_builders: Dict[DataType, SchemaBuilderMethod]
87+
strict_enum: bool
8288

83-
def __init__(self) -> None:
89+
def __init__(self, strict_enum: bool = True) -> None:
90+
self.strict_enum = strict_enum
8491
self._builders = {
8592
DataType.INTEGER: self._integer,
8693
DataType.NUMBER: self._number,
@@ -116,39 +123,39 @@ def create(self, data: dict) -> Schema:
116123

117124
return builder_func(data)
118125

119-
@staticmethod
120-
def _integer(data: dict) -> Integer:
126+
def _integer(self, data: dict) -> Integer:
127+
format_cast = IntegerFormat if self.strict_enum else LooseIntegerFormat
121128
attrs_map = {
122129
"multiple_of": PropertyMeta(name="multipleOf", cast=int),
123130
"maximum": PropertyMeta(name="maximum", cast=int),
124131
"exclusive_maximum": PropertyMeta(name="exclusiveMaximum", cast=int),
125132
"minimum": PropertyMeta(name="minimum", cast=int),
126133
"exclusive_minimum": PropertyMeta(name="exclusiveMinimum", cast=int),
127-
"format": PropertyMeta(name="format", cast=IntegerFormat),
134+
"format": PropertyMeta(name="format", cast=format_cast),
128135
}
129136

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

132-
@staticmethod
133-
def _number(data: dict) -> Number:
139+
def _number(self, data: dict) -> Number:
140+
format_cast = NumberFormat if self.strict_enum else LooseNumberFormat
134141
attrs_map = {
135142
"multiple_of": PropertyMeta(name="multipleOf", cast=float),
136143
"maximum": PropertyMeta(name="maximum", cast=float),
137144
"exclusive_maximum": PropertyMeta(name="exclusiveMaximum", cast=float),
138145
"minimum": PropertyMeta(name="minimum", cast=float),
139146
"exclusive_minimum": PropertyMeta(name="exclusiveMinimum", cast=float),
140-
"format": PropertyMeta(name="format", cast=NumberFormat),
147+
"format": PropertyMeta(name="format", cast=format_cast),
141148
}
142149

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

145-
@staticmethod
146-
def _string(data: dict) -> String:
152+
def _string(self, data: dict) -> String:
153+
format_cast = StringFormat if self.strict_enum else LooseStringFormat
147154
attrs_map = {
148155
"max_length": PropertyMeta(name="maxLength", cast=int),
149156
"min_length": PropertyMeta(name="minLength", cast=int),
150157
"pattern": PropertyMeta(name="pattern", cast=None),
151-
"format": PropertyMeta(name="format", cast=StringFormat),
158+
"format": PropertyMeta(name="format", cast=format_cast),
152159
}
153160

154161
return String(**extract_attrs(data, attrs_map))

src/openapi_parser/loose_types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class LooseEnum:
6+
value: str
7+
8+
9+
LooseContentType = LooseEnum
10+
LooseIntegerFormat = LooseEnum
11+
LooseNumberFormat = LooseEnum
12+
LooseStringFormat = LooseEnum

src/openapi_parser/parser.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ def load_specification(self, data: dict) -> Specification:
7878
return Specification(**attrs)
7979

8080

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

8484
info_builder = InfoBuilder()
8585
server_builder = ServerBuilder()
8686
external_doc_builder = ExternalDocBuilder()
8787
tag_builder = TagBuilder(external_doc_builder)
88-
schema_factory = SchemaFactory()
89-
content_builder = ContentBuilder(schema_factory)
88+
schema_factory = SchemaFactory(strict_enum=strict_enum)
89+
content_builder = ContentBuilder(schema_factory, strict_enum=strict_enum)
9090
header_builder = HeaderBuilder(schema_factory)
9191
parameter_builder = ParameterBuilder(schema_factory)
9292
schemas_builder = SchemasBuilder(schema_factory)
@@ -109,15 +109,18 @@ def _create_parser() -> Parser:
109109
schemas_builder)
110110

111111

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

121-
parser = _create_parser()
124+
parser = _create_parser(strict_enum=strict_enum)
122125

123126
return parser.load_specification(specification)

src/openapi_parser/specification.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from dataclasses import dataclass, field
2-
from typing import Any, Optional
2+
from typing import Any, Optional, Union
33

44
from .enumeration import *
5+
from .loose_types import (
6+
LooseContentType,
7+
LooseIntegerFormat,
8+
LooseNumberFormat,
9+
LooseStringFormat,
10+
)
511

612

713
@dataclass
@@ -80,7 +86,7 @@ class Integer(Schema):
8086
exclusive_maximum: Optional[int] = None
8187
minimum: Optional[int] = None
8288
exclusive_minimum: Optional[int] = None
83-
format: Optional[IntegerFormat] = None
89+
format: Optional[Union[IntegerFormat, LooseIntegerFormat]] = None
8490

8591

8692
@dataclass
@@ -90,15 +96,15 @@ class Number(Schema):
9096
exclusive_maximum: Optional[float] = None
9197
minimum: Optional[float] = None
9298
exclusive_minimum: Optional[float] = None
93-
format: Optional[NumberFormat] = None
99+
format: Optional[Union[NumberFormat, LooseNumberFormat]] = None
94100

95101

96102
@dataclass
97103
class String(Schema):
98104
max_length: Optional[int] = None
99105
min_length: Optional[int] = None
100106
pattern: Optional[str] = None
101-
format: Optional[StringFormat] = None
107+
format: Optional[Union[StringFormat, LooseStringFormat]] = None
102108

103109

104110
@dataclass
@@ -158,7 +164,7 @@ class Parameter:
158164

159165
@dataclass
160166
class Content:
161-
type: ContentType
167+
type: Union[ContentType, LooseContentType]
162168
schema: Schema
163169
# example: Optional[Any] # TODO
164170
# examples: list[Any] = field(default_factory=list) # TODO

0 commit comments

Comments
 (0)