Skip to content

Commit 17915a4

Browse files
authored
Merge pull request #2 from p1c2u/feature/models
Component schemas models
2 parents 1f01df4 + 5867126 commit 17915a4

File tree

9 files changed

+235
-28
lines changed

9 files changed

+235
-28
lines changed

openapi_core/components.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from openapi_core.schemas import SchemasGenerator
2+
3+
4+
class Components(object):
5+
"""Represents an OpenAPI Components in a service."""
6+
7+
def __init__(
8+
self, schemas=None, responses=None, parameters=None,
9+
request_bodies=None):
10+
self.schemas = schemas and dict(schemas) or {}
11+
self.responses = responses and dict(responses) or {}
12+
self.parameters = parameters and dict(parameters) or {}
13+
self.request_bodies = request_bodies and dict(request_bodies) or {}
14+
15+
16+
class ComponentsFactory(object):
17+
18+
def __init__(self, dereferencer):
19+
self.dereferencer = dereferencer
20+
21+
def create(self, components_spec):
22+
components_deref = self.dereferencer.dereference(components_spec)
23+
24+
schemas_spec = components_deref.get('schemas', [])
25+
responses_spec = components_deref.get('responses', [])
26+
parameters_spec = components_deref.get('parameters', [])
27+
request_bodies_spec = components_deref.get('request_bodies', [])
28+
29+
schemas = self._generate_schemas(schemas_spec)
30+
responses = self._generate_response(responses_spec)
31+
parameters = self._generate_parameters(parameters_spec)
32+
request_bodies = self._generate_request_bodies(request_bodies_spec)
33+
return Components(
34+
schemas=list(schemas), responses=responses, parameters=parameters,
35+
request_bodies=request_bodies,
36+
)
37+
38+
def _generate_schemas(self, schemas_spec):
39+
return SchemasGenerator(self.dereferencer).generate(schemas_spec)
40+
41+
def _generate_response(self, responses_spec):
42+
return responses_spec
43+
44+
def _generate_parameters(self, parameters_spec):
45+
return parameters_spec
46+
47+
def _generate_request_bodies(self, request_bodies_spec):
48+
return request_bodies_spec

openapi_core/infos.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class Info(object):
2+
3+
def __init__(self, title, version):
4+
self.title = title
5+
self.version = version
6+
7+
8+
class InfoFactory(object):
9+
10+
def __init__(self, dereferencer):
11+
self.dereferencer = dereferencer
12+
13+
def create(self, info_spec):
14+
info_deref = self.dereferencer.dereference(info_spec)
15+
title = info_deref['title']
16+
version = info_deref['version']
17+
return Info(title, version)

openapi_core/media_types.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""OpenAPI core mediaTypes module"""
22
from six import iteritems
33

4-
from openapi_core.schemas import SchemaFactory
4+
from openapi_core.schemas import SchemaRegistry
55

66

77
class MediaType(object):
@@ -34,4 +34,6 @@ def generate(self, content):
3434
yield content_type, MediaType(content_type, schema)
3535

3636
def _create_schema(self, schema_spec):
37-
return SchemaFactory(self.dereferencer).create(schema_spec)
37+
schema, _ = SchemaRegistry(self.dereferencer).get_or_create(
38+
schema_spec)
39+
return schema

openapi_core/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""OpenAPI core models module"""
2+
3+
4+
class BaseModel(dict):
5+
"""Base class for OpenAPI models."""
6+
7+
def __getattr__(self, attr_name):
8+
"""Only search through properties if attribute not found normally.
9+
:type attr_name: str
10+
"""
11+
try:
12+
return self[attr_name]
13+
except KeyError:
14+
raise AttributeError(
15+
'type object {0!r} has no attribute {1!r}'
16+
.format(type(self).__name__, attr_name)
17+
)
18+
19+
20+
class ModelFactory(object):
21+
22+
def create(self, properties, name=None):
23+
model = BaseModel
24+
if name is not None:
25+
model = type(name, (BaseModel, ), {})
26+
27+
return model(**properties)

