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

Rework components interface #381

Merged
merged 10 commits into from Feb 5, 2019
47 changes: 18 additions & 29 deletions apispec/core.py
Expand Up @@ -108,15 +108,7 @@ def to_dict(self):
security_key: self._security_schemes,
}

def schema(
self,
name,
properties=None,
enum=None,
description=None,
extra_fields=None,
**kwargs
):
def schema(self, name, definition=None, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Rather than use an OAS 2 term, "definition", what if we use a consistent parameter name for all the component methods? "component", perhaps? "obj" could also work, since OAS calls these "Objects", but that could be conflated with Python's "object".

"""Add a new definition to the spec.

.. note::
Expand All @@ -138,70 +130,67 @@ def schema(
raise DuplicateComponentNameError(
'Another schema with name "{}" is already registered.'.format(name)
)
ret = {}
definition = definition or {}
ret = definition.copy()
# Execute all helpers from plugins
for plugin in self._plugins:
try:
ret.update(plugin.schema_helper(name, definition=ret, **kwargs) or {})
ret.update(plugin.schema_helper(name, definition, **kwargs) or {})
except PluginMethodNotImplementedError:
continue
if properties:
ret["properties"] = properties
if enum:
ret["enum"] = enum
if description:
ret["description"] = description
if extra_fields:
Copy link
Member

@sloria sloria Feb 4, 2019

Choose a reason for hiding this comment

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

Ah, nice. This cleans things up quite a bit.

special_topics.rst will need to be updated--we can probably just get rid of the "Adding Additional Fields To Schema Objects" section altogether.

Update: ended up modifying the section to show how to add additional properties to auto-generated properties.

ret.update(extra_fields)
self._schemas[name] = ret
return self

def parameter(self, param_id, location, **kwargs):
def parameter(self, param_id, location, param=None, **kwargs):
""" Add a parameter which can be referenced.

:param str param_id: identifier by which parameter may be referenced.
:param str location: location of the parameter.
:param dict kwargs: parameter fields.
:param dict param: parameter fields.
:param dict kwargs: plugin-specific arguments
"""
if param_id in self._parameters:
raise DuplicateComponentNameError(
'Another parameter with name "{}" is already registered.'.format(
param_id
)
)
ret = kwargs.copy()
param = param or {}
ret = param.copy()
ret.setdefault("name", param_id)
ret["in"] = location
# Execute all helpers from plugins
for plugin in self._plugins:
try:
ret.update(plugin.parameter_helper(**kwargs) or {})
ret.update(plugin.parameter_helper(param, **kwargs) or {})
except PluginMethodNotImplementedError:
continue
self._parameters[param_id] = ret
return self

def response(self, ref_id, **kwargs):
def response(self, ref_id, resp=None, **kwargs):
"""Add a response which can be referenced.

:param str ref_id: ref_id to use as reference
:param dict kwargs: response fields
:param dict resp: response fields
:param dict kwargs: plugin-specific arguments
"""
if ref_id in self._responses:
raise DuplicateComponentNameError(
'Another response with name "{}" is already registered.'.format(ref_id)
)
ret = kwargs.copy()
resp = resp or {}
ret = resp.copy()
# Execute all helpers from plugins
for plugin in self._plugins:
try:
ret.update(plugin.response_helper(**kwargs) or {})
ret.update(plugin.response_helper(resp, **kwargs) or {})
except PluginMethodNotImplementedError:
continue
self._responses[ref_id] = ret
return self

def security_scheme(self, sec_id, **kwargs):
def security_scheme(self, sec_id, sec_scheme):
"""Add a security scheme which can be referenced.

:param str sec_id: sec_id to use as reference
Expand All @@ -213,7 +202,7 @@ def security_scheme(self, sec_id, **kwargs):
sec_id
)
)
self._security_schemes[sec_id] = kwargs
self._security_schemes[sec_id] = sec_scheme
return self


Expand Down
20 changes: 11 additions & 9 deletions apispec/ext/marshmallow/__init__.py
Expand Up @@ -139,7 +139,7 @@ class MyCustomFieldThatsKindaLikeAnInteger(Integer):
"""
return self.openapi.map_to_openapi_type(*args)

def schema_helper(self, name, schema=None, **kwargs):
def schema_helper(self, name, _, schema=None, **kwargs):
"""Definition helper that allows using a marshmallow
:class:`Schema <marshmallow.Schema>` to provide OpenAPI
metadata.
Expand All @@ -159,24 +159,26 @@ def schema_helper(self, name, schema=None, **kwargs):

return json_schema

def parameter_helper(self, **kwargs):
def parameter_helper(self, parameter, **kwargs):
"""Parameter component helper that allows using a marshmallow
:class:`Schema <marshmallow.Schema>` in parameter definition.

:param type|Schema schema: A marshmallow Schema class or instance.
:param dict parameter: parameter fields. May contain a marshmallow
Schema class or instance.
"""
# In OpenAPIv3, this only works when using the complex form using "content"
self.resolve_schema(kwargs)
return kwargs
self.resolve_schema(parameter)
return parameter

