1"""Utilities for generating OpenAPI Specification (fka Swagger) entities from
2:class:`Fields <marshmallow.fields.Field>`.
3
4.. warning::
5
6    This module is treated as private API.
7    Users should not need to use this module directly.
8"""
9import re
10import functools
11import operator
12import warnings
13
14import marshmallow
15from marshmallow.orderedset import OrderedSet
16
17
18RegexType = type(re.compile(""))
19
20# marshmallow field => (JSON Schema type, format)
21DEFAULT_FIELD_MAPPING = {
22    marshmallow.fields.Integer: ("integer", None),
23    marshmallow.fields.Number: ("number", None),
24    marshmallow.fields.Float: ("number", None),
25    marshmallow.fields.Decimal: ("number", None),
26    marshmallow.fields.String: ("string", None),
27    marshmallow.fields.Boolean: ("boolean", None),
28    marshmallow.fields.UUID: ("string", "uuid"),
29    marshmallow.fields.DateTime: ("string", "date-time"),
30    marshmallow.fields.Date: ("string", "date"),
31    marshmallow.fields.Time: ("string", None),
32    marshmallow.fields.TimeDelta: ("integer", None),
33    marshmallow.fields.Email: ("string", "email"),
34    marshmallow.fields.URL: ("string", "url"),
35    marshmallow.fields.Dict: ("object", None),
36    marshmallow.fields.Field: (None, None),
37    marshmallow.fields.Raw: (None, None),
38    marshmallow.fields.List: ("array", None),
39}
40
41
42# Properties that may be defined in a field's metadata that will be added to the output
43# of field2property
44# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
45_VALID_PROPERTIES = {
46    "format",
47    "title",
48    "description",
49    "default",
50    "multipleOf",
51    "maximum",
52    "exclusiveMaximum",
53    "minimum",
54    "exclusiveMinimum",
55    "maxLength",
56    "minLength",
57    "pattern",
58    "maxItems",
59    "minItems",
60    "uniqueItems",
61    "maxProperties",
62    "minProperties",
63    "required",
64    "enum",
65    "type",
66    "items",
67    "allOf",
68    "oneOf",
69    "anyOf",
70    "not",
71    "properties",
72    "additionalProperties",
73    "readOnly",
74    "writeOnly",
75    "xml",
76    "externalDocs",
77    "example",
78    "nullable",
79    "deprecated",
80}
81
82
83_VALID_PREFIX = "x-"
84
85
86class FieldConverterMixin:
87    """Adds methods for converting marshmallow fields to an OpenAPI properties."""
88
89    field_mapping = DEFAULT_FIELD_MAPPING
90
91    def init_attribute_functions(self):
92        self.attribute_functions = [
93            # self.field2type_and_format should run first
94            # as other functions may rely on its output
95            self.field2type_and_format,
96            self.field2default,
97            self.field2choices,
98            self.field2read_only,
99            self.field2write_only,
100            self.field2nullable,
101            self.field2range,
102            self.field2length,
103            self.field2pattern,
104            self.metadata2properties,
105            self.nested2properties,
106            self.pluck2properties,
107            self.list2properties,
108            self.dict2properties,
109            self.timedelta2properties,
110        ]
111
112    def map_to_openapi_type(self, *args):
113        """Decorator to set mapping for custom fields.
114
115        ``*args`` can be:
116
117        - a pair of the form ``(type, format)``
118        - a core marshmallow field type (in which case we reuse that type's mapping)
119        """
120        if len(args) == 1 and args[0] in self.field_mapping:
121            openapi_type_field = self.field_mapping[args[0]]
122        elif len(args) == 2:
123            openapi_type_field = args
124        else:
125            raise TypeError("Pass core marshmallow field type or (type, fmt) pair.")
126
127        def inner(field_type):
128            self.field_mapping[field_type] = openapi_type_field
129            return field_type
130
131        return inner
132
133    def add_attribute_function(self, func):
134        """Method to add an attribute function to the list of attribute functions
135        that will be called on a field to convert it from a field to an OpenAPI
136        property.
137
138        :param func func: the attribute function to add
139            The attribute function will be bound to the
140            `OpenAPIConverter <apispec.ext.marshmallow.openapi.OpenAPIConverter>`
141            instance.
142            It will be called for each field in a schema with
143            `self <apispec.ext.marshmallow.openapi.OpenAPIConverter>` and a
144            `field <marshmallow.fields.Field>` instance
145            positional arguments and `ret <dict>` keyword argument.
146            Must return a dictionary of OpenAPI properties that will be shallow
147            merged with the return values of all other attribute functions called on the field.
148            User added attribute functions will be called after all built-in attribute
149            functions in the order they were added. The merged results of all
150            previously called attribute functions are accessable via the `ret`
151            argument.
152        """
153        bound_func = func.__get__(self)
154        setattr(self, func.__name__, bound_func)
155        self.attribute_functions.append(bound_func)
156
157    def field2property(self, field):
158        """Return the JSON Schema property definition given a marshmallow
159        :class:`Field <marshmallow.fields.Field>`.
160
161        Will include field metadata that are valid properties of OpenAPI schema objects
162        (e.g. "description", "enum", "example").
163
164        https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
165
166        :param Field field: A marshmallow field.
167        :rtype: dict, a Property Object
168        """
169        ret = {}
170
171        for attr_func in self.attribute_functions:
172            ret.update(attr_func(field, ret=ret))
173
174        return ret
175
176    def field2type_and_format(self, field, **kwargs):
177        """Return the dictionary of OpenAPI type and format based on the field type.
178
179        :param Field field: A marshmallow field.
180        :rtype: dict
181        """
182        # If this type isn't directly in the field mapping then check the
183        # hierarchy until we find something that does.
184        for field_class in type(field).__mro__:
185            if field_class in self.field_mapping:
186                type_, fmt = self.field_mapping[field_class]
187                break
188        else:
189            warnings.warn(
190                "Field of type {} does not inherit from marshmallow.Field.".format(
191                    type(field)
192                ),
193                UserWarning,
194            )
195            type_, fmt = "string", None
196
197        ret = {}
198        if type_:
199            ret["type"] = type_
200        if fmt:
201            ret["format"] = fmt
202
203        return ret
204
205    def field2default(self, field, **kwargs):
206        """Return the dictionary containing the field's default value.
207
208        Will first look for a `default` key in the field's metadata and then
209        fall back on the field's `missing` parameter. A callable passed to the
210        field's missing parameter will be ignored.
211
212        :param Field field: A marshmallow field.
213        :rtype: dict
214        """
215        ret = {}
216        if "default" in field.metadata:
217            ret["default"] = field.metadata["default"]
218        else:
219            default = field.load_default
220            if default is not marshmallow.missing and not callable(default):
221                default = field._serialize(default, None, None)
222                ret["default"] = default
223        return ret
224
225    def field2choices(self, field, **kwargs):
226        """Return the dictionary of OpenAPI field attributes for valid choices definition.
227
228        :param Field field: A marshmallow field.
229        :rtype: dict
230        """
231        attributes = {}
232
233        comparable = [
234            validator.comparable
235            for validator in field.validators
236            if hasattr(validator, "comparable")
237        ]
238        if comparable:
239            attributes["enum"] = comparable
240        else:
241            choices = [
242                OrderedSet(validator.choices)
243                for validator in field.validators
244                if hasattr(validator, "choices")
245            ]
246            if choices:
247                attributes["enum"] = list(functools.reduce(operator.and_, choices))
248
249        return attributes
250
251    def field2read_only(self, field, **kwargs):
252        """Return the dictionary of OpenAPI field attributes for a dump_only field.
253
254        :param Field field: A marshmallow field.
255        :rtype: dict
256        """
257        attributes = {}
258        if field.dump_only:
259            attributes["readOnly"] = True
260        return attributes
261
262    def field2write_only(self, field, **kwargs):
263        """Return the dictionary of OpenAPI field attributes for a load_only field.
264
265        :param Field field: A marshmallow field.
266        :rtype: dict
267        """
268        attributes = {}
269        if field.load_only and self.openapi_version.major >= 3:
270            attributes["writeOnly"] = True
271        return attributes
272
273    def field2nullable(self, field, ret):
274        """Return the dictionary of OpenAPI field attributes for a nullable field.
275
276        :param Field field: A marshmallow field.
277        :rtype: dict
278        """
279        attributes = {}
280        if field.allow_none:
281            if self.openapi_version.major < 3:
282                attributes["x-nullable"] = True
283            elif self.openapi_version.minor < 1:
284                attributes["nullable"] = True
285            else:
286                attributes["type"] = [*make_type_list(ret.get("type")), "null"]
287        return attributes
288
289    def field2range(self, field, ret):
290        """Return the dictionary of OpenAPI field attributes for a set of
291        :class:`Range <marshmallow.validators.Range>` validators.
292
293        :param Field field: A marshmallow field.
294        :rtype: dict
295        """
296        validators = [
297            validator
298            for validator in field.validators
299            if (
300                hasattr(validator, "min")
301                and hasattr(validator, "max")
302                and not hasattr(validator, "equal")
303            )
304        ]
305
306        min_attr, max_attr = (
307            ("minimum", "maximum")
308            if set(make_type_list(ret.get("type"))) & {"number", "integer"}
309            else ("x-minimum", "x-maximum")
310        )
311        return make_min_max_attributes(validators, min_attr, max_attr)
312
313    def field2length(self, field, **kwargs):
314        """Return the dictionary of OpenAPI field attributes for a set of
315        :class:`Length <marshmallow.validators.Length>` validators.
316
317        :param Field field: A marshmallow field.
318        :rtype: dict
319        """
320        validators = [
321            validator
322            for validator in field.validators
323            if (
324                hasattr(validator, "min")
325                and hasattr(validator, "max")
326                and hasattr(validator, "equal")
327            )
328        ]
329
330        is_array = isinstance(
331            field, (marshmallow.fields.Nested, marshmallow.fields.List)
332        )
333        min_attr = "minItems" if is_array else "minLength"
334        max_attr = "maxItems" if is_array else "maxLength"
335
336        equal_list = [
337            validator.equal for validator in validators if validator.equal is not None
338        ]
339        if equal_list:
340            return {min_attr: equal_list[0], max_attr: equal_list[0]}
341
342        return make_min_max_attributes(validators, min_attr, max_attr)
343
344    def field2pattern(self, field, **kwargs):
345        """Return the dictionary of OpenAPI field attributes for a
346        :class:`Regexp <marshmallow.validators.Regexp>` validator.
347
348        If there is more than one such validator, only the first
349        is used in the output spec.
350
351        :param Field field: A marshmallow field.
352        :rtype: dict
353        """
354        regex_validators = (
355            v
356            for v in field.validators
357            if isinstance(getattr(v, "regex", None), RegexType)
358        )
359        v = next(regex_validators, None)
360        attributes = {} if v is None else {"pattern": v.regex.pattern}
361
362        if next(regex_validators, None) is not None:
363            warnings.warn(
364                "More than one regex validator defined on {} field. Only the "
365                "first one will be used in the output spec.".format(type(field)),
366                UserWarning,
367            )
368
369        return attributes
370
371    def metadata2properties(self, field, **kwargs):
372        """Return a dictionary of properties extracted from field metadata.
373
374        Will include field metadata that are valid properties of `OpenAPI schema
375        objects
376        <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject>`_
377        (e.g. "description", "enum", "example").
378
379        In addition, `specification extensions
380        <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions>`_
381        are supported.  Prefix `x_` to the desired extension when passing the
382        keyword argument to the field constructor. apispec will convert `x_` to
383        `x-` to comply with OpenAPI.
384
385        :param Field field: A marshmallow field.
386        :rtype: dict
387        """
388        # Dasherize metadata that starts with x_
389        metadata = {
390            key.replace("_", "-") if key.startswith("x_") else key: value
391            for key, value in field.metadata.items()
392            if isinstance(key, str)
393        }
394
395        # Avoid validation error with "Additional properties not allowed"
396        ret = {
397            key: value
398            for key, value in metadata.items()
399            if key in _VALID_PROPERTIES or key.startswith(_VALID_PREFIX)
400        }
401        return ret
402
403    def nested2properties(self, field, ret):
404        """Return a dictionary of properties from :class:`Nested <marshmallow.fields.Nested` fields.
405
406        Typically provides a reference object and will add the schema to the spec
407        if it is not already present
408        If a custom `schema_name_resolver` function returns `None` for the nested
409        schema a JSON schema object will be returned
410
411        :param Field field: A marshmallow field.
412        :rtype: dict
413        """
414        # Pluck is a subclass of Nested but is in essence a single field; it
415        # is treated separately by pluck2properties.
416        if isinstance(field, marshmallow.fields.Nested) and not isinstance(
417            field, marshmallow.fields.Pluck
418        ):
419            schema_dict = self.resolve_nested_schema(field.schema)
420            if ret and "$ref" in schema_dict:
421                ret.update({"allOf": [schema_dict]})
422            else:
423                ret.update(schema_dict)
424        return ret
425
426    def pluck2properties(self, field, **kwargs):
427        """Return a dictionary of properties from :class:`Pluck <marshmallow.fields.Pluck` fields.
428
429        Pluck effectively trans-includes a field from another schema into this,
430        possibly wrapped in an array (`many=True`).
431
432        :param Field field: A marshmallow field.
433        :rtype: dict
434        """
435        if isinstance(field, marshmallow.fields.Pluck):
436            plucked_field = field.schema.fields[field.field_name]
437            ret = self.field2property(plucked_field)
438            return {"type": "array", "items": ret} if field.many else ret
439        return {}
440
441    def list2properties(self, field, **kwargs):
442        """Return a dictionary of properties from :class:`List <marshmallow.fields.List>` fields.
443
444        Will provide an `items` property based on the field's `inner` attribute
445
446        :param Field field: A marshmallow field.
447        :rtype: dict
448        """
449        ret = {}
450        if isinstance(field, marshmallow.fields.List):
451            ret["items"] = self.field2property(field.inner)
452        return ret
453
454    def dict2properties(self, field, **kwargs):
455        """Return a dictionary of properties from :class:`Dict <marshmallow.fields.Dict>` fields.
456
457        Only applicable for Marshmallow versions greater than 3. Will provide an
458        `additionalProperties` property based on the field's `value_field` attribute
459
460        :param Field field: A marshmallow field.
461        :rtype: dict
462        """
463        ret = {}
464        if isinstance(field, marshmallow.fields.Dict):
465            value_field = field.value_field
466            if value_field:
467                ret["additionalProperties"] = self.field2property(value_field)
468        return ret
469
470    def timedelta2properties(self, field, **kwargs):
471        """Return a dictionary of properties from :class:`TimeDelta <marshmallow.fields.TimeDelta>` fields.
472
473        Adds a `x-unit` vendor property based on the field's `precision` attribute
474
475        :param Field field: A marshmallow field.
476        :rtype: dict
477        """
478        ret = {}
479        if isinstance(field, marshmallow.fields.TimeDelta):
480            ret["x-unit"] = field.precision
481        return ret
482
483
484def make_type_list(types):
485    """Return a list of types from a type attribute
486
487    Since OpenAPI 3.1.0, "type" can be a single type as string or a list of
488    types, including 'null'. This function takes a "type" attribute as input
489    and returns it as a list, be it an empty or single-element list.
490    This is useful to factorize type-conditional code or code adding a type.
491    """
492    if types is None:
493        return []
494    if isinstance(types, str):
495        return [types]
496    return types
497
498
499def make_min_max_attributes(validators, min_attr, max_attr):
500    """Return a dictionary of minimum and maximum attributes based on a list
501    of validators. If either minimum or maximum values are not present in any
502    of the validator objects that attribute will be omitted.
503
504    :param validators list: A list of `Marshmallow` validator objects. Each
505        objct is inspected for a minimum and maximum values
506    :param min_attr string: The OpenAPI attribute for the minimum value
507    :param max_attr string: The OpenAPI attribute for the maximum value
508    """
509    attributes = {}
510    min_list = [validator.min for validator in validators if validator.min is not None]
511    max_list = [validator.max for validator in validators if validator.max is not None]
512    if min_list:
513        attributes[min_attr] = max(min_list)
514    if max_list:
515        attributes[max_attr] = min(max_list)
516    return attributes
517