openapi_core/parameters.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""OpenAPI core parameters module"""
22
import logging
33

4-
from openapi_core.schemas import SchemaFactory
4+
from openapi_core.schemas import SchemaRegistry
55

66
log = logging.getLogger(__name__)
77

@@ -56,4 +56,6 @@ def generate(self, paramters):
5656
)
5757

5858
def _create_schema(self, schema_spec):
59-
return SchemaFactory(self.dereferencer).create(schema_spec)
59+
schema, _ = SchemaRegistry(self.dereferencer).get_or_create(
60+
schema_spec)
61+
return schema

openapi_core/schemas.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,25 @@
66
from json import loads
77
from six import iteritems
88

9+
from openapi_core.models import ModelFactory
10+
911
log = logging.getLogger(__name__)
1012

1113
DEFAULT_CAST_CALLABLE_GETTER = {
1214
'integer': int,
1315
'number': float,
1416
'boolean': lambda x: bool(strtobool(x)),
15-
'object': loads,
1617
}
1718

1819

1920
class Schema(object):
2021
"""Represents an OpenAPI Schema."""
2122

2223
def __init__(
23-
self, schema_type, properties=None, items=None, spec_format=None,
24-
required=False):
24+
self, schema_type, model=None, properties=None, items=None,
25+
spec_format=None, required=False):
2526
self.type = schema_type
27+
self.model = model
2628
self.properties = properties and dict(properties) or {}
2729
self.items = items
2830
self.format = spec_format
@@ -33,10 +35,10 @@ def __getitem__(self, name):
3335

3436
def get_cast_mapping(self):
3537
mapping = DEFAULT_CAST_CALLABLE_GETTER.copy()
36-
if self.items:
37-
mapping.update({
38-
'array': lambda x: list(map(self.items.unmarshal, x)),
39-
})
38+
mapping.update({
39+
'array': self._unmarshal_collection,
40+
'object': self._unmarshal_object,
41+
})
4042

4143
return defaultdict(lambda: lambda x: x, mapping)
4244

@@ -68,6 +70,19 @@ def unmarshal(self, value):
6870

6971
return casted
7072

73+
def _unmarshal_collection(self, value):
74+
return list(map(self.items.unmarshal, value))
75+
76+
def _unmarshal_object(self, value):
77+
if isinstance(value, (str, bytes)):
78+
value = loads(value)
79+
80+
properties = {}
81+
for prop_name, prop in iteritems(self.properties):
82+
prop_value = value.get(prop_name)
83+
properties[prop_name] = prop.unmarshal(prop_value)
84+
return ModelFactory().create(properties, name=self.model)
85+
7186

7287
class PropertiesGenerator(object):
7388

@@ -90,7 +105,9 @@ def __init__(self, dereferencer):
90105

91106
def create(self, schema_spec):
92107
schema_deref = self.dereferencer.dereference(schema_spec)
108+
93109
schema_type = schema_deref['type']
110+
model = schema_deref.get('x-model', None)
94111
required = schema_deref.get('required', False)
95112
properties_spec = schema_deref.get('properties', None)
96113
items_spec = schema_deref.get('items', None)
@@ -104,10 +121,50 @@ def create(self, schema_spec):
104121
items = self._create_items(items_spec)
105122

106123
return Schema(
107-
schema_type, properties=properties, items=items, required=required)
124+
schema_type, model=model, properties=properties, items=items,
125+
required=required,
126+
)
108127

109128
def _generate_properties(self, properties_spec):
110129
return PropertiesGenerator(self.dereferencer).generate(properties_spec)
111130

112131
def _create_items(self, items_spec):
113-
return SchemaFactory(self.dereferencer).create(items_spec)
132+
return self.create(items_spec)
133+
134+
135+
class SchemaRegistry(SchemaFactory):
136+
137+
def __init__(self, dereferencer):
138+
super(SchemaRegistry, self).__init__(dereferencer)
139+
self._schemas = {}
140+
141+
def get_or_create(self, schema_spec):
142+
schema_deref = self.dereferencer.dereference(schema_spec)
143+
model = schema_deref.get('x-model', None)
144+
145+
if model and model in self._schemas:
146+
return self._schemas[model], False
147+
148+
return self.create(schema_deref), True
149+
150+
def _create_items(self, items_spec):
151+
schema, _ = self.get_or_create(items_spec)
152+
return schema
153+
154+
155+
class SchemasGenerator(object):
156+
157+
def __init__(self, dereferencer):
158+
self.dereferencer = dereferencer
159+
160+
def generate(self, schemas_spec):
161+
schemas_deref = self.dereferencer.dereference(schemas_spec)
162+
163+
for schema_name, schema_spec in iteritems(schemas_deref):
164+
schema = self._create_schema(schema_spec)
165+
yield schema_name, schema
166+
167+
def _create_schema(self, schema_spec):
168+
schema, _ = SchemaRegistry(self.dereferencer).get_or_create(
169+
schema_spec)
170+
return schema

openapi_core/specs.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from openapi_spec_validator import openapi_v3_spec_validator
77

8+
from openapi_core.components import ComponentsFactory
9+
from openapi_core.infos import InfoFactory
810
from openapi_core.paths import PathsGenerator
911

1012

@@ -14,9 +16,11 @@
1416
class Spec(object):
1517
"""Represents an OpenAPI Specification for a service."""
1618

17-
def __init__(self, servers=None, paths=None):
19+
def __init__(self, info, paths, servers=None, components=None):
20+
self.info = info
21+
self.paths = paths and dict(paths)
1822
self.servers = servers or []
19-
self.paths = paths and dict(paths) or {}
23+
self.components = components
2024

2125
def __getitem__(self, path_name):
2226
return self.paths[path_name]
@@ -27,6 +31,9 @@ def get_server_url(self, index=0):
2731
def get_operation(self, path_pattern, http_method):
2832
return self.paths[path_pattern].operations[http_method]
2933

34+
def get_schema(self, name):
35+
return self.components.schemas[name]
36+
3037
# operations shortcuts
3138

3239
get = partialmethod(get_operation, http_method='get')
@@ -50,11 +57,22 @@ def create(self, spec_dict, spec_url=''):
5057

5158
spec_dict_deref = self.dereferencer.dereference(spec_dict)
5259

60+
info_spec = spec_dict_deref.get('info', [])
5361
servers = spec_dict_deref.get('servers', [])
54-
5562
paths = spec_dict_deref.get('paths', [])
63+
components_spec = spec_dict_deref.get('components', [])
64+
65+
info = self._create_info(info_spec)
5666
paths = self._generate_paths(paths)
57-
return Spec(servers=servers, paths=list(paths))
67+
components = self._create_components(components_spec)
68+
spec = Spec(info, list(paths), servers=servers, components=components)
69+
return spec
70+
71+
def _create_info(self, info_spec):
72+
return InfoFactory(self.dereferencer).create(info_spec)
5873

5974
def _generate_paths(self, paths):
6075
return PathsGenerator(self.dereferencer).generate(paths)
76+
77+
def _create_components(self, components_spec):
78+
return ComponentsFactory(self.dereferencer).create(components_spec)

tests/integration/data/v3.0/petstore.yaml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,28 +95,39 @@ paths:
9595
$ref: "#/components/schemas/Error"
9696
components:
9797
schemas:
98+
Address:
99+
type: object
100+
x-model: Address
101+
required:
102+
- city
103+
properties:
104+
street:
105+
type: string
106+
city:
107+
type: string
98108
Pet:
99109
type: object
110+
x-model: Pet
111+
allOf:
112+
- $ref: "#/components/schemas/PetCreate"
100113
required:
101114
- id
102-
- name
103115
properties:
104116
id:
105117
type: integer
106118
format: int64
107-
name:
108-
type: string
109-
tag:
110-
type: string
111119
PetCreate:
112120
type: object
121+
x-model: PetCreate
113122
required:
114123
- name
115124
properties:
116125
name:
117126
type: string
118127
tag:
119128
type: string
129+
address:
130+
$ref: "#/components/schemas/Address"
120131
Pets:
121132
type: array
122133
items:

0 commit comments

Comments
 (0)