def response_helper(self, **kwargs):
def response_helper(self, response, **kwargs):
"""Response component helper that allows using a marshmallow
:class:`Schema <marshmallow.Schema>` in response definition.

:param type|Schema schema: A marshmallow Schema class or instance.
:param dict parameter: response fields. May contain a marshmallow
Schema class or instance.
"""
self.resolve_schema(kwargs)
return kwargs
self.resolve_schema(response)
return response

def operation_helper(self, operations, **kwargs):
for operation in operations.values():
Expand Down
4 changes: 2 additions & 2 deletions apispec/plugin.py
Expand Up @@ -18,11 +18,11 @@ def schema_helper(self, name, definition, **kwargs):
"""May return definition as a dict."""
raise PluginMethodNotImplementedError

def parameter_helper(self, **kwargs):
def parameter_helper(self, parameter, **kwargs):
"""May return parameter component description as a dict."""
raise PluginMethodNotImplementedError

def response_helper(self, **kwargs):
def response_helper(self, response, **kwargs):
"""May return response component description as a dict."""
raise PluginMethodNotImplementedError

Expand Down
43 changes: 21 additions & 22 deletions tests/test_core.py
Expand Up @@ -100,7 +100,7 @@ def test_openapi_metadata_merge_v3(self, spec):
}
}
spec.components.schema(
"definition", properties=properties, description="definiton description"
"definition", {'properties': properties, 'description': "definiton description"}
)
metadata = spec.to_dict()
assert metadata["components"]["schemas"].get("ErrorResponse", False)
Expand Down Expand Up @@ -132,41 +132,40 @@ class TestDefinitions:
}

def test_definition(self, spec):
spec.components.schema("Pet", properties=self.properties)
spec.components.schema("Pet", {"properties": self.properties})
defs = get_definitions(spec)
assert "Pet" in defs
assert defs["Pet"]["properties"] == self.properties

def test_definition_is_chainable(self, spec):
spec.components.schema("Pet", properties={}).schema("Plant", properties={})
spec.components.schema("Pet", {"properties": {}}).schema("Plant", {"properties": {}})
defs = get_definitions(spec)
assert "Pet" in defs
assert "Plant" in defs

def test_definition_description(self, spec):
model_description = "An animal which lives with humans."
spec.components.schema(
"Pet", properties=self.properties, description=model_description
"Pet", {"properties": self.properties, "description": model_description}
)
defs = get_definitions(spec)
assert defs["Pet"]["description"] == model_description

def test_definition_stores_enum(self, spec):
enum = ["name", "photoUrls"]
spec.components.schema("Pet", properties=self.properties, enum=enum)
spec.components.schema("Pet", {"properties": self.properties, "enum": enum})
defs = get_definitions(spec)
assert defs["Pet"]["enum"] == enum

def test_definition_extra_fields(self, spec):
extra_fields = {"discriminator": "name"}
spec.components.schema(
"Pet", properties=self.properties, extra_fields=extra_fields
"Pet", {"properties": self.properties, "discriminator": "name"}
)
defs = get_definitions(spec)
assert defs["Pet"]["discriminator"] == "name"

