Skip to content

Commit 9ec6fa2

Browse files
committed
Avoid premature serialisation
We were using JSONSchema to serialise schemas and put them in-line. This resulted in invalid OpenAPI output. Now, I pass through Schema objects whenever possible, or use the resolver to reference them. This results in valid OpenAPI output though currently there are name conflicts that lose data.
1 parent c6c0b5b commit 9ec6fa2

File tree

1 file changed

+46
-42
lines changed

1 file changed

+46
-42
lines changed

src/labthings/apispec/plugins.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
from .. import fields
1212
from ..json.schemas import schema_to_json
13-
from ..schema import EventSchema, build_action_schema
13+
from ..schema import EventSchema, ActionSchema, Schema, build_action_schema
1414
from ..utilities import get_docstring, get_summary, merge
15+
from .utilities import ensure_schema, get_marshamallow_plugin
1516
from ..views import ActionView, EventView, PropertyView, View
1617

1718

@@ -54,6 +55,10 @@ class MarshmallowPlugin(_MarshmallowPlugin):
5455
class FlaskLabThingsPlugin(BasePlugin):
5556
"""APIspec plugin for Flask LabThings"""
5657

58+
def init_spec(self, spec):
59+
self.spec = spec
60+
return super().init_spec(spec)
61+
5762
@classmethod
5863
def spec_for_interaction(cls, interaction):
5964
d = {}
@@ -88,11 +93,13 @@ def spec_for_interaction(cls, interaction):
8893
}
8994
},
9095
}
96+
if hasattr(prop, "responses"):
97+
d[method]["responses"].update(prop.responses)
9198
return d
9299

93100
@classmethod
94101
def spec_for_property(cls, prop):
95-
class_json_schema = schema_to_json(prop.schema) if prop.schema else None
102+
class_schema = ensure_schema(prop.schema) or {}
96103

97104
d = cls.spec_for_interaction(prop)
98105

@@ -104,21 +111,13 @@ def spec_for_property(cls, prop):
104111
{
105112
"requestBody": {
106113
"content": {
107-
prop.content_type: (
108-
{"schema": class_json_schema}
109-
if class_json_schema
110-
else {}
111-
)
114+
prop.content_type: { "schema": class_schema }
112115
}
113116
},
114117
"responses": {
115118
200: {
116119
"content": {
117-
prop.content_type: (
118-
{"schema": class_json_schema}
119-
if class_json_schema
120-
else {}
121-
)
120+
prop.content_type: { "schema": class_schema }
122121
},
123122
"description": "Write property",
124123
}
@@ -134,11 +133,7 @@ def spec_for_property(cls, prop):
134133
"responses": {
135134
200: {
136135
"content": {
137-
prop.content_type: (
138-
{"schema": class_json_schema}
139-
if class_json_schema
140-
else {}
141-
)
136+
prop.content_type: { "schema": class_schema }
142137
},
143138
"description": "Read property",
144139
}
@@ -152,17 +147,30 @@ def spec_for_property(cls, prop):
152147

153148
return d
154149

155-
@classmethod
156-
def spec_for_action(cls, action):
157-
class_args = schema_to_json(action.args)
158-
action_json_schema = schema_to_json(
159-
build_action_schema(action.schema, action.args)()
160-
)
161-
queue_json_schema = schema_to_json(
162-
build_action_schema(action.schema, action.args)(many=True)
163-
)
150+
def spec_for_action(self, action):
151+
action_input = ensure_schema(action.args)
152+
action_output = ensure_schema(action.schema)
153+
# We combine input/output parameters with ActionSchema using an
154+
# allOf directive, so we don't end up duplicating the schema
155+
# for every action.
156+
if action_output or action_input:
157+
# It would be neater to combine the schemas in OpenAPI with allOf
158+
# but this seems to break everything and I don't know why!!
159+
plugin = get_marshamallow_plugin(self.spec)
160+
action_io_schema = build_action_schema(action_output, action_input, base_class=Schema)
161+
action_schema = {
162+
"allOf": [
163+
plugin.resolver.resolve_schema_dict(ActionSchema),
164+
plugin.resolver.resolve_schema_dict(action_io_schema),
165+
]
166+
}
167+
# The line below builds an ActionSchema subclass. This works and
168+
# is valid, but results in ActionSchema being duplicated many times...
169+
#action_schema = build_action_schema(action_output, action_input)
170+
else:
171+
action_schema = ActionSchema
164172

165-
d = cls.spec_for_interaction(action)
173+
d = self.spec_for_interaction(action)
166174

167175
# Add in Action spec
168176
d = merge(
@@ -172,7 +180,7 @@ def spec_for_action(cls, action):
172180
"requestBody": {
173181
"content": {
174182
action.content_type: (
175-
{"schema": class_args} if class_args else {}
183+
{"schema": action_input} if action_input else {}
176184
)
177185
}
178186
},
@@ -181,24 +189,17 @@ def spec_for_action(cls, action):
181189
# 200 responses with cls.responses = {200: {...}}
182190
200: {
183191
"description": "Action completed immediately",
184-
# Allow customising 200 (immediate response) content type
192+
# Allow customising 200 (immediate response) content type?
193+
# TODO: I'm not convinced it's still possible to customise this.
185194
"content": {
186-
action.response_content_type: (
187-
{"schema": action_json_schema}
188-
if action_json_schema
189-
else {}
190-
)
195+
"application/json": { "schema": action_schema }
191196
},
192197
},
193198
201: {
194199
"description": "Action started",
195200
# Our POST 201 MUST be application/json
196201
"content": {
197-
"application/json": (
198-
{"schema": action_json_schema}
199-
if action_json_schema
200-
else {}
201-
)
202+
"application/json": { "schema": action_schema }
202203
},
203204
},
204205
},
@@ -210,9 +211,12 @@ def spec_for_action(cls, action):
210211
"description": "Action queue",
211212
"content": {
212213
"application/json": (
213-
{"schema": queue_json_schema}
214-
if queue_json_schema
215-
else {}
214+
{
215+
"schema": {
216+
"type": "array",
217+
"items": action_schema,
218+
}
219+
}
216220
)
217221
},
218222
}

0 commit comments

Comments
 (0)