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

Add Support for multiple examples in application/json #309

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 7 additions & 1 deletion spectree/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import BaseModel

from ._types import ModelType, NamingStrategy, OptionalModelType
from .utils import gen_list_model, get_model_key, parse_code
from .utils import gen_list_model, get_model_key, has_examples, parse_code

# according to https://tools.ietf.org/html/rfc2616#section-10
# https://tools.ietf.org/html/rfc7231#section-6.1
Expand Down Expand Up @@ -160,4 +160,10 @@ def generate_spec(
},
}

schema_extra = getattr(model.__config__, "schema_extra", None)
if schema_extra and has_examples(schema_extra):
responses[parse_code(code)]["content"]["application/json"][
"examples"
] = schema_extra

return responses
4 changes: 3 additions & 1 deletion spectree/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ async def async_validate(*args: Any, **kwargs: Any):
if model is not None:
model_key = self._add_model(model=model)
setattr(validation, name, model_key)
validation.json_model = model

if resp:
# Make sure that the endpoint specific status code and data model for
Expand Down Expand Up @@ -321,7 +322,8 @@ def _generate_spec(self) -> Dict[str, Any]:
if deprecated:
routes[path][method.lower()]["deprecated"] = deprecated

request_body = parse_request(func)
json_model = getattr(func, "json_model", None)
request_body = parse_request(func, json_model)
if request_body:
routes[path][method.lower()]["requestBody"] = request_body

