Skip to content

Commit 0adb401

Browse files
committed
Added basic schema documentation to Thing Descriptions
1 parent c2aeb54 commit 0adb401

File tree

3 files changed

+96
-7
lines changed

3 files changed

+96
-7
lines changed

labthings/server/decorators.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,32 @@ def ThingProperty(viewcls):
100100
thing_property = ThingProperty
101101

102102

103+
class PropertySchema(object):
104+
def __init__(self, schema, code=200):
105+
"""
106+
:param schema: a dict of whose keys will make up the final
107+
serialized response output
108+
"""
109+
self.schema = schema
110+
self.code = code
111+
112+
def __call__(self, viewcls):
113+
update_spec(viewcls, {"_propertySchema": self.schema})
114+
115+
if hasattr(viewcls, "get") and callable(viewcls.get):
116+
viewcls.get = marshal_with(self.schema, code=self.code)(viewcls.get)
117+
118+
if hasattr(viewcls, "post") and callable(viewcls.post):
119+
viewcls.post = marshal_with(self.schema, code=self.code)(viewcls.post)
120+
viewcls.post = use_args(self.schema)(viewcls.post)
121+
122+
if hasattr(viewcls, "put") and callable(viewcls.put):
123+
viewcls.put = marshal_with(self.schema, code=self.code)(viewcls.put)
124+
viewcls.put = use_args(self.schema)(viewcls.put)
125+
126+
return viewcls
127+
128+
103129
class use_body(object):
104130
"""
105131
Gets the request body as a single value and adds it as a positional argument
@@ -149,7 +175,11 @@ class use_args(object):
149175

150176
def __init__(self, schema, **kwargs):
151177
self.schema = schema
152-
self.wrapper = flaskparser.use_args(schema, **kwargs)
178+
179+
if isinstance(schema, Field):
180+
self.wrapper = use_body(schema, **kwargs)
181+
else:
182+
self.wrapper = flaskparser.use_args(schema, **kwargs)
153183

154184
def __call__(self, f):
155185
# Pass params to call function attribute for external access

labthings/server/spec/__init__.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ def method2operation(method: callable, spec: APISpec):
130130

131131

132132
def convert_schema(schema, spec: APISpec):
133+
"""
134+
Ensure that a given schema is either a real Marshmallow schema,
135+
or is a dictionary describing the schema inline.
136+
137+
Marshmallow schemas are left as they are so that the APISpec module
138+
can add them to the "schemas" list in our APISpec documentation.
139+
"""
133140
if isinstance(schema, BaseSchema):
134141
return schema
135142
elif isinstance(schema, Mapping):
@@ -143,6 +150,9 @@ def convert_schema(schema, spec: APISpec):
143150

144151

145152
def map2properties(schema, spec: APISpec):
153+
"""
154+
Convert any dictionary-like map of Marshmallow fields into a dictionary describing it's JSON schema
155+
"""
146156
marshmallow_plugin = next(
147157
plugin for plugin in spec.plugins if isinstance(plugin, MarshmallowPlugin)
148158
)
@@ -157,13 +167,36 @@ def map2properties(schema, spec: APISpec):
157167
else:
158168
d[k] = v
159169

160-
return {"properties": d}
170+
return {"type": "object", "properties": d}
161171

162172

163173
def field2property(field, spec: APISpec):
174+
"""
175+
Convert a single Marshmallow field into a JSON schema of that field
176+
"""
164177
marshmallow_plugin = next(
165178
plugin for plugin in spec.plugins if isinstance(plugin, MarshmallowPlugin)
166179
)
167180
converter = marshmallow_plugin.converter
168181

169182
return converter.field2property(field)
183+
184+
185+
def schema2json(schema, spec: APISpec):
186+
"""
187+
Convert any Marshmallow schema, field, or dictionary of fields stright to a JSON schema
188+
This should not be used when generating APISpec documentation, otherwise schemas wont
189+
be listed in the "schemas" list. This is used, for example, in the Thing Description.
190+
"""
191+
if not isinstance(schema, BaseSchema):
192+
schema = convert_schema(schema, spec)
193+
194+
if isinstance(schema, BaseSchema):
195+
marshmallow_plugin = next(
196+
plugin for plugin in spec.plugins if isinstance(plugin, MarshmallowPlugin)
197+
)
198+
converter = marshmallow_plugin.converter
199+
200+
schema = converter.schema2jsonschema(schema)
201+
202+
return schema

labthings/server/views/docs/__init__.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ...view import View
1414
from ...find import current_labthing
15-
from ...spec import rule_to_path, rule_to_params
15+
from ...spec import get_spec, rule_to_path, rule_to_params, convert_schema, schema2json
1616

1717

1818
class APISpecView(View):
@@ -46,14 +46,40 @@ def get(self):
4646

4747
props = {}
4848
for key, prop in current_labthing().properties.items():
49+
props[key] = {}
4950
prop_rules = current_app.url_map._rules_by_endpoint.get(prop.endpoint)
5051
prop_urls = [rule_to_path(rule) for rule in prop_rules]
5152

52-
props[key] = {}
53+
# Look for a _propertySchema in the Property classes API SPec
54+
prop_spec = get_spec(prop)
55+
prop_schema = prop_spec.get("_propertySchema")
56+
if not prop_schema:
57+
# If prop is read-only
58+
if hasattr(prop, "get") and not (
59+
hasattr(prop, "post") or hasattr(prop, "put")
60+
):
61+
prop_schema = get_spec(prop.get).get("_schema").get(200)
62+
# If prop is write-only
63+
elif not hasattr(prop, "get") and (
64+
hasattr(prop, "post") or hasattr(prop, "put")
65+
):
66+
if hasattr(prop, "post"):
67+
prop_schema = get_spec(prop.post).get("_params")
68+
elif hasattr(prop, "put"):
69+
prop_schema = get_spec(prop.put).get("_params")
70+
else:
71+
prop_schema = {}
72+
73+
prop_json_schema = schema2json(prop_schema, current_labthing().spec)
74+
75+
props[key].update(prop_json_schema)
76+
77+
# Generate the rest of the description
5378
props[key]["title"] = prop.__name__
54-
# TODO: Get description from __apispec__ preferentially
55-
props[key]["description"] = get_docstring(prop) or (
56-
get_docstring(prop.get) if hasattr(prop, "get") else ""
79+
props[key]["description"] = (
80+
props[key].get("description")
81+
or get_docstring(prop)
82+
or (get_docstring(prop.get) if hasattr(prop, "get") else "")
5783
)
5884
props[key]["readOnly"] = not (
5985
hasattr(prop, "post") or hasattr(prop, "put") or hasattr(prop, "delete")

0 commit comments

Comments
 (0)