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