import inspect
from functools import partial
from typing import Any, Dict, Optional
from statham.schema.elements import (
Array,
Boolean,
CompositionElement,
Element,
Integer,
Not,
Nothing,
Null,
Number,
String,
)
from statham.schema.elements.meta import ObjectMeta
from statham.schema.property import _Property
from statham.serializers.orderer import get_object_classes
[docs]def serialize_json(
*elements: Element, definitions: Dict[str, Element] = None
) -> Dict[str, Any]:
"""Serialize elements to a JSON Schema dictionary.
Object classes are included in definitions. The first element is
the top-level schema.
:param elements: The :class:`~statham.schema.elements.Element` objects
to serialize.
:param definitions: A dictionary of elements which should be members
of the schema definitions keyword, and referenced everywhere else.
:return: A JSON-serializable dictionary containing the JSON Schema
for the provided element(s).
"""
primary = elements[0]
object_classes = get_object_classes(*elements)
serialize = partial(
_serialize_element, object_refs=True, definitions=definitions
)
schema: Dict[str, Any] = {
**serialize(primary),
"definitions": {
object_class.__name__: serialize(object_class)
for object_class in object_classes
if object_class is not primary
},
}
if definitions:
schema["definitions"].update(
{key: serialize(element) for key, element in definitions.items()}
)
if not schema["definitions"]:
del schema["definitions"]
return schema
def _serialize_element(
element: Element,
object_refs: bool = False,
definitions: Dict[str, Any] = None,
):
"""Convert a schema element to a JSON Schema dictionary.
This does the heavy lifting behind `serialize_json`.
"""
if isinstance(element, Nothing):
return False
schema = {
param.name: getattr(element, param.name, param.default)
for param in inspect.signature(Element.__init__).parameters.values()
if param.kind == param.KEYWORD_ONLY
and getattr(element, param.name, param.default) != param.default
}
if not schema.get("properties", True):
del schema["properties"]
if "properties" in schema:
schema["required"] = [
prop.source or name
for name, prop in schema["properties"].items()
if prop.required
]
if not schema.get("required", True):
del schema["required"]
if isinstance(element, CompositionElement):
schema[element.mode] = element.elements
if isinstance(element, Not):
schema["not"] = element.element
if type(element) in _TYPE_MAPPING: # pylint: disable=unidiomatic-typecheck
schema["type"] = _TYPE_MAPPING[type(element)]
if isinstance(element, ObjectMeta):
schema["title"] = element.__name__
return _serialize_recursive(
schema, object_refs=object_refs, definitions=definitions
)
_TYPE_MAPPING = {
Array: "array",
Boolean: "boolean",
Integer: "integer",
Null: "null",
ObjectMeta: "object",
Number: "number",
String: "string",
}
def _serialize_recursive(
data: Any, object_refs: bool = False, definitions: Dict[str, Element] = None
) -> Any:
"""Recursively serialize schema elements."""
recur = partial(
_serialize_recursive, object_refs=object_refs, definitions=definitions
)
if isinstance(data, _Property):
data = data.element
if isinstance(data, ObjectMeta) and object_refs:
return {"$ref": f"#/definitions/{data.__name__}"}
if isinstance(data, Element):
return _from_definitions(
definitions,
data,
_serialize_element(
data, object_refs=object_refs, definitions=definitions
),
)
if not isinstance(data, (list, dict)):
return data
if isinstance(data, list):
return [recur(item) for item in data]
return {key: recur(value) for key, value in data.items()}
def _from_definitions(
definitions: Optional[Dict[str, Element]],
element: Element,
default: Any = None,
):
"""Check if this element is present in definitions."""
if not definitions:
return default
return next(
(
{"$ref": f"#/definitions/{key}"}
for key, definition in definitions.items()
if definition == element
),
default,
)