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