Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion sphinxcontrib/openapi/schema_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@
}


# Keywords an example can possibly be derived from. A schema that has none of
# them carries annotations only, and thus contributes no example. Please note,
# '$ref' is absent because references are expected to be resolved before a
# schema gets here, and 'default' because it's not consulted below.
_EXAMPLE_KEYWORDS = frozenset(
["example", "oneOf", "anyOf", "allOf", "enum", "type", "properties", "items"]
)


# Tells "no example has been found yet" apart from an example that is 'None',
# which is what an explicit 'example: null' produces. Never returned.
_NO_EXAMPLE = object()


def example_from_schema(schema):
"""
Generates an example request/response body from the provided schema.
Expand Down Expand Up @@ -62,8 +76,42 @@ def example_from_schema(schema):
elif "allOf" in schema:
# Combine schema examples
example = {}

# Merging examples only makes sense for objects. If a subschema is of
# any other type, an instance of the composed schema is a value of that
# type, so the composed example is that value and there's nothing to
# merge it into. Every subschema is still visited, so that the outcome
# doesn't depend on their order, and an explicitly provided example wins
# over one derived from a type.
non_object_example = _NO_EXAMPLE
non_object_example_is_explicit = False

for sub_schema in schema["allOf"]:
example.update(example_from_schema(sub_schema))
# OAS 3.1 allows a subschema to be a boolean, which carries no
# example to contribute.
if not isinstance(sub_schema, dict):
continue

# A subschema that carries annotations only, a lone 'description'
# being the common case, has no example to contribute either.
if not _EXAMPLE_KEYWORDS & sub_schema.keys():
continue

sub_example = example_from_schema(sub_schema)

if not isinstance(sub_example, dict):
is_explicit = "example" in sub_schema
if non_object_example is _NO_EXAMPLE or (
is_explicit and not non_object_example_is_explicit
):
non_object_example = sub_example
non_object_example_is_explicit = is_explicit
continue

example.update(sub_example)

if non_object_example is not _NO_EXAMPLE:
return non_object_example
return example

elif "enum" in schema:
Expand Down
56 changes: 56 additions & 0 deletions tests/renderers/httpdomain/test_render_restructuredtext_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,62 @@ def test_oas3_generate_examples_from_schema(fakestate, oas_fragment):
""")


def test_oas3_generate_examples_from_schema_with_composed_property(
fakestate, oas_fragment
):
"""Schema of a property composed with 'allOf' can be used to generate an example."""

testrenderer = renderers.HttpdomainRenderer(
fakestate, {"generate-examples-from-schemas": True}
)
markup = textify(testrenderer.render_restructuredtext_markup(oas_fragment("""
openapi: 3.0.3
info:
title: An example spec
version: 1.0
paths:
/test:
get:
description: an operation description
responses:
'200':
content:
application/json:
schema:
type: object
properties:
status:
description: a property description
allOf:
- type: string
enum:
- PENDING
- RUNNING
description: a response description
""")))
assert markup == textwrap.dedent("""\
.. http:get:: /test

an operation description

:resjson status:
a property description
:resjsonobj status: string:enum

:statuscode 200:
a response description

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json

{
"status": "PENDING"
}
""")


def test_oas3_request_body(testrenderer, oas_fragment):
"""Request body example is rendered."""

Expand Down
103 changes: 103 additions & 0 deletions tests/test_schema_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,109 @@
},
id="oneOf_anyOf_allOf",
),
pytest.param(
{
"type": "object",
"properties": {
"timestamp": {
"allOf": [{"type": "string", "example": "2020-01-01T01:01:01Z"}]
},
"status": {
"description": "The status of the job.",
"allOf": [{"type": "string", "enum": ["PENDING", "RUNNING"]}],
},
"count": {"allOf": [{"type": "integer"}]},
"tags": {"allOf": [{"type": "array", "items": {"type": "string"}}]},
},
},
{
"timestamp": "2020-01-01T01:01:01Z",
"status": "PENDING",
"count": 1,
"tags": ["string", "string"],
},
id="allOf_of_non_object",
),
pytest.param(
{
"type": "object",
"properties": {
"annotated": {
"allOf": [
{
"type": "object",
"properties": {"one": {"type": "string"}},
},
{"description": "this only annotates the schema above"},
]
},
},
},
{"annotated": {"one": "string"}},
id="allOf_with_annotation_only_subschema",
),
pytest.param(
{
"type": "object",
"properties": {
"explicit_last": {
"allOf": [
{"type": "string"},
{"type": "string", "example": "PENDING"},
]
},
"explicit_first": {
"allOf": [
{"type": "string", "example": "PENDING"},
{"type": "string"},
]
},
},
},
{"explicit_last": "PENDING", "explicit_first": "PENDING"},
id="allOf_of_non_object_prefers_explicit_example",
),
pytest.param(
{
"type": "object",
"properties": {
"boolean_subschema": {
"allOf": [
True,
{
"type": "object",
"properties": {"one": {"type": "string"}},
},
]
},
"only_boolean_subschema": {"allOf": [True]},
},
},
{"boolean_subschema": {"one": "string"}, "only_boolean_subschema": {}},
id="allOf_with_boolean_subschema",
),
pytest.param(
# A composition of an object and a non-object cannot be satisfied,
# so there's no sensible example to generate. Rendering the
# non-object one at least keeps the type of the last word on what an
# instance looks like.
{
"type": "object",
"properties": {
"unsatisfiable": {
"allOf": [
{
"type": "object",
"properties": {"one": {"type": "string"}},
},
{"type": "string", "example": "PENDING"},
]
},
},
},
{"unsatisfiable": "PENDING"},
id="allOf_of_object_and_non_object",
),
pytest.param(
{
"type": "object",
Expand Down