Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support schemas in callbacks #544

Merged
merged 6 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ Contributors (chronological)
- Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_
- Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_
- Colin Bounouar `@Colin-b <https://github.com/Colin-b>`_
- Mikko Kortelainen `@kortsi <https://github.com/kortsi>
- David Bishop `@teancom <https://github.com/teancom>`_
- Andrea Ghensi `@sanzoghenzo <https://github.com/sanzoghenzo>`_
52 changes: 52 additions & 0 deletions src/apispec/ext/marshmallow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,57 @@ def response_helper(self, response, **kwargs):
self.resolver.resolve_response(response)
return response

def resolve_callback(self, callbacks):
"""Resolve marshmallow Schemas in a dict mapping callback name to OpenApi `Callback Object
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject`_.

This is done recursively, so it it is possible to define callbacks in your callbacks.

Example: ::

#Input
{
"userEvent": {
"https://my.example/user-callback": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": UserSchema
}
}
}
},
}
}
}

#Output
{
"userEvent": {
"https://my.example/user-callback": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
},
}
}
}


"""
for callback in callbacks.values():
if isinstance(callback, dict):
for path in callback.values():
self.operation_helper(path)

def operation_helper(self, operations, **kwargs):
for operation in operations.values():
if not isinstance(operation, dict):
Expand All @@ -195,6 +246,7 @@ def operation_helper(self, operations, **kwargs):
operation["parameters"]
)
if self.openapi_version.major >= 3:
self.resolve_callback(operation.get("callbacks", {}))
if "requestBody" in operation:
self.resolver.resolve_schema(operation["requestBody"])
for response in operation.get("responses", {}).values():
Expand Down
258 changes: 258 additions & 0 deletions tests/test_ext_marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ def get_nested_schema(schema, field_name):


class TestOperationHelper:
@pytest.fixture
def make_pet_callback_spec(self, spec_fixture):
def _make_pet_spec(operations):
spec_fixture.spec.path(
path="/pet",
operations={
"post": {"callbacks": {"petEvent": {"petCallbackUrl": operations}}}
},
)
return spec_fixture

return _make_pet_spec

