1"""marshmallow plugin for apispec. Allows passing a marshmallow
2`Schema` to `spec.components.schema <apispec.core.Components.schema>`,
3`spec.components.parameter <apispec.core.Components.parameter>`,
4`spec.components.response <apispec.core.Components.response>`
5(for response and headers schemas) and
6`spec.path <apispec.APISpec.path>` (for responses and response headers).
7
8Requires marshmallow>=3.13.0.
9
10``MarshmallowPlugin`` maps marshmallow ``Field`` classes with OpenAPI types and
11formats.
12
13It inspects field attributes to automatically document properties
14such as read/write-only, range and length constraints, etc.
15
16OpenAPI properties can also be passed as metadata to the ``Field`` instance
17if they can't be inferred from the field attributes (`description`,...), or to
18override automatic documentation (`readOnly`,...). A metadata attribute is used
19in the documentation either if it is a valid OpenAPI property, or if it starts
20with `"x-"` (vendor extension).
21
22.. warning::
23
24    ``MarshmallowPlugin`` infers the ``default`` property from the
25    ``load_default`` attribute of the ``Field`` (unless ``load_default`` is a
26    callable). Since default values are entered in deserialized form,
27    the value displayed in the doc is serialized by the ``Field`` instance.
28    This may lead to inaccurate documentation in very specific cases.
29    The default value to display in the documentation can be
30    specified explicitly by passing ``default`` as field metadata.
31
32::
33
34    from pprint import pprint
35    import datetime as dt
36
37    from apispec import APISpec
38    from apispec.ext.marshmallow import MarshmallowPlugin
39    from marshmallow import Schema, fields
40
41    spec = APISpec(
42        title="Example App",
43        version="1.0.0",
44        openapi_version="3.0.2",
45        plugins=[MarshmallowPlugin()],
46    )
47
48
49    class UserSchema(Schema):
50        id = fields.Int(dump_only=True)
51        name = fields.Str(metadata={"description": "The user's name"})
52        created = fields.DateTime(
53            dump_only=True,
54            dump_default=dt.datetime.utcnow,
55            metadata={"default": "The current datetime"}
56        )
57
58
59    spec.components.schema("User", schema=UserSchema)
60    pprint(spec.to_dict()["components"]["schemas"])
61    # {'User': {'properties': {'created': {'default': 'The current datetime',
62    #                                      'format': 'date-time',
63    #                                      'readOnly': True,
64    #                                      'type': 'string'},
65    #                          'id': {'readOnly': True,
66    #                                 'type': 'integer'},
67    #                          'name': {'description': "The user's name",
68    #                                   'type': 'string'}},
69    #           'type': 'object'}}
70
71"""
72import warnings
73
74from apispec import BasePlugin
75from .common import resolve_schema_instance, make_schema_key, resolve_schema_cls
76from .openapi import OpenAPIConverter
77from .schema_resolver import SchemaResolver
78
79
80def resolver(schema):
81    """Default schema name resolver function that strips 'Schema' from the end of the class name."""
82    schema_cls = resolve_schema_cls(schema)
83    name = schema_cls.__name__
84    if name.endswith("Schema"):
85        return name[:-6] or name
86    return name
87
88
89class MarshmallowPlugin(BasePlugin):
90    """APISpec plugin for translating marshmallow schemas to OpenAPI/JSONSchema format.
91
92    :param callable schema_name_resolver: Callable to generate the schema definition name.
93        Receives the `Schema` class and returns the name to be used in refs within
94        the generated spec. When working with circular referencing this function
95        must must not return `None` for schemas in a circular reference chain.
96
97        Example: ::
98
99            from apispec.ext.marshmallow.common import resolve_schema_cls
100
101            def schema_name_resolver(schema):
102                schema_cls = resolve_schema_cls(schema)
103                return schema_cls.__name__
104    """
105
106    Converter = OpenAPIConverter
107    Resolver = SchemaResolver
108
109    def __init__(self, schema_name_resolver=None):
110        super().__init__()
111        self.schema_name_resolver = schema_name_resolver or resolver
112        self.spec = None
113        self.openapi_version = None
114        self.converter = None
115        self.resolver = None
116
117    def init_spec(self, spec):
118        super().init_spec(spec)
119        self.spec = spec
120        self.openapi_version = spec.openapi_version
121        self.converter = self.Converter(
122            openapi_version=spec.openapi_version,
123            schema_name_resolver=self.schema_name_resolver,
124            spec=spec,
125        )
126        self.resolver = self.Resolver(
127            openapi_version=spec.openapi_version, converter=self.converter
128        )
129
130    def map_to_openapi_type(self, *args):
131        """Decorator to set mapping for custom fields.
132
133        ``*args`` can be:
134
135        - a pair of the form ``(type, format)``
136        - a core marshmallow field type (in which case we reuse that type's mapping)
137
138        Examples: ::
139
140            @ma_plugin.map_to_openapi_type('string', 'uuid')
141            class MyCustomField(Integer):
142                # ...
143
144            @ma_plugin.map_to_openapi_type(Integer)  # will map to ('integer', None)
145            class MyCustomFieldThatsKindaLikeAnInteger(Integer):
146                # ...
147        """
148        return self.converter.map_to_openapi_type(*args)
149
150    def schema_helper(self, name, _, schema=None, **kwargs):
151        """Definition helper that allows using a marshmallow
152        :class:`Schema <marshmallow.Schema>` to provide OpenAPI
153        metadata.
154
155        :param type|Schema schema: A marshmallow Schema class or instance.
156        """
157        if schema is None:
158            return None
159
160        schema_instance = resolve_schema_instance(schema)
161
162        schema_key = make_schema_key(schema_instance)
163        self.warn_if_schema_already_in_spec(schema_key)
164        self.converter.refs[schema_key] = name
165
166        json_schema = self.converter.schema2jsonschema(schema_instance)
167
168        return json_schema
169
170    def parameter_helper(self, parameter, **kwargs):
171        """Parameter component helper that allows using a marshmallow
172        :class:`Schema <marshmallow.Schema>` in parameter definition.
173
174        :param dict parameter: parameter fields. May contain a marshmallow
175            Schema class or instance.
176        """
177        self.resolver.resolve_schema(parameter)
178        return parameter
179
180    def response_helper(self, response, **kwargs):
181        """Response component helper that allows using a marshmallow
182        :class:`Schema <marshmallow.Schema>` in response definition.
183
184        :param dict parameter: response fields. May contain a marshmallow
185            Schema class or instance.
186        """
187        self.resolver.resolve_response(response)
188        return response
189
190    def header_helper(self, header, **kwargs):
191        """Header component helper that allows using a marshmallow
192        :class:`Schema <marshmallow.Schema>` in header definition.
193
194        :param dict header: header fields. May contain a marshmallow
195            Schema class or instance.
196        """
197        self.resolver.resolve_schema(header)
198        return header
199
200    def operation_helper(self, operations, **kwargs):
201        self.resolver.resolve_operations(operations)
202
203    def warn_if_schema_already_in_spec(self, schema_key):
204        """Method to warn the user if the schema has already been added to the
205        spec.
206        """
207        if schema_key in self.converter.refs:
208            warnings.warn(
209                "{} has already been added to the spec. Adding it twice may "
210                "cause references to not resolve properly.".format(schema_key[0]),
211                UserWarning,
212            )
213