def test_definition_duplicate_name(self, spec):
spec.components.schema("Pet", properties=self.properties)
spec.components.schema("Pet", {"properties": self.properties})
with pytest.raises(
DuplicateComponentNameError,
match='Another schema with name "Pet" is already registered.',
Expand Down Expand Up @@ -302,7 +301,7 @@ def test_parameter(self, spec):
route_spec = self.paths["/pet/{petId}"]["get"]

spec.components.parameter(
"test_parameter", "path", **route_spec["parameters"][0]
"test_parameter", "path", route_spec["parameters"][0]
)

spec.path(
Expand Down Expand Up @@ -335,20 +334,20 @@ def test_parameter_is_chainable(self, spec):
def test_parameter_duplicate_name(self, spec):
route_spec = self.paths["/pet/{petId}"]["get"]
spec.components.parameter(
"test_parameter", "path", **route_spec["parameters"][0]
"test_parameter", "path", route_spec["parameters"][0]
)
with pytest.raises(
DuplicateComponentNameError,
match='Another parameter with name "test_parameter" is already registered.',
):
spec.components.parameter(
"test_parameter", "path", **route_spec["parameters"][0]
"test_parameter", "path", route_spec["parameters"][0]
)

def test_response(self, spec):
route_spec = self.paths["/pet/{petId}"]["get"]

spec.components.response("test_response", **route_spec["responses"]["200"])
spec.components.response("test_response", route_spec["responses"]["200"])

spec.path(
path="/pet/{petId}",
Expand Down Expand Up @@ -380,7 +379,7 @@ def test_response_is_chainable(self, spec):

def test_response_duplicate_name(self, spec):
route_spec = self.paths["/pet/{petId}"]["get"]
spec.components.response("test_response", **route_spec["responses"]["200"])
spec.components.response("test_response", route_spec["responses"]["200"])
with pytest.raises(
DuplicateComponentNameError,
match='Another response with name "test_response" is already registered.',
Expand All @@ -389,24 +388,24 @@ def test_response_duplicate_name(self, spec):

def test_security_scheme(self, spec):
sec_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
spec.components.security_scheme("ApiKeyAuth", **sec_scheme)
spec.components.security_scheme("ApiKeyAuth", sec_scheme)
assert get_security_schemes(spec)["ApiKeyAuth"] == sec_scheme

def test_security_scheme_is_chainable(self, spec):
spec.components.security_scheme("sec_1").security_scheme("sec_2")
spec.components.security_scheme("sec_1", {}).security_scheme("sec_2", {})
security_schemes = get_security_schemes(spec)
assert "sec_1" in security_schemes
assert "sec_2" in security_schemes

def test_security_scheme_duplicate_name(self, spec):
sec_scheme_1 = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
sec_scheme_2 = {"type": "apiKey", "in": "header", "name": "X-API-Key-2"}
spec.components.security_scheme("ApiKeyAuth", **sec_scheme_1)
spec.components.security_scheme("ApiKeyAuth", sec_scheme_1)
with pytest.raises(
DuplicateComponentNameError,
match='Another security scheme with name "ApiKeyAuth" is already registered.',
):
spec.components.security_scheme("ApiKeyAuth", **sec_scheme_2)
spec.components.security_scheme("ApiKeyAuth", sec_scheme_2)

def test_path_check_invalid_http_method(self, spec):
spec.path("/pet/{petId}", operations={"get": {}})
Expand All @@ -424,11 +423,11 @@ def schema_helper(self, name, definition, **kwargs):
if not return_none:
return {"properties": {"name": {"type": "string"}}}

def parameter_helper(self, **kwargs):
def parameter_helper(self, parameter, **kwargs):
if not return_none:
return {"description": "some parameter"}

def response_helper(self, **kwargs):
def response_helper(self, response, **kwargs):
if not return_none:
return {"description": "42"}

Expand All @@ -453,7 +452,7 @@ def test_plugin_schema_helper_is_used(self, openapi_version, return_none):
openapi_version=openapi_version,
plugins=(self.test_plugin_factory(return_none),),
)
spec.components.schema("Pet", {})
spec.components.schema("Pet")
definitions = get_definitions(spec)
if return_none:
assert definitions["Pet"] == {}
Expand All @@ -469,7 +468,7 @@ def test_plugin_parameter_helper_is_used(self, openapi_version, return_none):
openapi_version=openapi_version,
plugins=(self.test_plugin_factory(return_none),),
)
spec.components.parameter("Pet", "body", **{})
spec.components.parameter("Pet", "body", {})
parameters = get_parameters(spec)
if return_none:
assert parameters["Pet"] == {"in": "body", "name": "Pet"}
Expand All @@ -489,7 +488,7 @@ def test_plugin_response_helper_is_used(self, openapi_version, return_none):
openapi_version=openapi_version,
plugins=(self.test_plugin_factory(return_none),),
)
spec.components.response("Pet", **{})
spec.components.response("Pet", {})
responses = get_responses(spec)
if return_none:
assert responses["Pet"] == {}
Expand Down
14 changes: 7 additions & 7 deletions tests/test_ext_marshmallow.py
Expand Up @@ -36,7 +36,7 @@ def test_can_use_schema_as_definition(self, spec, schema):
assert props["name"]["type"] == "string"

def test_schema_helper_without_schema(self, spec):
spec.components.schema("Pet", properties={"key": {"type": "integer"}})
spec.components.schema("Pet", {"properties": {"key": {"type": "integer"}}})
definitions = get_definitions(spec)
assert definitions["Pet"]["properties"] == {"key": {"type": "integer"}}

Expand Down Expand Up @@ -167,10 +167,10 @@ class TestComponentParameterHelper:
@pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
def test_can_use_schema_in_parameter(self, spec, schema):
if spec.openapi_version.major < 3:
kwargs = {"schema": schema}
param = {"schema": schema}
else:
kwargs = {"content": {"application/json": {"schema": schema}}}
spec.components.parameter("Pet", "body", **kwargs)
param = {"content": {"application/json": {"schema": schema}}}
spec.components.parameter("Pet", "body", param)
parameter = get_parameters(spec)["Pet"]
assert parameter["in"] == "body"
if spec.openapi_version.major < 3:
Expand All @@ -190,10 +190,10 @@ class TestComponentResponseHelper:
@pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
def test_can_use_schema_in_response(self, spec, schema):
if spec.openapi_version.major < 3:
kwargs = {"schema": schema}
resp = {"schema": schema}
else:
kwargs = {"content": {"application/json": {"schema": schema}}}
spec.components.response("GetPetOk", **kwargs)
resp = {"content": {"application/json": {"schema": schema}}}
spec.components.response("GetPetOk", resp)
response = get_responses(spec)["GetPetOk"]
if spec.openapi_version.major < 3:
reference = response["schema"]
Expand Down