11import re
2+ from copy import deepcopy
23
34from apispec import BasePlugin
4-
5- from apispec .ext .marshmallow import (
6- MarshmallowPlugin as _MarshmallowPlugin ,
7- )
5+ from apispec .ext .marshmallow import MarshmallowPlugin as _MarshmallowPlugin
86from apispec .ext .marshmallow import OpenAPIConverter
97from flask .views import http_method_funcs
108
119from .. import fields
1210from ..json .schemas import schema_to_json
13- from ..schema import EventSchema , build_action_schema
11+ from ..schema import ActionSchema , EventSchema
1412from ..utilities import get_docstring , get_summary , merge
1513from ..views import ActionView , EventView , PropertyView , View
14+ from .utilities import ensure_schema , get_marshmallow_plugin
1615
1716
1817class ExtendedOpenAPIConverter (OpenAPIConverter ):
@@ -54,6 +53,12 @@ class MarshmallowPlugin(_MarshmallowPlugin):
5453class FlaskLabThingsPlugin (BasePlugin ):
5554 """APIspec plugin for Flask LabThings"""
5655
56+ spec = None
57+
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 = {}
@@ -62,14 +67,18 @@ def spec_for_interaction(cls, interaction):
6267 if hasattr (interaction , method ):
6368 prop = getattr (interaction , method )
6469 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+ ),
7382 "tags" : list (interaction .get_tags ()),
7483 "responses" : {
7584 "5XX" : {
@@ -87,12 +96,25 @@ def spec_for_interaction(cls, interaction):
8796 },
8897 }
8998 },
99+ "parameters" : [],
90100 }
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+ )
91113 return d
92114
93115 @classmethod
94116 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 {}
96118
97119 d = cls .spec_for_interaction (prop )
98120
@@ -103,22 +125,12 @@ def spec_for_property(cls, prop):
103125 d .get (method , {}),
104126 {
105127 "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 }}
113129 },
114130 "responses" : {
115131 200 : {
116132 "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 }
122134 },
123135 "description" : "Write property" ,
124136 }
@@ -133,36 +145,57 @@ def spec_for_property(cls, prop):
133145 {
134146 "responses" : {
135147 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 }},
143149 "description" : "Read property" ,
144150 }
145151 },
146152 },
147153 )
148154
149- # Enable custom responses from all methods
150- for method in d .keys ():
151- d [method ]["responses" ].update (prop .responses )
152-
153155 return d
154156
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"
163161 )
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
164197
165- d = cls .spec_for_interaction (action )
198+ d = self .spec_for_interaction (action )
166199
167200 # Add in Action spec
168201 d = merge (
@@ -172,7 +205,7 @@ def spec_for_action(cls, action):
172205 "requestBody" : {
173206 "content" : {
174207 action .content_type : (
175- {"schema" : class_args } if class_args else {}
208+ {"schema" : action_input } if action_input else {}
176209 )
177210 }
178211 },
@@ -181,25 +214,14 @@ def spec_for_action(cls, action):
181214 # 200 responses with cls.responses = {200: {...}}
182215 200 : {
183216 "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 }},
192220 },
193221 201 : {
194222 "description" : "Action started" ,
195223 # 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 }},
203225 },
204226 },
205227 },
@@ -210,18 +232,19 @@ def spec_for_action(cls, action):
210232 "description" : "Action queue" ,
211233 "content" : {
212234 "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+ }
216241 )
217242 },
218243 }
219244 },
220245 },
221246 },
222247 )
223- # Enable custom responses from POST
224- d ["post" ]["responses" ].update (action .responses )
225248 return d
226249
227250 @classmethod
0 commit comments