Expand Down
14 changes: 13 additions & 1 deletion spectree/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ def parse_comments(func: Callable[..., Any]) -> Tuple[Optional[str], Optional[st
return summary, description


def parse_request(func: Any) -> Dict[str, Any]:
def has_examples(schema_exta: dict) -> bool:
for _, v in schema_exta.items():
Comment on lines +79 to +80
Copy link
Collaborator

@yedpodtrzitko yedpodtrzitko May 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this suggestion might be rendered useless by another comment below, but:

Suggested change
def has_examples(schema_exta: dict) -> bool:
for _, v in schema_exta.items():
def has_examples(schema_extra: dict) -> bool:
for v in schema_extra.values():

if isinstance(v, dict) and "value" in v.keys():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: .keys() is superfluous since it's the default place where in is checking

Suggested change
if isinstance(v, dict) and "value" in v.keys():
if isinstance(v, dict) and "value" in v:

return True
return False


def parse_request(func: Any, model: Optional[Any] = None) -> Dict[str, Any]:
"""
get json spec
"""
Expand All @@ -86,6 +93,11 @@ def parse_request(func: Any) -> Dict[str, Any]:
"schema": {"$ref": f"#/components/schemas/{func.json}"}
}

if model:
schema_extra = getattr(model.__config__, "schema_extra", None)
if schema_extra and has_examples(schema_extra):
content_items["application/json"]["examples"] = schema_extra
Comment on lines +97 to +99
Copy link
Collaborator

@yedpodtrzitko yedpodtrzitko May 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code checks if there's ANY dictionary with a key named value in schema_extra, and based on that it assumes that ALL items in schema_extra are examples. Which might not be necessarily the case:

class Data(BaseModel):
    value: bool

    class Config:
        schema_extra = {
            "examples": {
                "example1": {"value": {"key1": "value1", "key2": "value2"}},
                "example2": {"value": {"key1": "value1", "key2": "value2"}},
            },
            "properties": {"value": {"this is not an example": True}},
        }

I think it could be better to nest all examples into schema_extra.examples, check presence of examples, and then then take just that as examples:

if schema_extra and "examples" in schema_extra:
    content_items["application/json"]["examples"] = schema_extra["examples"]

What do you think?


if hasattr(func, "form"):
content_items["multipart/form-data"] = {
"schema": {"$ref": f"#/components/schemas/{func.form}"}
Expand Down
5 changes: 5 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ class DemoModel(BaseModel):
name: str = Field(..., description="user name")


class DemoModelWithSchemaExtra(BaseModel):
class Config:
schema_extra = {"example1": {"value": {"key1": "value1", "key2": "value2"}}}


class DemoQuery(BaseModel):
names1: List[str] = Field(...)
names2: List[str] = Field(..., style="matrix", explode=True, non_keyword="dummy")
Expand Down
30 changes: 29 additions & 1 deletion tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from spectree.response import DEFAULT_CODE_DESC, Response
from spectree.utils import gen_list_model

from .common import JSON, DemoModel, get_model_path_key
from .common import JSON, DemoModel, DemoModelWithSchemaExtra, get_model_path_key


class NormalClass:
Expand Down Expand Up @@ -107,6 +107,34 @@ def test_response_spec():
assert spec.get(404) is None


def test_response_spec_with_schema_extra():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if possible, can you use the fixture snapshot_json and compare the whole payload instead of cherrypicked parts? See example here:

def test_plugin_spec(api, snapshot_json):
models = {
get_model_key(model=m): get_model_schema(model=m)
for m in (Query, JSON, Resp, Cookies, Headers)
}
for name, schema in models.items():
schema.pop("definitions", None)
assert api.spec["components"]["schemas"][name] == schema
assert api.spec == snapshot_json(name="full_spec")

to generate the snapshot, add the following parameter: pytest --snapshot-update

resp = Response(
"HTTP_200",
HTTP_201=DemoModelWithSchemaExtra,
HTTP_401=(DemoModelWithSchemaExtra, "custom code description"),
HTTP_402=(None, "custom code description"),
)
resp.add_model(422, ValidationError)
spec = resp.generate_spec()
assert spec["200"]["description"] == DEFAULT_CODE_DESC["HTTP_200"]
assert spec["201"]["description"] == DEFAULT_CODE_DESC["HTTP_201"]
assert spec["422"]["description"] == DEFAULT_CODE_DESC["HTTP_422"]
assert spec["401"]["description"] == "custom code description"
assert spec["402"]["description"] == "custom code description"
assert spec["201"]["content"]["application/json"]["schema"]["$ref"].split("/")[
-1
] == get_model_path_key("tests.common.DemoModelWithSchemaExtra")
assert spec["201"]["content"]["application/json"]["examples"] == {
"example1": {"value": {"key1": "value1", "key2": "value2"}}
}
assert spec["422"]["content"]["application/json"]["schema"]["$ref"].split("/")[
-1
] == get_model_path_key("spectree.models.ValidationError")

assert spec.get(200) is None
assert spec.get(404) is None


def test_list_model():
resp = Response(HTTP_200=List[JSON])
model = resp.find_model(200)
Expand Down
22 changes: 21 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
parse_resp,
)

from .common import DemoModel, DemoQuery, get_model_path_key
from .common import DemoModel, DemoModelWithSchemaExtra, DemoQuery, get_model_path_key

api = SpecTree()

Expand All @@ -31,6 +31,14 @@ def demo_func():
description"""


@api.validate(json=DemoModelWithSchemaExtra, resp=Response(HTTP_200=DemoModel))
def demo_with_schema_extra_func():
"""
summary

description"""


@api.validate(query=DemoQuery)
def demo_func_with_query():
"""
Expand Down Expand Up @@ -230,6 +238,18 @@ def test_parse_request():
assert parse_request(demo_class.demo_method) == {}


def test_parse_request_with_schema_extra():
model_path_key = get_model_path_key("tests.common.DemoModelWithSchemaExtra")

assert parse_request(
demo_with_schema_extra_func, demo_with_schema_extra_func.json_model
)["content"]["application/json"] == {
"schema": {"$ref": f"#/components/schemas/{model_path_key}"},
"examples": {"example1": {"value": {"key1": "value1", "key2": "value2"}}},
}
assert parse_request(demo_class.demo_method) == {}


def test_parse_params():
models = {
get_model_path_key("tests.common.DemoModel"): DemoModel.schema(
Expand Down