@pytest.mark.parametrize(
"pet_schema",
(PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
Expand Down Expand Up @@ -417,6 +430,56 @@ def test_schema_v3(self, spec_fixture, pet_schema):
assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
assert get["responses"]["200"]["description"] == "successful operation"

@pytest.mark.parametrize(
"pet_schema",
(PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
)
@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
def test_callback_schema_v3(self, make_pet_callback_spec, pet_schema):
spec_fixture = make_pet_callback_spec(
{
"get": {
"responses": {
"200": {
"content": {"application/json": {"schema": pet_schema}},
"description": "successful operation",
"headers": {"PetHeader": {"schema": pet_schema}},
}
}
}
}
)
p = get_paths(spec_fixture.spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
get = c["get"]
if isinstance(pet_schema, Schema) and pet_schema.many is True:
assert (
get["responses"]["200"]["content"]["application/json"]["schema"]["type"]
== "array"
)
schema_reference = get["responses"]["200"]["content"]["application/json"][
"schema"
]["items"]
assert (
get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
== "array"
)
header_reference = get["responses"]["200"]["headers"]["PetHeader"][
"schema"
]["items"]
else:
schema_reference = get["responses"]["200"]["content"]["application/json"][
"schema"
]
header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]

assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
assert len(spec_fixture.spec.components._schemas) == 1
resolved_schema = spec_fixture.spec.components._schemas["Pet"]
assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
assert get["responses"]["200"]["description"] == "successful operation"

@pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
def test_schema_expand_parameters_v2(self, spec_fixture):
spec_fixture.spec.path(
Expand Down Expand Up @@ -485,6 +548,41 @@ def test_schema_expand_parameters_v3(self, spec_fixture):
assert post["requestBody"]["description"] == "a pet schema"
assert post["requestBody"]["required"]

@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
def test_callback_schema_expand_parameters_v3(self, make_pet_callback_spec):
spec_fixture = make_pet_callback_spec(
{
"get": {"parameters": [{"in": "query", "schema": PetSchema}]},
"post": {
"requestBody": {
"description": "a pet schema",
"required": True,
"content": {"application/json": {"schema": PetSchema}},
}
},
}
)
p = get_paths(spec_fixture.spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
get = c["get"]
assert get["parameters"] == spec_fixture.openapi.schema2parameters(
PetSchema(), location="query"
)
for parameter in get["parameters"]:
description = parameter.get("description", False)
assert description
name = parameter["name"]
assert description == PetSchema.description[name]
post = c["post"]
post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict(
PetSchema
)
assert (
post["requestBody"]["content"]["application/json"]["schema"] == post_schema
)
assert post["requestBody"]["description"] == "a pet schema"
assert post["requestBody"]["required"]

@pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
def test_schema_uses_ref_if_available_v2(self, spec_fixture):
spec_fixture.spec.components.schema("Pet", schema=PetSchema)
Expand Down Expand Up @@ -514,6 +612,24 @@ def test_schema_uses_ref_if_available_v3(self, spec_fixture):
"schema"
] == build_ref(spec_fixture.spec, "schema", "Pet")

@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
def test_callback_schema_uses_ref_if_available_v3(self, make_pet_callback_spec):
spec_fixture = make_pet_callback_spec(
{
"get": {
"responses": {
"200": {"content": {"application/json": {"schema": PetSchema}}}
}
}
}
)
p = get_paths(spec_fixture.spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
get = c["get"]
assert get["responses"]["200"]["content"]["application/json"][
"schema"
] == build_ref(spec_fixture.spec, "schema", "Pet")

def test_schema_uses_ref_if_available_name_resolver_returns_none_v2(self):
def resolver(schema):
return None
Expand Down Expand Up @@ -606,6 +722,48 @@ def resolver(schema):
in get["responses"]["200"]["content"]["application/json"]["schema"]
)

def test_callback_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
def resolver(schema):
return None

spec = APISpec(
title="Test auto-reference",
version="0.1",
openapi_version="3.0.0",
plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
)
spec.components.schema("Pet", schema=PetSchema)
spec.path(
path="/pet",
operations={
"post": {
"callbacks": {
"petEvent": {
"petCallbackUrl": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": PetSchema
}
}
}
}
}
}
}
}
}
},
)
p = get_paths(spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
get = c["get"]
assert get["responses"]["200"]["content"]["application/json"][
"schema"
] == build_ref(spec, "schema", "Pet")

@pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2(
self, spec_fixture
Expand Down Expand Up @@ -648,6 +806,27 @@ def test_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")

@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
def test_callback_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
self, make_pet_callback_spec
):
spec_fixture = make_pet_callback_spec(
{
"get": {"parameters": [{"in": "query", "schema": PetSchema}]},
"post": {
"requestBody": {
"content": {"application/json": {"schema": PetSchema}}
}
},
}
)
p = get_paths(spec_fixture.spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
assert "schema" in c["get"]["parameters"][0]
post = c["post"]
schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")

@pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
def test_schema_array_uses_ref_if_available_v2(self, spec_fixture):
spec_fixture.spec.components.schema("Pet", schema=PetSchema)
Expand Down Expand Up @@ -720,6 +899,51 @@ def test_schema_array_uses_ref_if_available_v3(self, spec_fixture):
]
assert response_schema == resolved_schema

@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
def test_callback_schema_array_uses_ref_if_available_v3(
self, make_pet_callback_spec
):
spec_fixture = make_pet_callback_spec(
{
"get": {
"parameters": [
{
"name": "Pet",
"in": "query",
"content": {
"application/json": {
"schema": {"type": "array", "items": PetSchema}
}
},
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {"type": "array", "items": PetSchema}
}
}
}
},
}
}
)
p = get_paths(spec_fixture.spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
get = c["get"]
assert len(get["parameters"]) == 1
resolved_schema = {
"type": "array",
"items": build_ref(spec_fixture.spec, "schema", "Pet"),
}
request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
assert request_schema == resolved_schema
response_schema = get["responses"]["200"]["content"]["application/json"][
"schema"
]
assert response_schema == resolved_schema

@pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
def test_schema_partially_v2(self, spec_fixture):
spec_fixture.spec.components.schema("Pet", schema=PetSchema)
Expand Down Expand Up @@ -784,6 +1008,40 @@ def test_schema_partially_v3(self, spec_fixture):
},
}

@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
def test_callback_schema_partially_v3(self, make_pet_callback_spec):
spec_fixture = make_pet_callback_spec(
{
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"mother": PetSchema,
"father": PetSchema,
},
}
}
}
}
}
}
}
)
p = get_paths(spec_fixture.spec)["/pet"]
c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
get = c["get"]
assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
"type": "object",
"properties": {
"mother": build_ref(spec_fixture.spec, "schema", "Pet"),
"father": build_ref(spec_fixture.spec, "schema", "Pet"),
},
}

def test_parameter_reference(self, spec_fixture):
if spec_fixture.spec.openapi_version.major < 3:
param = {"schema": PetSchema}
Expand Down