sc-oa 0.7.0.15__py3-none-any.whl → 0.7.0.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sc-oa might be problematic. Click here for more details.
- {sc_oa-0.7.0.15.dist-info → sc_oa-0.7.0.16.dist-info}/METADATA +36 -37
- sc_oa-0.7.0.16.dist-info/RECORD +26 -0
- {sc_oa-0.7.0.15.dist-info → sc_oa-0.7.0.16.dist-info}/licenses/LICENSE +23 -23
- sphinxcontrib/__init__.py +10 -10
- sphinxcontrib/openapi/__init__.py +93 -93
- sphinxcontrib/openapi/__main__.py +86 -86
- sphinxcontrib/openapi/_lib2to3.py +378 -378
- sphinxcontrib/openapi/directive.py +58 -58
- sphinxcontrib/openapi/locale/es_ES/LC_MESSAGES/openapi.po +170 -170
- sphinxcontrib/openapi/locale/fr_FR/LC_MESSAGES/openapi.po +170 -170
- sphinxcontrib/openapi/locale/openapi.pot +168 -168
- sphinxcontrib/openapi/openapi20.py +263 -263
- sphinxcontrib/openapi/openapi30.py +749 -749
- sphinxcontrib/openapi/renderers/__init__.py +18 -18
- sphinxcontrib/openapi/renderers/_description.py +26 -26
- sphinxcontrib/openapi/renderers/_httpdomain.py +620 -620
- sphinxcontrib/openapi/renderers/_httpdomain_old.py +59 -59
- sphinxcontrib/openapi/renderers/_model.py +426 -426
- sphinxcontrib/openapi/renderers/_toc.py +48 -48
- sphinxcontrib/openapi/renderers/abc.py +46 -46
- sphinxcontrib/openapi/schema_utils.py +137 -137
- sphinxcontrib/openapi/utils.py +137 -137
- sc_oa-0.7.0.15.dist-info/RECORD +0 -26
- {sc_oa-0.7.0.15.dist-info → sc_oa-0.7.0.16.dist-info}/WHEEL +0 -0
- {sc_oa-0.7.0.15.dist-info → sc_oa-0.7.0.16.dist-info}/top_level.txt +0 -0
|
@@ -1,620 +1,620 @@
|
|
|
1
|
-
"""OpenAPI spec renderer."""
|
|
2
|
-
|
|
3
|
-
import collections.abc
|
|
4
|
-
import copy
|
|
5
|
-
import functools
|
|
6
|
-
import http.client
|
|
7
|
-
import json
|
|
8
|
-
|
|
9
|
-
import deepmerge
|
|
10
|
-
import docutils.parsers.rst.directives as directives
|
|
11
|
-
import m2r2
|
|
12
|
-
import requests
|
|
13
|
-
import sphinx.util.logging as logging
|
|
14
|
-
|
|
15
|
-
from sphinxcontrib.openapi import _lib2to3 as lib2to3
|
|
16
|
-
from sphinxcontrib.openapi.renderers import abc
|
|
17
|
-
from sphinxcontrib.openapi.schema_utils import example_from_schema
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
CaseInsensitiveDict = requests.structures.CaseInsensitiveDict
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def indented(generator, indent=3):
|
|
27
|
-
for item in generator:
|
|
28
|
-
if item:
|
|
29
|
-
item = " " * indent + item
|
|
30
|
-
yield item
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _iterinorder(iterable, order_by, key=lambda x: x, case_sensitive=False):
|
|
34
|
-
"""Iterate over iterable in a given order."""
|
|
35
|
-
|
|
36
|
-
order_by = collections.defaultdict(
|
|
37
|
-
# Assume default priority is `Infinity` which means the lowest one.
|
|
38
|
-
# This value is effectively used if there's no corresponding value in a
|
|
39
|
-
# given 'order_by' array.
|
|
40
|
-
lambda: float("Inf"),
|
|
41
|
-
# Passed 'order_by' may be 'None' which means *do not reorder, use
|
|
42
|
-
# natural order*. In order to avoid special cases in the code, we're
|
|
43
|
-
# simply falling back to an empty 'order_by' array since it effectively
|
|
44
|
-
# means *assume every item in 'iterable' has equal priority*.
|
|
45
|
-
((value, i) for i, value in enumerate(order_by or [])),
|
|
46
|
-
)
|
|
47
|
-
yield from sorted(
|
|
48
|
-
iterable,
|
|
49
|
-
key=lambda value: order_by[
|
|
50
|
-
key(value) if case_sensitive else key(value).lower()
|
|
51
|
-
],
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _iterexamples(media_types, example_preference, examples_from_schemas):
|
|
56
|
-
"""Iterate over examples and return them according to the caller preference."""
|
|
57
|
-
|
|
58
|
-
for content_type in _iterinorder(media_types, example_preference):
|
|
59
|
-
media_type = media_types[content_type]
|
|
60
|
-
|
|
61
|
-
# Look for a example in a bunch of possible places. According to
|
|
62
|
-
# OpenAPI v3 spec, `examples` and `example` keys are mutually
|
|
63
|
-
# exclusive, so there's no much difference between their
|
|
64
|
-
# inspection order, while both must take precedence over a
|
|
65
|
-
# schema example.
|
|
66
|
-
if media_type.get("examples", {}):
|
|
67
|
-
for example in media_type["examples"].values():
|
|
68
|
-
if "externalValue" in example:
|
|
69
|
-
if not example["externalValue"].startswith(("http://", "https://")):
|
|
70
|
-
logger.warning(
|
|
71
|
-
"Not supported protocol in 'externalValue': %s",
|
|
72
|
-
example["externalValue"],
|
|
73
|
-
)
|
|
74
|
-
continue
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
response = requests.get(example["externalValue"])
|
|
78
|
-
response.raise_for_status()
|
|
79
|
-
|
|
80
|
-
example["value"] = response.text
|
|
81
|
-
example.pop("externalValue")
|
|
82
|
-
except Exception:
|
|
83
|
-
logger.error(
|
|
84
|
-
"Cannot retrieve example from: '%s'",
|
|
85
|
-
example["externalValue"],
|
|
86
|
-
)
|
|
87
|
-
continue
|
|
88
|
-
break
|
|
89
|
-
else:
|
|
90
|
-
# If the loop over examples has not been interrupted, we
|
|
91
|
-
# probably didn't find an example to render. In that case,
|
|
92
|
-
# let's try and go next media type.
|
|
93
|
-
continue
|
|
94
|
-
elif media_type.get("example"):
|
|
95
|
-
# Save example from "example" in "examples" compatible format. This
|
|
96
|
-
# allows to treat all returned examples the same way.
|
|
97
|
-
example = {"value": media_type["example"]}
|
|
98
|
-
elif media_type.get("schema", {}).get("example"):
|
|
99
|
-
# Save example from "schema" in "examples" compatible format. This
|
|
100
|
-
# allows to treat all returned examples the same way.
|
|
101
|
-
example = {"value": media_type["schema"]["example"]}
|
|
102
|
-
elif "schema" in media_type and examples_from_schemas:
|
|
103
|
-
# Convert schema to example
|
|
104
|
-
example = {"value": example_from_schema(media_type["schema"])}
|
|
105
|
-
pass
|
|
106
|
-
else:
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
yield content_type, example
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _get_markers_from_object(oas_object, schema):
|
|
113
|
-
"""Retrieve a bunch of OAS object markers."""
|
|
114
|
-
|
|
115
|
-
markers = []
|
|
116
|
-
|
|
117
|
-
schema_type = _get_schema_type(schema)
|
|
118
|
-
if schema_type:
|
|
119
|
-
if schema.get("format"):
|
|
120
|
-
schema_type = f"{schema_type}:{schema['format']}"
|
|
121
|
-
elif schema.get("enum"):
|
|
122
|
-
schema_type = f"{schema_type}:enum"
|
|
123
|
-
markers.append(schema_type)
|
|
124
|
-
elif schema.get("enum"):
|
|
125
|
-
markers.append("enum")
|
|
126
|
-
|
|
127
|
-
if oas_object.get("required"):
|
|
128
|
-
markers.append("required")
|
|
129
|
-
|
|
130
|
-
if oas_object.get("deprecated"):
|
|
131
|
-
markers.append("deprecated")
|
|
132
|
-
|
|
133
|
-
if schema.get("deprecated"):
|
|
134
|
-
markers.append("deprecated")
|
|
135
|
-
|
|
136
|
-
return markers
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _is_json_mimetype(mimetype):
|
|
140
|
-
"""Returns 'True' if a given mimetype implies JSON data."""
|
|
141
|
-
|
|
142
|
-
return any(
|
|
143
|
-
[
|
|
144
|
-
mimetype == "application/json",
|
|
145
|
-
mimetype.startswith("application/") and mimetype.endswith("+json"),
|
|
146
|
-
]
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _is_2xx_status(status_code):
|
|
151
|
-
"""Returns 'True' if a given status code is one of successful."""
|
|
152
|
-
|
|
153
|
-
return str(status_code).startswith("2")
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _get_schema_type(schema):
|
|
157
|
-
"""Retrieve schema type either by reading 'type' or guessing."""
|
|
158
|
-
|
|
159
|
-
# There are a lot of OpenAPI specs out there that may lack 'type' property
|
|
160
|
-
# in their schemas. I fount no explanations on what is expected behaviour
|
|
161
|
-
# in this case neither in OpenAPI nor in JSON Schema specifications. Thus
|
|
162
|
-
# let's assume what everyone assumes, and try to guess schema type at least
|
|
163
|
-
# for two most popular types: 'object' and 'array'.
|
|
164
|
-
if "type" not in schema:
|
|
165
|
-
if "properties" in schema:
|
|
166
|
-
schema_type = "object"
|
|
167
|
-
elif "items" in schema:
|
|
168
|
-
schema_type = "array"
|
|
169
|
-
else:
|
|
170
|
-
schema_type = None
|
|
171
|
-
else:
|
|
172
|
-
schema_type = schema["type"]
|
|
173
|
-
return schema_type
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
_merge_mappings = deepmerge.Merger(
|
|
177
|
-
[(collections.abc.Mapping, deepmerge.strategy.dict.DictStrategies("merge"))],
|
|
178
|
-
["override"],
|
|
179
|
-
["override"],
|
|
180
|
-
).merge
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
class HttpdomainRenderer(abc.RestructuredTextRenderer):
|
|
184
|
-
"""Render OpenAPI v3 using `sphinxcontrib-httpdomain` extension."""
|
|
185
|
-
|
|
186
|
-
_markup_converters = {"commonmark": m2r2.convert, "restructuredtext": lambda x: x}
|
|
187
|
-
_response_examples_for = {"200", "201", "202", "2XX"}
|
|
188
|
-
_request_parameters_order = ["header", "path", "query", "cookie"]
|
|
189
|
-
|
|
190
|
-
option_spec = {
|
|
191
|
-
"markup": functools.partial(directives.choice, values=_markup_converters),
|
|
192
|
-
"http-methods-order": lambda s: s.split(),
|
|
193
|
-
"response-examples-for": None,
|
|
194
|
-
"request-parameters-order": None,
|
|
195
|
-
"example-preference": None,
|
|
196
|
-
"request-example-preference": None,
|
|
197
|
-
"response-example-preference": None,
|
|
198
|
-
"generate-examples-from-schemas": directives.flag,
|
|
199
|
-
"no-json-schema-description": directives.flag,
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
def __init__(self, state, options):
|
|
203
|
-
super().__init__(state, options)
|
|
204
|
-
|
|
205
|
-
self._convert_markup = self._markup_converters[
|
|
206
|
-
options.get("markup", "commonmark")
|
|
207
|
-
]
|
|
208
|
-
self._http_methods_order = [
|
|
209
|
-
http_method.lower() for http_method in options.get("http-methods-order", [])
|
|
210
|
-
]
|
|
211
|
-
self._response_examples_for = options.get(
|
|
212
|
-
"response-examples-for", self._response_examples_for
|
|
213
|
-
)
|
|
214
|
-
self._request_parameters_order = [
|
|
215
|
-
parameter_type.lower()
|
|
216
|
-
for parameter_type in options.get(
|
|
217
|
-
"request-parameters-order", self._request_parameters_order
|
|
218
|
-
)
|
|
219
|
-
]
|
|
220
|
-
self._example_preference = options.get("example-preference")
|
|
221
|
-
self._request_example_preference = options.get(
|
|
222
|
-
"request-example-preference", self._example_preference
|
|
223
|
-
)
|
|
224
|
-
self._response_example_preference = options.get(
|
|
225
|
-
"response-example-preference", self._example_preference
|
|
226
|
-
)
|
|
227
|
-
self._generate_example_from_schema = "generate-examples-from-schemas" in options
|
|
228
|
-
self._json_schema_description = "no-json-schema-description" not in options
|
|
229
|
-
|
|
230
|
-
def render_restructuredtext_markup(self, spec):
|
|
231
|
-
"""Spec render entry point."""
|
|
232
|
-
|
|
233
|
-
if spec.get("swagger") == "2.0":
|
|
234
|
-
spec = lib2to3.convert(spec)
|
|
235
|
-
yield from self.render_paths(spec.get("paths", {}))
|
|
236
|
-
|
|
237
|
-
def render_paths(self, paths):
|
|
238
|
-
"""Render OAS paths item."""
|
|
239
|
-
|
|
240
|
-
for endpoint, path in paths.items():
|
|
241
|
-
common_parameters = path.pop("parameters", [])
|
|
242
|
-
|
|
243
|
-
# OpenAPI's path description may contain objects of different
|
|
244
|
-
# types. Since we're interested in rendering only objects of
|
|
245
|
-
# operation type, let's remove irrelevant one from the definition
|
|
246
|
-
# in order to simplify further code.
|
|
247
|
-
for key in {"summary", "description", "servers"}:
|
|
248
|
-
path.pop(key, None)
|
|
249
|
-
|
|
250
|
-
for method in _iterinorder(path, self._http_methods_order):
|
|
251
|
-
operation = path[method]
|
|
252
|
-
operation.setdefault("parameters", [])
|
|
253
|
-
operation_parameters_ids = set(
|
|
254
|
-
(parameter["name"], parameter["in"])
|
|
255
|
-
for parameter in operation["parameters"]
|
|
256
|
-
)
|
|
257
|
-
operation["parameters"] = [
|
|
258
|
-
parameter
|
|
259
|
-
for parameter in common_parameters
|
|
260
|
-
if (parameter["name"], parameter["in"])
|
|
261
|
-
not in operation_parameters_ids
|
|
262
|
-
] + operation["parameters"]
|
|
263
|
-
|
|
264
|
-
yield from self.render_operation(endpoint, method, operation)
|
|
265
|
-
yield ""
|
|
266
|
-
|
|
267
|
-
def render_operation(self, endpoint, method, operation):
|
|
268
|
-
"""Render OAS operation item."""
|
|
269
|
-
|
|
270
|
-
yield f".. http:{method}:: {endpoint}"
|
|
271
|
-
|
|
272
|
-
if operation.get("deprecated"):
|
|
273
|
-
yield f" :deprecated:"
|
|
274
|
-
yield f""
|
|
275
|
-
|
|
276
|
-
if operation.get("summary"):
|
|
277
|
-
yield f" **{operation['summary']}**"
|
|
278
|
-
yield f""
|
|
279
|
-
|
|
280
|
-
if operation.get("description"):
|
|
281
|
-
yield from indented(
|
|
282
|
-
self._convert_markup(operation["description"]).strip().splitlines()
|
|
283
|
-
)
|
|
284
|
-
yield f""
|
|
285
|
-
|
|
286
|
-
yield from indented(self.render_parameters(operation.get("parameters", [])))
|
|
287
|
-
if "requestBody" in operation:
|
|
288
|
-
yield from indented(
|
|
289
|
-
self.render_request_body(operation["requestBody"], endpoint, method)
|
|
290
|
-
)
|
|
291
|
-
yield from indented(self.render_responses(operation["responses"]))
|
|
292
|
-
|
|
293
|
-
def render_parameters(self, parameters):
|
|
294
|
-
"""Render OAS operation's parameters."""
|
|
295
|
-
|
|
296
|
-
for parameter in _iterinorder(
|
|
297
|
-
parameters, self._request_parameters_order, key=lambda value: value["in"]
|
|
298
|
-
):
|
|
299
|
-
yield from self.render_parameter(parameter)
|
|
300
|
-
|
|
301
|
-
def render_parameter(self, parameter):
|
|
302
|
-
"""Render OAS operation's parameter."""
|
|
303
|
-
|
|
304
|
-
kinds = CaseInsensitiveDict(
|
|
305
|
-
{"path": "param", "query": "queryparam", "header": "reqheader"}
|
|
306
|
-
)
|
|
307
|
-
schema = parameter.get("schema", {})
|
|
308
|
-
|
|
309
|
-
if "content" in parameter:
|
|
310
|
-
# According to OpenAPI v3 spec, 'content' in this case may
|
|
311
|
-
# have one and only one entry. Hence casting its values to
|
|
312
|
-
# list is not expensive and should be acceptable.
|
|
313
|
-
schema = list(parameter["content"].values())[0].get("schema", {})
|
|
314
|
-
|
|
315
|
-
if parameter["in"] not in kinds:
|
|
316
|
-
logger.warning(
|
|
317
|
-
"OpenAPI spec contains parameter '%s' (in: '%s') that cannot "
|
|
318
|
-
"be rendererd.",
|
|
319
|
-
parameter["name"],
|
|
320
|
-
parameter["in"],
|
|
321
|
-
)
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
yield f":{kinds[parameter['in']]} {parameter['name']}:"
|
|
325
|
-
|
|
326
|
-
if parameter.get("description"):
|
|
327
|
-
yield from indented(
|
|
328
|
-
self._convert_markup(parameter["description"]).strip().splitlines()
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
markers = _get_markers_from_object(parameter, schema)
|
|
332
|
-
if markers:
|
|
333
|
-
markers = ", ".join(markers)
|
|
334
|
-
yield f":{kinds[parameter['in']]}type {parameter['name']}: {markers}"
|
|
335
|
-
|
|
336
|
-
def render_request_body(self, request_body, endpoint, method):
|
|
337
|
-
"""Render OAS operation's requestBody."""
|
|
338
|
-
|
|
339
|
-
if self._json_schema_description:
|
|
340
|
-
for content_type, content in request_body["content"].items():
|
|
341
|
-
if _is_json_mimetype(content_type) and content.get("schema"):
|
|
342
|
-
yield from self.render_json_schema_description(
|
|
343
|
-
content["schema"], "req"
|
|
344
|
-
)
|
|
345
|
-
yield ""
|
|
346
|
-
break
|
|
347
|
-
|
|
348
|
-
yield from self.render_request_body_example(request_body, endpoint, method)
|
|
349
|
-
yield ""
|
|
350
|
-
|
|
351
|
-
def render_request_body_example(self, request_body, endpoint, method):
|
|
352
|
-
"""Render OAS operation's requestBody's example."""
|
|
353
|
-
|
|
354
|
-
content_type, example = next(
|
|
355
|
-
_iterexamples(
|
|
356
|
-
request_body["content"],
|
|
357
|
-
self._request_example_preference,
|
|
358
|
-
self._generate_example_from_schema,
|
|
359
|
-
),
|
|
360
|
-
(None, None),
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
if content_type and example:
|
|
364
|
-
example = example["value"]
|
|
365
|
-
|
|
366
|
-
if not isinstance(example, str):
|
|
367
|
-
example = json.dumps(example, indent=2)
|
|
368
|
-
|
|
369
|
-
yield f".. sourcecode:: http"
|
|
370
|
-
yield f""
|
|
371
|
-
yield f" {method.upper()} {endpoint} HTTP/1.1"
|
|
372
|
-
yield f" Content-Type: {content_type}"
|
|
373
|
-
yield f""
|
|
374
|
-
yield from indented(example.splitlines())
|
|
375
|
-
|
|
376
|
-
def render_responses(self, responses):
|
|
377
|
-
"""Render OAS operation's responses."""
|
|
378
|
-
|
|
379
|
-
if self._json_schema_description:
|
|
380
|
-
for status_code, response in responses.items():
|
|
381
|
-
if _is_2xx_status(status_code):
|
|
382
|
-
for content_type, content in response.get("content", {}).items():
|
|
383
|
-
if _is_json_mimetype(content_type) and content.get("schema"):
|
|
384
|
-
yield from self.render_json_schema_description(
|
|
385
|
-
content["schema"], "res"
|
|
386
|
-
)
|
|
387
|
-
yield ""
|
|
388
|
-
break
|
|
389
|
-
break
|
|
390
|
-
|
|
391
|
-
for status_code, response in responses.items():
|
|
392
|
-
# Due to the way how YAML spec is parsed, status code may be
|
|
393
|
-
# infered as integer. In order to spare some cycles on type
|
|
394
|
-
# guessing going on, let's ensure it's always string at this point.
|
|
395
|
-
yield from self.render_response(str(status_code), response)
|
|
396
|
-
|
|
397
|
-
def render_response(self, status_code, response):
|
|
398
|
-
"""Render OAS operation's response."""
|
|
399
|
-
|
|
400
|
-
yield f":statuscode {status_code}:"
|
|
401
|
-
yield from indented(
|
|
402
|
-
self._convert_markup(response["description"]).strip().splitlines()
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
if "content" in response and status_code in self._response_examples_for:
|
|
406
|
-
yield ""
|
|
407
|
-
yield from indented(
|
|
408
|
-
self.render_response_example(response["content"], status_code)
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
if "headers" in response:
|
|
412
|
-
yield ""
|
|
413
|
-
|
|
414
|
-
for header_name, header_value in response["headers"].items():
|
|
415
|
-
# According to OpenAPI v3 specification, if a response header
|
|
416
|
-
# is defined with the name 'Content-Type', it shall be ignored.
|
|
417
|
-
if header_name.lower() == "content-type":
|
|
418
|
-
continue
|
|
419
|
-
|
|
420
|
-
yield f":resheader {header_name}:"
|
|
421
|
-
|
|
422
|
-
if header_value.get("description"):
|
|
423
|
-
yield from indented(
|
|
424
|
-
self._convert_markup(header_value["description"])
|
|
425
|
-
.strip()
|
|
426
|
-
.splitlines()
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
schema = header_value.get("schema", {})
|
|
430
|
-
if "content" in header_value:
|
|
431
|
-
# According to OpenAPI v3 spec, 'content' in this case may
|
|
432
|
-
# have one and only one entry. Hence casting its values to
|
|
433
|
-
# list is not expensive and should be acceptable.
|
|
434
|
-
schema = list(header_value["content"].values())[0].get("schema", {})
|
|
435
|
-
|
|
436
|
-
markers = _get_markers_from_object(header_value, schema)
|
|
437
|
-
if markers:
|
|
438
|
-
markers = ", ".join(markers)
|
|
439
|
-
yield f":resheadertype {header_name}: {markers}"
|
|
440
|
-
|
|
441
|
-
def render_response_example(self, media_type, status_code):
|
|
442
|
-
# OpenAPI 3.0 spec may contain more than one response media type, and
|
|
443
|
-
# each media type may contain more than one example. Rendering all
|
|
444
|
-
# invariants normally is not an option because the result will be hard
|
|
445
|
-
# to read and follow. The best option we can go with at this moment is
|
|
446
|
-
# to render first found example of either response media type. Users
|
|
447
|
-
# should control what to render by putting recommended example first in
|
|
448
|
-
# the list.
|
|
449
|
-
content_type, example = next(
|
|
450
|
-
_iterexamples(
|
|
451
|
-
media_type,
|
|
452
|
-
self._response_example_preference,
|
|
453
|
-
self._generate_example_from_schema,
|
|
454
|
-
),
|
|
455
|
-
(None, None),
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
if content_type and example:
|
|
459
|
-
example = example["value"]
|
|
460
|
-
|
|
461
|
-
if not isinstance(example, str):
|
|
462
|
-
example = json.dumps(example, indent=2)
|
|
463
|
-
|
|
464
|
-
# According to OpenAPI v3 spec, status code may be a special value
|
|
465
|
-
# - "default". It's not quite clear what to render in this case.
|
|
466
|
-
# One possible option is to avoid rendering status code at all.
|
|
467
|
-
# This option, however, suffers from broken code highlighting
|
|
468
|
-
# because Pygments relies on the snippet to start with HTTP
|
|
469
|
-
# protocol line. That said, probably the best we can do at the
|
|
470
|
-
# moment is to render some generic status.
|
|
471
|
-
if status_code == "default":
|
|
472
|
-
status_code = "000"
|
|
473
|
-
status_text = "Reason-Phrase"
|
|
474
|
-
else:
|
|
475
|
-
# According to OpenAPI v3 spec, status code may define a range
|
|
476
|
-
# of response codes. Since we're talking about rendered example
|
|
477
|
-
# here, we may show either code from range, but for the sake of
|
|
478
|
-
# simplicity let's pick the first one.
|
|
479
|
-
status_code = status_code.replace("XX", "00")
|
|
480
|
-
status_text = http.client.responses.get(int(status_code), "-")
|
|
481
|
-
|
|
482
|
-
yield f".. sourcecode:: http"
|
|
483
|
-
yield f""
|
|
484
|
-
yield f" HTTP/1.1 {status_code} {status_text}"
|
|
485
|
-
yield f" Content-Type: {content_type}"
|
|
486
|
-
yield f""
|
|
487
|
-
yield from indented(example.splitlines())
|
|
488
|
-
|
|
489
|
-
def render_json_schema_description(self, schema, req_or_res):
|
|
490
|
-
"""Render JSON schema's description."""
|
|
491
|
-
|
|
492
|
-
def _resolve_combining_schema(schema):
|
|
493
|
-
if "oneOf" in schema:
|
|
494
|
-
# The part with merging is a vague one since I only found a
|
|
495
|
-
# single 'oneOf' example where such merging was assumed, and no
|
|
496
|
-
# explanations in the spec itself.
|
|
497
|
-
merged_schema = schema.copy()
|
|
498
|
-
merged_schema.update(merged_schema.pop("oneOf")[0])
|
|
499
|
-
return merged_schema
|
|
500
|
-
|
|
501
|
-
elif "anyOf" in schema:
|
|
502
|
-
# The part with merging is a vague one since I only found a
|
|
503
|
-
# single 'oneOf' example where such merging was assumed, and no
|
|
504
|
-
# explanations in the spec itself.
|
|
505
|
-
merged_schema = schema.copy()
|
|
506
|
-
merged_schema.update(merged_schema.pop("anyOf")[0])
|
|
507
|
-
return merged_schema
|
|
508
|
-
|
|
509
|
-
elif "allOf" in schema:
|
|
510
|
-
# Since the item is represented by all schemas from the array,
|
|
511
|
-
# the best we can do is to render them all at once
|
|
512
|
-
# sequentially. Please note, the only way the end result will
|
|
513
|
-
# ever make sense is when all schemas from the array are of
|
|
514
|
-
# object type.
|
|
515
|
-
merged_schema = schema.copy()
|
|
516
|
-
for item in merged_schema.pop("allOf"):
|
|
517
|
-
merged_schema = _merge_mappings(merged_schema, copy.deepcopy(item))
|
|
518
|
-
return merged_schema
|
|
519
|
-
|
|
520
|
-
elif "not" in schema:
|
|
521
|
-
# Eh.. do nothing because I have no idea what can we do.
|
|
522
|
-
return {}
|
|
523
|
-
|
|
524
|
-
return schema
|
|
525
|
-
|
|
526
|
-
def _traverse_schema(schema, name, is_required=False):
|
|
527
|
-
schema_type = _get_schema_type(schema)
|
|
528
|
-
|
|
529
|
-
if {"oneOf", "anyOf", "allOf"} & schema.keys():
|
|
530
|
-
# Since an item can represented by either or any schema from
|
|
531
|
-
# the array of schema in case of `oneOf` and `anyOf`
|
|
532
|
-
# respectively, the best we can do for them is to render the
|
|
533
|
-
# first found variant. In other words, we are going to traverse
|
|
534
|
-
# only a single schema variant and leave the rest out. This is
|
|
535
|
-
# by design and it was decided so in order to keep produced
|
|
536
|
-
# description clear and simple.
|
|
537
|
-
yield from _traverse_schema(_resolve_combining_schema(schema), name)
|
|
538
|
-
|
|
539
|
-
elif "not" in schema:
|
|
540
|
-
yield name, {}, is_required
|
|
541
|
-
|
|
542
|
-
elif schema_type == "object":
|
|
543
|
-
if name:
|
|
544
|
-
yield name, schema, is_required
|
|
545
|
-
|
|
546
|
-
required = set(schema.get("required", []))
|
|
547
|
-
|
|
548
|
-
for key, value in schema.get("properties", {}).items():
|
|
549
|
-
# In case of the first recursion call, when 'name' is an
|
|
550
|
-
# empty string, we should go with 'key' only in order to
|
|
551
|
-
# avoid leading dot at the beginning.
|
|
552
|
-
yield from _traverse_schema(
|
|
553
|
-
value,
|
|
554
|
-
f"{name}.{key}" if name else key,
|
|
555
|
-
is_required=key in required,
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
elif schema_type == "array":
|
|
559
|
-
yield from _traverse_schema(schema["items"], f"{name}[]")
|
|
560
|
-
|
|
561
|
-
elif "enum" in schema:
|
|
562
|
-
yield name, schema, is_required
|
|
563
|
-
|
|
564
|
-
elif schema_type is not None:
|
|
565
|
-
yield name, schema, is_required
|
|
566
|
-
|
|
567
|
-
schema = _resolve_combining_schema(schema)
|
|
568
|
-
schema_type = _get_schema_type(schema)
|
|
569
|
-
|
|
570
|
-
# On root level, httpdomain supports only 'object' and 'array' response
|
|
571
|
-
# types. If it's something else, let's do not even try to render it.
|
|
572
|
-
if schema_type not in {"object", "array"}:
|
|
573
|
-
return
|
|
574
|
-
|
|
575
|
-
# According to httpdomain's documentation, 'reqjsonobj' is an alias for
|
|
576
|
-
# 'reqjson'. However, since the same name is passed as a type directive
|
|
577
|
-
# internally, it actually can be used to specify its type. The same
|
|
578
|
-
# goes for 'resjsonobj'.
|
|
579
|
-
directives_map = {
|
|
580
|
-
"req": {
|
|
581
|
-
"object": ("reqjson", "reqjsonobj"),
|
|
582
|
-
"array": ("reqjsonarr", "reqjsonarrtype"),
|
|
583
|
-
},
|
|
584
|
-
"res": {
|
|
585
|
-
"object": ("resjson", "resjsonobj"),
|
|
586
|
-
"array": ("resjsonarr", "resjsonarrtype"),
|
|
587
|
-
},
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
# These httpdomain's fields always expect either JSON Object or JSON
|
|
591
|
-
# Array. No primitive types are allowed as input.
|
|
592
|
-
directive, typedirective = directives_map[req_or_res][schema_type]
|
|
593
|
-
|
|
594
|
-
# Since we use JSON array specific httpdomain directives if a schema
|
|
595
|
-
# we're about to render is an array, there's no need to render that
|
|
596
|
-
# array in the first place.
|
|
597
|
-
if schema_type == "array":
|
|
598
|
-
schema = schema["items"]
|
|
599
|
-
|
|
600
|
-
# Even if a root element is an array, items it contain must not be
|
|
601
|
-
# of a primitive types.
|
|
602
|
-
if _get_schema_type(schema) not in {"object", "array"}:
|
|
603
|
-
return
|
|
604
|
-
|
|
605
|
-
for name, schema, is_required in _traverse_schema(schema, ""):
|
|
606
|
-
yield f":{directive} {name}:"
|
|
607
|
-
|
|
608
|
-
if schema.get("description"):
|
|
609
|
-
yield from indented(
|
|
610
|
-
self._convert_markup(schema["description"]).strip().splitlines()
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
markers = _get_markers_from_object({}, schema)
|
|
614
|
-
|
|
615
|
-
if is_required:
|
|
616
|
-
markers.append("required")
|
|
617
|
-
|
|
618
|
-
if markers:
|
|
619
|
-
markers = ", ".join(markers)
|
|
620
|
-
yield f":{typedirective} {name}: {markers}"
|
|
1
|
+
"""OpenAPI spec renderer."""
|
|
2
|
+
|
|
3
|
+
import collections.abc
|
|
4
|
+
import copy
|
|
5
|
+
import functools
|
|
6
|
+
import http.client
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import deepmerge
|
|
10
|
+
import docutils.parsers.rst.directives as directives
|
|
11
|
+
import m2r2
|
|
12
|
+
import requests
|
|
13
|
+
import sphinx.util.logging as logging
|
|
14
|
+
|
|
15
|
+
from sphinxcontrib.openapi import _lib2to3 as lib2to3
|
|
16
|
+
from sphinxcontrib.openapi.renderers import abc
|
|
17
|
+
from sphinxcontrib.openapi.schema_utils import example_from_schema
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CaseInsensitiveDict = requests.structures.CaseInsensitiveDict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def indented(generator, indent=3):
|
|
27
|
+
for item in generator:
|
|
28
|
+
if item:
|
|
29
|
+
item = " " * indent + item
|
|
30
|
+
yield item
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _iterinorder(iterable, order_by, key=lambda x: x, case_sensitive=False):
|
|
34
|
+
"""Iterate over iterable in a given order."""
|
|
35
|
+
|
|
36
|
+
order_by = collections.defaultdict(
|
|
37
|
+
# Assume default priority is `Infinity` which means the lowest one.
|
|
38
|
+
# This value is effectively used if there's no corresponding value in a
|
|
39
|
+
# given 'order_by' array.
|
|
40
|
+
lambda: float("Inf"),
|
|
41
|
+
# Passed 'order_by' may be 'None' which means *do not reorder, use
|
|
42
|
+
# natural order*. In order to avoid special cases in the code, we're
|
|
43
|
+
# simply falling back to an empty 'order_by' array since it effectively
|
|
44
|
+
# means *assume every item in 'iterable' has equal priority*.
|
|
45
|
+
((value, i) for i, value in enumerate(order_by or [])),
|
|
46
|
+
)
|
|
47
|
+
yield from sorted(
|
|
48
|
+
iterable,
|
|
49
|
+
key=lambda value: order_by[
|
|
50
|
+
key(value) if case_sensitive else key(value).lower()
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _iterexamples(media_types, example_preference, examples_from_schemas):
|
|
56
|
+
"""Iterate over examples and return them according to the caller preference."""
|
|
57
|
+
|
|
58
|
+
for content_type in _iterinorder(media_types, example_preference):
|
|
59
|
+
media_type = media_types[content_type]
|
|
60
|
+
|
|
61
|
+
# Look for a example in a bunch of possible places. According to
|
|
62
|
+
# OpenAPI v3 spec, `examples` and `example` keys are mutually
|
|
63
|
+
# exclusive, so there's no much difference between their
|
|
64
|
+
# inspection order, while both must take precedence over a
|
|
65
|
+
# schema example.
|
|
66
|
+
if media_type.get("examples", {}):
|
|
67
|
+
for example in media_type["examples"].values():
|
|
68
|
+
if "externalValue" in example:
|
|
69
|
+
if not example["externalValue"].startswith(("http://", "https://")):
|
|
70
|
+
logger.warning(
|
|
71
|
+
"Not supported protocol in 'externalValue': %s",
|
|
72
|
+
example["externalValue"],
|
|
73
|
+
)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
response = requests.get(example["externalValue"])
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
|
|
80
|
+
example["value"] = response.text
|
|
81
|
+
example.pop("externalValue")
|
|
82
|
+
except Exception:
|
|
83
|
+
logger.error(
|
|
84
|
+
"Cannot retrieve example from: '%s'",
|
|
85
|
+
example["externalValue"],
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
break
|
|
89
|
+
else:
|
|
90
|
+
# If the loop over examples has not been interrupted, we
|
|
91
|
+
# probably didn't find an example to render. In that case,
|
|
92
|
+
# let's try and go next media type.
|
|
93
|
+
continue
|
|
94
|
+
elif media_type.get("example"):
|
|
95
|
+
# Save example from "example" in "examples" compatible format. This
|
|
96
|
+
# allows to treat all returned examples the same way.
|
|
97
|
+
example = {"value": media_type["example"]}
|
|
98
|
+
elif media_type.get("schema", {}).get("example"):
|
|
99
|
+
# Save example from "schema" in "examples" compatible format. This
|
|
100
|
+
# allows to treat all returned examples the same way.
|
|
101
|
+
example = {"value": media_type["schema"]["example"]}
|
|
102
|
+
elif "schema" in media_type and examples_from_schemas:
|
|
103
|
+
# Convert schema to example
|
|
104
|
+
example = {"value": example_from_schema(media_type["schema"])}
|
|
105
|
+
pass
|
|
106
|
+
else:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
yield content_type, example
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_markers_from_object(oas_object, schema):
|
|
113
|
+
"""Retrieve a bunch of OAS object markers."""
|
|
114
|
+
|
|
115
|
+
markers = []
|
|
116
|
+
|
|
117
|
+
schema_type = _get_schema_type(schema)
|
|
118
|
+
if schema_type:
|
|
119
|
+
if schema.get("format"):
|
|
120
|
+
schema_type = f"{schema_type}:{schema['format']}"
|
|
121
|
+
elif schema.get("enum"):
|
|
122
|
+
schema_type = f"{schema_type}:enum"
|
|
123
|
+
markers.append(schema_type)
|
|
124
|
+
elif schema.get("enum"):
|
|
125
|
+
markers.append("enum")
|
|
126
|
+
|
|
127
|
+
if oas_object.get("required"):
|
|
128
|
+
markers.append("required")
|
|
129
|
+
|
|
130
|
+
if oas_object.get("deprecated"):
|
|
131
|
+
markers.append("deprecated")
|
|
132
|
+
|
|
133
|
+
if schema.get("deprecated"):
|
|
134
|
+
markers.append("deprecated")
|
|
135
|
+
|
|
136
|
+
return markers
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_json_mimetype(mimetype):
|
|
140
|
+
"""Returns 'True' if a given mimetype implies JSON data."""
|
|
141
|
+
|
|
142
|
+
return any(
|
|
143
|
+
[
|
|
144
|
+
mimetype == "application/json",
|
|
145
|
+
mimetype.startswith("application/") and mimetype.endswith("+json"),
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_2xx_status(status_code):
|
|
151
|
+
"""Returns 'True' if a given status code is one of successful."""
|
|
152
|
+
|
|
153
|
+
return str(status_code).startswith("2")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_schema_type(schema):
|
|
157
|
+
"""Retrieve schema type either by reading 'type' or guessing."""
|
|
158
|
+
|
|
159
|
+
# There are a lot of OpenAPI specs out there that may lack 'type' property
|
|
160
|
+
# in their schemas. I fount no explanations on what is expected behaviour
|
|
161
|
+
# in this case neither in OpenAPI nor in JSON Schema specifications. Thus
|
|
162
|
+
# let's assume what everyone assumes, and try to guess schema type at least
|
|
163
|
+
# for two most popular types: 'object' and 'array'.
|
|
164
|
+
if "type" not in schema:
|
|
165
|
+
if "properties" in schema:
|
|
166
|
+
schema_type = "object"
|
|
167
|
+
elif "items" in schema:
|
|
168
|
+
schema_type = "array"
|
|
169
|
+
else:
|
|
170
|
+
schema_type = None
|
|
171
|
+
else:
|
|
172
|
+
schema_type = schema["type"]
|
|
173
|
+
return schema_type
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_merge_mappings = deepmerge.Merger(
|
|
177
|
+
[(collections.abc.Mapping, deepmerge.strategy.dict.DictStrategies("merge"))],
|
|
178
|
+
["override"],
|
|
179
|
+
["override"],
|
|
180
|
+
).merge
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class HttpdomainRenderer(abc.RestructuredTextRenderer):
|
|
184
|
+
"""Render OpenAPI v3 using `sphinxcontrib-httpdomain` extension."""
|
|
185
|
+
|
|
186
|
+
_markup_converters = {"commonmark": m2r2.convert, "restructuredtext": lambda x: x}
|
|
187
|
+
_response_examples_for = {"200", "201", "202", "2XX"}
|
|
188
|
+
_request_parameters_order = ["header", "path", "query", "cookie"]
|
|
189
|
+
|
|
190
|
+
option_spec = {
|
|
191
|
+
"markup": functools.partial(directives.choice, values=_markup_converters),
|
|
192
|
+
"http-methods-order": lambda s: s.split(),
|
|
193
|
+
"response-examples-for": None,
|
|
194
|
+
"request-parameters-order": None,
|
|
195
|
+
"example-preference": None,
|
|
196
|
+
"request-example-preference": None,
|
|
197
|
+
"response-example-preference": None,
|
|
198
|
+
"generate-examples-from-schemas": directives.flag,
|
|
199
|
+
"no-json-schema-description": directives.flag,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def __init__(self, state, options):
|
|
203
|
+
super().__init__(state, options)
|
|
204
|
+
|
|
205
|
+
self._convert_markup = self._markup_converters[
|
|
206
|
+
options.get("markup", "commonmark")
|
|
207
|
+
]
|
|
208
|
+
self._http_methods_order = [
|
|
209
|
+
http_method.lower() for http_method in options.get("http-methods-order", [])
|
|
210
|
+
]
|
|
211
|
+
self._response_examples_for = options.get(
|
|
212
|
+
"response-examples-for", self._response_examples_for
|
|
213
|
+
)
|
|
214
|
+
self._request_parameters_order = [
|
|
215
|
+
parameter_type.lower()
|
|
216
|
+
for parameter_type in options.get(
|
|
217
|
+
"request-parameters-order", self._request_parameters_order
|
|
218
|
+
)
|
|
219
|
+
]
|
|
220
|
+
self._example_preference = options.get("example-preference")
|
|
221
|
+
self._request_example_preference = options.get(
|
|
222
|
+
"request-example-preference", self._example_preference
|
|
223
|
+
)
|
|
224
|
+
self._response_example_preference = options.get(
|
|
225
|
+
"response-example-preference", self._example_preference
|
|
226
|
+
)
|
|
227
|
+
self._generate_example_from_schema = "generate-examples-from-schemas" in options
|
|
228
|
+
self._json_schema_description = "no-json-schema-description" not in options
|
|
229
|
+
|
|
230
|
+
def render_restructuredtext_markup(self, spec):
|
|
231
|
+
"""Spec render entry point."""
|
|
232
|
+
|
|
233
|
+
if spec.get("swagger") == "2.0":
|
|
234
|
+
spec = lib2to3.convert(spec)
|
|
235
|
+
yield from self.render_paths(spec.get("paths", {}))
|
|
236
|
+
|
|
237
|
+
def render_paths(self, paths):
|
|
238
|
+
"""Render OAS paths item."""
|
|
239
|
+
|
|
240
|
+
for endpoint, path in paths.items():
|
|
241
|
+
common_parameters = path.pop("parameters", [])
|
|
242
|
+
|
|
243
|
+
# OpenAPI's path description may contain objects of different
|
|
244
|
+
# types. Since we're interested in rendering only objects of
|
|
245
|
+
# operation type, let's remove irrelevant one from the definition
|
|
246
|
+
# in order to simplify further code.
|
|
247
|
+
for key in {"summary", "description", "servers"}:
|
|
248
|
+
path.pop(key, None)
|
|
249
|
+
|
|
250
|
+
for method in _iterinorder(path, self._http_methods_order):
|
|
251
|
+
operation = path[method]
|
|
252
|
+
operation.setdefault("parameters", [])
|
|
253
|
+
operation_parameters_ids = set(
|
|
254
|
+
(parameter["name"], parameter["in"])
|
|
255
|
+
for parameter in operation["parameters"]
|
|
256
|
+
)
|
|
257
|
+
operation["parameters"] = [
|
|
258
|
+
parameter
|
|
259
|
+
for parameter in common_parameters
|
|
260
|
+
if (parameter["name"], parameter["in"])
|
|
261
|
+
not in operation_parameters_ids
|
|
262
|
+
] + operation["parameters"]
|
|
263
|
+
|
|
264
|
+
yield from self.render_operation(endpoint, method, operation)
|
|
265
|
+
yield ""
|
|
266
|
+
|
|
267
|
+
def render_operation(self, endpoint, method, operation):
|
|
268
|
+
"""Render OAS operation item."""
|
|
269
|
+
|
|
270
|
+
yield f".. http:{method}:: {endpoint}"
|
|
271
|
+
|
|
272
|
+
if operation.get("deprecated"):
|
|
273
|
+
yield f" :deprecated:"
|
|
274
|
+
yield f""
|
|
275
|
+
|
|
276
|
+
if operation.get("summary"):
|
|
277
|
+
yield f" **{operation['summary']}**"
|
|
278
|
+
yield f""
|
|
279
|
+
|
|
280
|
+
if operation.get("description"):
|
|
281
|
+
yield from indented(
|
|
282
|
+
self._convert_markup(operation["description"]).strip().splitlines()
|
|
283
|
+
)
|
|
284
|
+
yield f""
|
|
285
|
+
|
|
286
|
+
yield from indented(self.render_parameters(operation.get("parameters", [])))
|
|
287
|
+
if "requestBody" in operation:
|
|
288
|
+
yield from indented(
|
|
289
|
+
self.render_request_body(operation["requestBody"], endpoint, method)
|
|
290
|
+
)
|
|
291
|
+
yield from indented(self.render_responses(operation["responses"]))
|
|
292
|
+
|
|
293
|
+
def render_parameters(self, parameters):
|
|
294
|
+
"""Render OAS operation's parameters."""
|
|
295
|
+
|
|
296
|
+
for parameter in _iterinorder(
|
|
297
|
+
parameters, self._request_parameters_order, key=lambda value: value["in"]
|
|
298
|
+
):
|
|
299
|
+
yield from self.render_parameter(parameter)
|
|
300
|
+
|
|
301
|
+
def render_parameter(self, parameter):
|
|
302
|
+
"""Render OAS operation's parameter."""
|
|
303
|
+
|
|
304
|
+
kinds = CaseInsensitiveDict(
|
|
305
|
+
{"path": "param", "query": "queryparam", "header": "reqheader"}
|
|
306
|
+
)
|
|
307
|
+
schema = parameter.get("schema", {})
|
|
308
|
+
|
|
309
|
+
if "content" in parameter:
|
|
310
|
+
# According to OpenAPI v3 spec, 'content' in this case may
|
|
311
|
+
# have one and only one entry. Hence casting its values to
|
|
312
|
+
# list is not expensive and should be acceptable.
|
|
313
|
+
schema = list(parameter["content"].values())[0].get("schema", {})
|
|
314
|
+
|
|
315
|
+
if parameter["in"] not in kinds:
|
|
316
|
+
logger.warning(
|
|
317
|
+
"OpenAPI spec contains parameter '%s' (in: '%s') that cannot "
|
|
318
|
+
"be rendererd.",
|
|
319
|
+
parameter["name"],
|
|
320
|
+
parameter["in"],
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
yield f":{kinds[parameter['in']]} {parameter['name']}:"
|
|
325
|
+
|
|
326
|
+
if parameter.get("description"):
|
|
327
|
+
yield from indented(
|
|
328
|
+
self._convert_markup(parameter["description"]).strip().splitlines()
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
markers = _get_markers_from_object(parameter, schema)
|
|
332
|
+
if markers:
|
|
333
|
+
markers = ", ".join(markers)
|
|
334
|
+
yield f":{kinds[parameter['in']]}type {parameter['name']}: {markers}"
|
|
335
|
+
|
|
336
|
+
def render_request_body(self, request_body, endpoint, method):
|
|
337
|
+
"""Render OAS operation's requestBody."""
|
|
338
|
+
|
|
339
|
+
if self._json_schema_description:
|
|
340
|
+
for content_type, content in request_body["content"].items():
|
|
341
|
+
if _is_json_mimetype(content_type) and content.get("schema"):
|
|
342
|
+
yield from self.render_json_schema_description(
|
|
343
|
+
content["schema"], "req"
|
|
344
|
+
)
|
|
345
|
+
yield ""
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
yield from self.render_request_body_example(request_body, endpoint, method)
|
|
349
|
+
yield ""
|
|
350
|
+
|
|
351
|
+
def render_request_body_example(self, request_body, endpoint, method):
|
|
352
|
+
"""Render OAS operation's requestBody's example."""
|
|
353
|
+
|
|
354
|
+
content_type, example = next(
|
|
355
|
+
_iterexamples(
|
|
356
|
+
request_body["content"],
|
|
357
|
+
self._request_example_preference,
|
|
358
|
+
self._generate_example_from_schema,
|
|
359
|
+
),
|
|
360
|
+
(None, None),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if content_type and example:
|
|
364
|
+
example = example["value"]
|
|
365
|
+
|
|
366
|
+
if not isinstance(example, str):
|
|
367
|
+
example = json.dumps(example, indent=2)
|
|
368
|
+
|
|
369
|
+
yield f".. sourcecode:: http"
|
|
370
|
+
yield f""
|
|
371
|
+
yield f" {method.upper()} {endpoint} HTTP/1.1"
|
|
372
|
+
yield f" Content-Type: {content_type}"
|
|
373
|
+
yield f""
|
|
374
|
+
yield from indented(example.splitlines())
|
|
375
|
+
|
|
376
|
+
def render_responses(self, responses):
|
|
377
|
+
"""Render OAS operation's responses."""
|
|
378
|
+
|
|
379
|
+
if self._json_schema_description:
|
|
380
|
+
for status_code, response in responses.items():
|
|
381
|
+
if _is_2xx_status(status_code):
|
|
382
|
+
for content_type, content in response.get("content", {}).items():
|
|
383
|
+
if _is_json_mimetype(content_type) and content.get("schema"):
|
|
384
|
+
yield from self.render_json_schema_description(
|
|
385
|
+
content["schema"], "res"
|
|
386
|
+
)
|
|
387
|
+
yield ""
|
|
388
|
+
break
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
for status_code, response in responses.items():
|
|
392
|
+
# Due to the way how YAML spec is parsed, status code may be
|
|
393
|
+
# infered as integer. In order to spare some cycles on type
|
|
394
|
+
# guessing going on, let's ensure it's always string at this point.
|
|
395
|
+
yield from self.render_response(str(status_code), response)
|
|
396
|
+
|
|
397
|
+
def render_response(self, status_code, response):
|
|
398
|
+
"""Render OAS operation's response."""
|
|
399
|
+
|
|
400
|
+
yield f":statuscode {status_code}:"
|
|
401
|
+
yield from indented(
|
|
402
|
+
self._convert_markup(response["description"]).strip().splitlines()
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if "content" in response and status_code in self._response_examples_for:
|
|
406
|
+
yield ""
|
|
407
|
+
yield from indented(
|
|
408
|
+
self.render_response_example(response["content"], status_code)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if "headers" in response:
|
|
412
|
+
yield ""
|
|
413
|
+
|
|
414
|
+
for header_name, header_value in response["headers"].items():
|
|
415
|
+
# According to OpenAPI v3 specification, if a response header
|
|
416
|
+
# is defined with the name 'Content-Type', it shall be ignored.
|
|
417
|
+
if header_name.lower() == "content-type":
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
yield f":resheader {header_name}:"
|
|
421
|
+
|
|
422
|
+
if header_value.get("description"):
|
|
423
|
+
yield from indented(
|
|
424
|
+
self._convert_markup(header_value["description"])
|
|
425
|
+
.strip()
|
|
426
|
+
.splitlines()
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
schema = header_value.get("schema", {})
|
|
430
|
+
if "content" in header_value:
|
|
431
|
+
# According to OpenAPI v3 spec, 'content' in this case may
|
|
432
|
+
# have one and only one entry. Hence casting its values to
|
|
433
|
+
# list is not expensive and should be acceptable.
|
|
434
|
+
schema = list(header_value["content"].values())[0].get("schema", {})
|
|
435
|
+
|
|
436
|
+
markers = _get_markers_from_object(header_value, schema)
|
|
437
|
+
if markers:
|
|
438
|
+
markers = ", ".join(markers)
|
|
439
|
+
yield f":resheadertype {header_name}: {markers}"
|
|
440
|
+
|
|
441
|
+
def render_response_example(self, media_type, status_code):
|
|
442
|
+
# OpenAPI 3.0 spec may contain more than one response media type, and
|
|
443
|
+
# each media type may contain more than one example. Rendering all
|
|
444
|
+
# invariants normally is not an option because the result will be hard
|
|
445
|
+
# to read and follow. The best option we can go with at this moment is
|
|
446
|
+
# to render first found example of either response media type. Users
|
|
447
|
+
# should control what to render by putting recommended example first in
|
|
448
|
+
# the list.
|
|
449
|
+
content_type, example = next(
|
|
450
|
+
_iterexamples(
|
|
451
|
+
media_type,
|
|
452
|
+
self._response_example_preference,
|
|
453
|
+
self._generate_example_from_schema,
|
|
454
|
+
),
|
|
455
|
+
(None, None),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if content_type and example:
|
|
459
|
+
example = example["value"]
|
|
460
|
+
|
|
461
|
+
if not isinstance(example, str):
|
|
462
|
+
example = json.dumps(example, indent=2)
|
|
463
|
+
|
|
464
|
+
# According to OpenAPI v3 spec, status code may be a special value
|
|
465
|
+
# - "default". It's not quite clear what to render in this case.
|
|
466
|
+
# One possible option is to avoid rendering status code at all.
|
|
467
|
+
# This option, however, suffers from broken code highlighting
|
|
468
|
+
# because Pygments relies on the snippet to start with HTTP
|
|
469
|
+
# protocol line. That said, probably the best we can do at the
|
|
470
|
+
# moment is to render some generic status.
|
|
471
|
+
if status_code == "default":
|
|
472
|
+
status_code = "000"
|
|
473
|
+
status_text = "Reason-Phrase"
|
|
474
|
+
else:
|
|
475
|
+
# According to OpenAPI v3 spec, status code may define a range
|
|
476
|
+
# of response codes. Since we're talking about rendered example
|
|
477
|
+
# here, we may show either code from range, but for the sake of
|
|
478
|
+
# simplicity let's pick the first one.
|
|
479
|
+
status_code = status_code.replace("XX", "00")
|
|
480
|
+
status_text = http.client.responses.get(int(status_code), "-")
|
|
481
|
+
|
|
482
|
+
yield f".. sourcecode:: http"
|
|
483
|
+
yield f""
|
|
484
|
+
yield f" HTTP/1.1 {status_code} {status_text}"
|
|
485
|
+
yield f" Content-Type: {content_type}"
|
|
486
|
+
yield f""
|
|
487
|
+
yield from indented(example.splitlines())
|
|
488
|
+
|
|
489
|
+
def render_json_schema_description(self, schema, req_or_res):
|
|
490
|
+
"""Render JSON schema's description."""
|
|
491
|
+
|
|
492
|
+
def _resolve_combining_schema(schema):
|
|
493
|
+
if "oneOf" in schema:
|
|
494
|
+
# The part with merging is a vague one since I only found a
|
|
495
|
+
# single 'oneOf' example where such merging was assumed, and no
|
|
496
|
+
# explanations in the spec itself.
|
|
497
|
+
merged_schema = schema.copy()
|
|
498
|
+
merged_schema.update(merged_schema.pop("oneOf")[0])
|
|
499
|
+
return merged_schema
|
|
500
|
+
|
|
501
|
+
elif "anyOf" in schema:
|
|
502
|
+
# The part with merging is a vague one since I only found a
|
|
503
|
+
# single 'oneOf' example where such merging was assumed, and no
|
|
504
|
+
# explanations in the spec itself.
|
|
505
|
+
merged_schema = schema.copy()
|
|
506
|
+
merged_schema.update(merged_schema.pop("anyOf")[0])
|
|
507
|
+
return merged_schema
|
|
508
|
+
|
|
509
|
+
elif "allOf" in schema:
|
|
510
|
+
# Since the item is represented by all schemas from the array,
|
|
511
|
+
# the best we can do is to render them all at once
|
|
512
|
+
# sequentially. Please note, the only way the end result will
|
|
513
|
+
# ever make sense is when all schemas from the array are of
|
|
514
|
+
# object type.
|
|
515
|
+
merged_schema = schema.copy()
|
|
516
|
+
for item in merged_schema.pop("allOf"):
|
|
517
|
+
merged_schema = _merge_mappings(merged_schema, copy.deepcopy(item))
|
|
518
|
+
return merged_schema
|
|
519
|
+
|
|
520
|
+
elif "not" in schema:
|
|
521
|
+
# Eh.. do nothing because I have no idea what can we do.
|
|
522
|
+
return {}
|
|
523
|
+
|
|
524
|
+
return schema
|
|
525
|
+
|
|
526
|
+
def _traverse_schema(schema, name, is_required=False):
|
|
527
|
+
schema_type = _get_schema_type(schema)
|
|
528
|
+
|
|
529
|
+
if {"oneOf", "anyOf", "allOf"} & schema.keys():
|
|
530
|
+
# Since an item can represented by either or any schema from
|
|
531
|
+
# the array of schema in case of `oneOf` and `anyOf`
|
|
532
|
+
# respectively, the best we can do for them is to render the
|
|
533
|
+
# first found variant. In other words, we are going to traverse
|
|
534
|
+
# only a single schema variant and leave the rest out. This is
|
|
535
|
+
# by design and it was decided so in order to keep produced
|
|
536
|
+
# description clear and simple.
|
|
537
|
+
yield from _traverse_schema(_resolve_combining_schema(schema), name)
|
|
538
|
+
|
|
539
|
+
elif "not" in schema:
|
|
540
|
+
yield name, {}, is_required
|
|
541
|
+
|
|
542
|
+
elif schema_type == "object":
|
|
543
|
+
if name:
|
|
544
|
+
yield name, schema, is_required
|
|
545
|
+
|
|
546
|
+
required = set(schema.get("required", []))
|
|
547
|
+
|
|
548
|
+
for key, value in schema.get("properties", {}).items():
|
|
549
|
+
# In case of the first recursion call, when 'name' is an
|
|
550
|
+
# empty string, we should go with 'key' only in order to
|
|
551
|
+
# avoid leading dot at the beginning.
|
|
552
|
+
yield from _traverse_schema(
|
|
553
|
+
value,
|
|
554
|
+
f"{name}.{key}" if name else key,
|
|
555
|
+
is_required=key in required,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
elif schema_type == "array":
|
|
559
|
+
yield from _traverse_schema(schema["items"], f"{name}[]")
|
|
560
|
+
|
|
561
|
+
elif "enum" in schema:
|
|
562
|
+
yield name, schema, is_required
|
|
563
|
+
|
|
564
|
+
elif schema_type is not None:
|
|
565
|
+
yield name, schema, is_required
|
|
566
|
+
|
|
567
|
+
schema = _resolve_combining_schema(schema)
|
|
568
|
+
schema_type = _get_schema_type(schema)
|
|
569
|
+
|
|
570
|
+
# On root level, httpdomain supports only 'object' and 'array' response
|
|
571
|
+
# types. If it's something else, let's do not even try to render it.
|
|
572
|
+
if schema_type not in {"object", "array"}:
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# According to httpdomain's documentation, 'reqjsonobj' is an alias for
|
|
576
|
+
# 'reqjson'. However, since the same name is passed as a type directive
|
|
577
|
+
# internally, it actually can be used to specify its type. The same
|
|
578
|
+
# goes for 'resjsonobj'.
|
|
579
|
+
directives_map = {
|
|
580
|
+
"req": {
|
|
581
|
+
"object": ("reqjson", "reqjsonobj"),
|
|
582
|
+
"array": ("reqjsonarr", "reqjsonarrtype"),
|
|
583
|
+
},
|
|
584
|
+
"res": {
|
|
585
|
+
"object": ("resjson", "resjsonobj"),
|
|
586
|
+
"array": ("resjsonarr", "resjsonarrtype"),
|
|
587
|
+
},
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
# These httpdomain's fields always expect either JSON Object or JSON
|
|
591
|
+
# Array. No primitive types are allowed as input.
|
|
592
|
+
directive, typedirective = directives_map[req_or_res][schema_type]
|
|
593
|
+
|
|
594
|
+
# Since we use JSON array specific httpdomain directives if a schema
|
|
595
|
+
# we're about to render is an array, there's no need to render that
|
|
596
|
+
# array in the first place.
|
|
597
|
+
if schema_type == "array":
|
|
598
|
+
schema = schema["items"]
|
|
599
|
+
|
|
600
|
+
# Even if a root element is an array, items it contain must not be
|
|
601
|
+
# of a primitive types.
|
|
602
|
+
if _get_schema_type(schema) not in {"object", "array"}:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
for name, schema, is_required in _traverse_schema(schema, ""):
|
|
606
|
+
yield f":{directive} {name}:"
|
|
607
|
+
|
|
608
|
+
if schema.get("description"):
|
|
609
|
+
yield from indented(
|
|
610
|
+
self._convert_markup(schema["description"]).strip().splitlines()
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
markers = _get_markers_from_object({}, schema)
|
|
614
|
+
|
|
615
|
+
if is_required:
|
|
616
|
+
markers.append("required")
|
|
617
|
+
|
|
618
|
+
if markers:
|
|
619
|
+
markers = ", ".join(markers)
|
|
620
|
+
yield f":{typedirective} {name}: {markers}"
|