1
1
import re
2
+ from copy import deepcopy
2
3
3
4
from apispec import BasePlugin
4
-
5
- from apispec .ext .marshmallow import (
6
- MarshmallowPlugin as _MarshmallowPlugin ,
7
- )
5
+ from apispec .ext .marshmallow import MarshmallowPlugin as _MarshmallowPlugin
8
6
from apispec .ext .marshmallow import OpenAPIConverter
9
7
from flask .views import http_method_funcs
10
8
11
9
from .. import fields
12
10
from ..json .schemas import schema_to_json
13
- from ..schema import EventSchema , build_action_schema
11
+ from ..schema import ActionSchema , EventSchema
14
12
from ..utilities import get_docstring , get_summary , merge
15
13
from ..views import ActionView , EventView , PropertyView , View
14
+ from .utilities import ensure_schema , get_marshmallow_plugin
16
15
17
16
18
17
class ExtendedOpenAPIConverter (OpenAPIConverter ):
@@ -54,6 +53,12 @@ class MarshmallowPlugin(_MarshmallowPlugin):
54
53
class FlaskLabThingsPlugin (BasePlugin ):
55
54
"""APIspec plugin for Flask LabThings"""
56
55
56
+ spec = None
57
+
58
+ def init_spec (self , spec ):
59
+ self .spec = spec
60
+ return super ().init_spec (spec )
61
+
57
62
@classmethod
58
63
def spec_for_interaction (cls , interaction ):
59
64
d = {}
@@ -62,14 +67,18 @@ def spec_for_interaction(cls, interaction):
62
67
if hasattr (interaction , method ):
63
68
prop = getattr (interaction , method )
64
69
d [method ] = {
65
- "description" : getattr (prop , "description" , None )
66
- or get_docstring (prop , remove_newlines = False )
67
- or getattr (interaction , "description" , None )
68
- or get_docstring (interaction , remove_newlines = False ),
69
- "summary" : getattr (prop , "summary" , None )
70
- or get_summary (prop )
71
- or getattr (interaction , "summary" , None )
72
- or get_summary (interaction ),
70
+ "description" : (
71
+ getattr (prop , "description" , None )
72
+ or get_docstring (prop , remove_newlines = False )
73
+ or getattr (interaction , "description" , None )
74
+ or get_docstring (interaction , remove_newlines = False )
75
+ ),
76
+ "summary" : (
77
+ getattr (prop , "summary" , None )
78
+ or get_summary (prop )
79
+ or getattr (interaction , "summary" , None )
80
+ or get_summary (interaction )
81
+ ),
73
82
"tags" : list (interaction .get_tags ()),
74
83
"responses" : {
75
84
"5XX" : {
@@ -87,12 +96,25 @@ def spec_for_interaction(cls, interaction):
87
96
},
88
97
}
89
98
},
99
+ "parameters" : [],
90
100
}
101
+ # Allow custom responses from the class, overridden by the method
102
+ d [method ]["responses" ].update (
103
+ deepcopy (getattr (interaction , "responses" , {}))
104
+ )
105
+ d [method ]["responses" ].update (deepcopy (getattr (prop , "responses" , {})))
106
+ # Allow custom parameters from the class & method
107
+ d [method ]["parameters" ].extend (
108
+ deepcopy (getattr (interaction , "parameters" , {}))
109
+ )
110
+ d [method ]["parameters" ].extend (
111
+ deepcopy (getattr (prop , "parameters" , {}))
112
+ )
91
113
return d
92
114
93
115
@classmethod
94
116
def spec_for_property (cls , prop ):
95
- class_json_schema = schema_to_json (prop .schema ) if prop . schema else None
117
+ class_schema = ensure_schema (prop .schema ) or {}
96
118
97
119
d = cls .spec_for_interaction (prop )
98
120
@@ -103,22 +125,12 @@ def spec_for_property(cls, prop):
103
125
d .get (method , {}),
104
126
{
105
127
"requestBody" : {
106
- "content" : {
107
- prop .content_type : (
108
- {"schema" : class_json_schema }
109
- if class_json_schema
110
- else {}
111
- )
112
- }
128
+ "content" : {prop .content_type : {"schema" : class_schema }}
113
129
},
114
130
"responses" : {
115
131
200 : {
116
132
"content" : {
117
- prop .content_type : (
118
- {"schema" : class_json_schema }
119
- if class_json_schema
120
- else {}
121
- )
133
+ prop .content_type : {"schema" : class_schema }
122
134
},
123
135
"description" : "Write property" ,
124
136
}
@@ -133,36 +145,57 @@ def spec_for_property(cls, prop):
133
145
{
134
146
"responses" : {
135
147
200 : {
136
- "content" : {
137
- prop .content_type : (
138
- {"schema" : class_json_schema }
139
- if class_json_schema
140
- else {}
141
- )
142
- },
148
+ "content" : {prop .content_type : {"schema" : class_schema }},
143
149
"description" : "Read property" ,
144
150
}
145
151
},
146
152
},
147
153
)
148
154
149
- # Enable custom responses from all methods
150
- for method in d .keys ():
151
- d [method ]["responses" ].update (prop .responses )
152
-
153
155
return d
154
156
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 )
157
+ def spec_for_action (self , action ):
158
+ action_input = ensure_schema (action .args , name = f"{ action .__name__ } InputSchema" )
159
+ action_output = ensure_schema (
160
+ action .schema , name = f"{ action .__name__ } OutputSchema"
163
161
)
162
+ # We combine input/output parameters with ActionSchema using an
163
+ # allOf directive, so we don't end up duplicating the schema
164
+ # for every action.
165
+ if action_output or action_input :
166
+ # It would be neater to combine the schemas in OpenAPI with allOf
167
+ # I think the code below does it - but I'm not yet convinced it is working
168
+ # TODO: add tests to validate this
169
+ plugin = get_marshmallow_plugin (self .spec )
170
+ action_input_dict = (
171
+ plugin .resolver .resolve_schema_dict (action_input )
172
+ if action_input
173
+ else {}
174
+ )
175
+ action_output_dict = (
176
+ plugin .resolver .resolve_schema_dict (action_output )
177
+ if action_output
178
+ else {}
179
+ )
180
+ action_schema = {
181
+ "allOf" : [
182
+ plugin .resolver .resolve_schema_dict (ActionSchema ),
183
+ {
184
+ "type" : "object" ,
185
+ "properties" : {
186
+ "input" : action_input_dict ,
187
+ "output" : action_output_dict ,
188
+ },
189
+ },
190
+ ]
191
+ }
192
+ # The line below builds an ActionSchema subclass. This works and
193
+ # is valid, but results in ActionSchema being duplicated many times...
194
+ # action_schema = build_action_schema(action_output, action_input)
195
+ else :
196
+ action_schema = ActionSchema
164
197
165
- d = cls .spec_for_interaction (action )
198
+ d = self .spec_for_interaction (action )
166
199
167
200
# Add in Action spec
168
201
d = merge (
@@ -172,7 +205,7 @@ def spec_for_action(cls, action):
172
205
"requestBody" : {
173
206
"content" : {
174
207
action .content_type : (
175
- {"schema" : class_args } if class_args else {}
208
+ {"schema" : action_input } if action_input else {}
176
209
)
177
210
}
178
211
},
@@ -181,25 +214,14 @@ def spec_for_action(cls, action):
181
214
# 200 responses with cls.responses = {200: {...}}
182
215
200 : {
183
216
"description" : "Action completed immediately" ,
184
- # Allow customising 200 (immediate response) content type
185
- "content" : {
186
- action .response_content_type : (
187
- {"schema" : action_json_schema }
188
- if action_json_schema
189
- else {}
190
- )
191
- },
217
+ # Allow customising 200 (immediate response) content type?
218
+ # TODO: I'm not convinced it's still possible to customise this.
219
+ "content" : {"application/json" : {"schema" : action_schema }},
192
220
},
193
221
201 : {
194
222
"description" : "Action started" ,
195
223
# Our POST 201 MUST be application/json
196
- "content" : {
197
- "application/json" : (
198
- {"schema" : action_json_schema }
199
- if action_json_schema
200
- else {}
201
- )
202
- },
224
+ "content" : {"application/json" : {"schema" : action_schema }},
203
225
},
204
226
},
205
227
},
@@ -210,18 +232,19 @@ def spec_for_action(cls, action):
210
232
"description" : "Action queue" ,
211
233
"content" : {
212
234
"application/json" : (
213
- {"schema" : queue_json_schema }
214
- if queue_json_schema
215
- else {}
235
+ {
236
+ "schema" : {
237
+ "type" : "array" ,
238
+ "items" : action_schema ,
239
+ }
240
+ }
216
241
)
217
242
},
218
243
}
219
244
},
220
245
},
221
246
},
222
247
)
223
- # Enable custom responses from POST
224
- d ["post" ]["responses" ].update (action .responses )
225
248
return d
226
249
227
250
@classmethod
0 commit comments