some random stuff. caelestia incoming
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from .builder import JSONSchemaBuilder, build_json_schema
|
||||
from .dialects import DRAFT_2020_12, OPEN_API_3_1
|
||||
|
||||
__all__ = [
|
||||
"JSONSchemaBuilder",
|
||||
"build_json_schema",
|
||||
"DRAFT_2020_12",
|
||||
"OPEN_API_3_1",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,134 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mashumaro.jsonschema.models import JSONSchema, Number
|
||||
|
||||
|
||||
class Annotation:
|
||||
pass
|
||||
|
||||
|
||||
class Constraint(Annotation):
|
||||
pass
|
||||
|
||||
|
||||
class NumberConstraint(Constraint):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Minimum(NumberConstraint):
|
||||
value: Number
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Maximum(NumberConstraint):
|
||||
value: Number
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class ExclusiveMinimum(NumberConstraint):
|
||||
value: Number
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class ExclusiveMaximum(NumberConstraint):
|
||||
value: Number
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MultipleOf(NumberConstraint):
|
||||
value: Number
|
||||
|
||||
|
||||
class StringConstraint(Constraint):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MinLength(StringConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MaxLength(StringConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Pattern(StringConstraint):
|
||||
value: str
|
||||
|
||||
|
||||
class ArrayConstraint(Constraint):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MinItems(ArrayConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MaxItems(ArrayConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class UniqueItems(ArrayConstraint):
|
||||
value: bool
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Contains(ArrayConstraint):
|
||||
value: JSONSchema
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MinContains(ArrayConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MaxContains(ArrayConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
class ObjectConstraint(Constraint):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MaxProperties(ObjectConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MinProperties(ObjectConstraint):
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class DependentRequired(ObjectConstraint):
|
||||
value: dict[str, set[str]]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Annotation",
|
||||
"MultipleOf",
|
||||
"Maximum",
|
||||
"ExclusiveMaximum",
|
||||
"Minimum",
|
||||
"ExclusiveMinimum",
|
||||
"MaxLength",
|
||||
"MinLength",
|
||||
"Pattern",
|
||||
"MaxItems",
|
||||
"MinItems",
|
||||
"UniqueItems",
|
||||
"Contains",
|
||||
"MaxContains",
|
||||
"MinContains",
|
||||
"MaxProperties",
|
||||
"MinProperties",
|
||||
"DependentRequired",
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from mashumaro.jsonschema.dialects import DRAFT_2020_12, JSONSchemaDialect
|
||||
from mashumaro.jsonschema.models import Context, JSONSchema
|
||||
from mashumaro.jsonschema.plugins import BasePlugin
|
||||
from mashumaro.jsonschema.schema import Instance, get_schema
|
||||
|
||||
try:
|
||||
from mashumaro.mixins.orjson import (
|
||||
DataClassORJSONMixin as DataClassJSONMixin,
|
||||
)
|
||||
except ImportError: # pragma: no cover
|
||||
from mashumaro.mixins.json import DataClassJSONMixin # type: ignore
|
||||
|
||||
|
||||
def build_json_schema(
|
||||
instance_type: Type,
|
||||
context: Optional[Context] = None,
|
||||
with_definitions: bool = True,
|
||||
all_refs: Optional[bool] = None,
|
||||
with_dialect_uri: bool = False,
|
||||
dialect: Optional[JSONSchemaDialect] = None,
|
||||
ref_prefix: Optional[str] = None,
|
||||
plugins: Sequence[BasePlugin] = (),
|
||||
) -> JSONSchema:
|
||||
if context is None:
|
||||
context = Context()
|
||||
else:
|
||||
context = Context(
|
||||
dialect=context.dialect,
|
||||
definitions=context.definitions,
|
||||
all_refs=context.all_refs,
|
||||
ref_prefix=context.ref_prefix,
|
||||
plugins=context.plugins,
|
||||
)
|
||||
if dialect is not None:
|
||||
context.dialect = dialect
|
||||
if all_refs is not None:
|
||||
context.all_refs = all_refs
|
||||
elif context.all_refs is None:
|
||||
context.all_refs = context.dialect.all_refs
|
||||
if ref_prefix is not None:
|
||||
context.ref_prefix = ref_prefix.rstrip("/")
|
||||
elif context.ref_prefix is None:
|
||||
context.ref_prefix = context.dialect.definitions_root_pointer
|
||||
if plugins:
|
||||
context.plugins = plugins
|
||||
instance = Instance(instance_type)
|
||||
schema = get_schema(instance, context, with_dialect_uri=with_dialect_uri)
|
||||
if with_definitions and context.definitions:
|
||||
schema.definitions = context.definitions
|
||||
return schema
|
||||
|
||||
|
||||
@dataclass
|
||||
class JSONSchemaDefinitions(DataClassJSONMixin):
|
||||
definitions: dict[str, JSONSchema]
|
||||
|
||||
def __post_serialize__( # type: ignore
|
||||
self, d: dict[Any, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
return d["definitions"]
|
||||
|
||||
|
||||
class JSONSchemaBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
dialect: JSONSchemaDialect = DRAFT_2020_12,
|
||||
all_refs: Optional[bool] = None,
|
||||
ref_prefix: Optional[str] = None,
|
||||
plugins: Sequence[BasePlugin] = (),
|
||||
):
|
||||
if all_refs is None:
|
||||
all_refs = dialect.all_refs
|
||||
if ref_prefix is None:
|
||||
ref_prefix = dialect.definitions_root_pointer
|
||||
self.context = Context(
|
||||
dialect=dialect,
|
||||
all_refs=all_refs,
|
||||
ref_prefix=ref_prefix.rstrip("/"),
|
||||
plugins=plugins,
|
||||
)
|
||||
|
||||
def build(self, instance_type: Type) -> JSONSchema:
|
||||
return build_json_schema(
|
||||
instance_type=instance_type,
|
||||
context=self.context,
|
||||
with_definitions=False,
|
||||
)
|
||||
|
||||
def get_definitions(self) -> JSONSchemaDefinitions:
|
||||
return JSONSchemaDefinitions(self.context.definitions)
|
||||
|
||||
|
||||
__all__ = ["JSONSchemaBuilder", "build_json_schema"]
|
||||
@@ -0,0 +1,29 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JSONSchemaDialect:
|
||||
uri: str
|
||||
definitions_root_pointer: str
|
||||
all_refs: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JSONSchemaDraft202012Dialect(JSONSchemaDialect):
|
||||
uri: str = "https://json-schema.org/draft/2020-12/schema"
|
||||
definitions_root_pointer: str = "#/$defs"
|
||||
all_refs: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpenAPISchema31Dialect(JSONSchemaDialect):
|
||||
uri: str = "https://spec.openapis.org/oas/3.1/dialect/base"
|
||||
definitions_root_pointer: str = "#/components/schemas"
|
||||
all_refs: bool = True
|
||||
|
||||
|
||||
DRAFT_2020_12 = JSONSchemaDraft202012Dialect()
|
||||
OPEN_API_3_1 = OpenAPISchema31Dialect()
|
||||
|
||||
|
||||
__all__ = ["JSONSchemaDialect", "DRAFT_2020_12", "OPEN_API_3_1"]
|
||||
@@ -0,0 +1,212 @@
|
||||
import datetime
|
||||
import ipaddress
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import MISSING, dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from typing_extensions import TYPE_CHECKING, Self, TypeAlias
|
||||
|
||||
from mashumaro.config import BaseConfig
|
||||
from mashumaro.core.meta.helpers import iter_all_subclasses
|
||||
from mashumaro.helper import pass_through
|
||||
from mashumaro.jsonschema.dialects import DRAFT_2020_12, JSONSchemaDialect
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from mashumaro.jsonschema.plugins import BasePlugin
|
||||
else:
|
||||
BasePlugin = Any
|
||||
|
||||
try:
|
||||
from mashumaro.mixins.orjson import (
|
||||
DataClassORJSONMixin as DataClassJSONMixin,
|
||||
)
|
||||
except ImportError: # pragma: no cover
|
||||
from mashumaro.mixins.json import DataClassJSONMixin # type: ignore
|
||||
|
||||
|
||||
# https://github.com/python/mypy/issues/3186
|
||||
Number: TypeAlias = Union[int, float]
|
||||
|
||||
Null = object()
|
||||
|
||||
|
||||
class JSONSchemaInstanceType(Enum):
|
||||
NULL = "null"
|
||||
BOOLEAN = "boolean"
|
||||
OBJECT = "object"
|
||||
ARRAY = "array"
|
||||
NUMBER = "number"
|
||||
STRING = "string"
|
||||
INTEGER = "integer"
|
||||
|
||||
|
||||
class JSONSchemaInstanceFormat(Enum):
|
||||
pass
|
||||
|
||||
|
||||
class JSONSchemaStringFormat(JSONSchemaInstanceFormat):
|
||||
DATETIME = "date-time"
|
||||
DATE = "date"
|
||||
TIME = "time"
|
||||
DURATION = "duration"
|
||||
EMAIL = "email"
|
||||
IDN_EMAIL = "idn-email"
|
||||
HOSTNAME = "hostname"
|
||||
IDN_HOSTNAME = "idn-hostname"
|
||||
IPV4ADDRESS = "ipv4"
|
||||
IPV6ADDRESS = "ipv6"
|
||||
URI = "uri"
|
||||
URI_REFERENCE = "uri-reference"
|
||||
IRI = "iri"
|
||||
IRI_REFERENCE = "iri-reference"
|
||||
UUID = "uuid"
|
||||
URI_TEMPLATE = "uri-template"
|
||||
JSON_POINTER = "json-pointer"
|
||||
RELATIVE_JSON_POINTER = "relative-json-pointer"
|
||||
REGEX = "regex"
|
||||
|
||||
|
||||
class JSONSchemaInstanceFormatExtension(JSONSchemaInstanceFormat):
|
||||
TIMEDELTA = "time-delta"
|
||||
TIME_ZONE = "time-zone"
|
||||
IPV4NETWORK = "ipv4network"
|
||||
IPV6NETWORK = "ipv6network"
|
||||
IPV4INTERFACE = "ipv4interface"
|
||||
IPV6INTERFACE = "ipv6interface"
|
||||
DECIMAL = "decimal"
|
||||
FRACTION = "fraction"
|
||||
BASE64 = "base64"
|
||||
PATH = "path"
|
||||
|
||||
|
||||
DATETIME_FORMATS = {
|
||||
datetime.datetime: JSONSchemaStringFormat.DATETIME,
|
||||
datetime.date: JSONSchemaStringFormat.DATE,
|
||||
datetime.time: JSONSchemaStringFormat.TIME,
|
||||
}
|
||||
|
||||
|
||||
IPADDRESS_FORMATS = {
|
||||
ipaddress.IPv4Address: JSONSchemaStringFormat.IPV4ADDRESS,
|
||||
ipaddress.IPv6Address: JSONSchemaStringFormat.IPV6ADDRESS,
|
||||
ipaddress.IPv4Network: JSONSchemaInstanceFormatExtension.IPV4NETWORK,
|
||||
ipaddress.IPv6Network: JSONSchemaInstanceFormatExtension.IPV6NETWORK,
|
||||
ipaddress.IPv4Interface: JSONSchemaInstanceFormatExtension.IPV4INTERFACE,
|
||||
ipaddress.IPv6Interface: JSONSchemaInstanceFormatExtension.IPV6INTERFACE,
|
||||
}
|
||||
|
||||
|
||||
def _deserialize_json_schema_instance_format(
|
||||
value: Any,
|
||||
) -> JSONSchemaInstanceFormat:
|
||||
for cls in iter_all_subclasses(JSONSchemaInstanceFormat):
|
||||
try:
|
||||
return cls(value)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
raise ValueError(value)
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class JSONSchema(DataClassJSONMixin):
|
||||
# Common keywords
|
||||
schema: Optional[str] = None
|
||||
type: Optional[JSONSchemaInstanceType] = None
|
||||
enum: Optional[list[Any]] = None
|
||||
const: Optional[Any] = field(default_factory=lambda: MISSING)
|
||||
format: Optional[JSONSchemaInstanceFormat] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
anyOf: Optional[List["JSONSchema"]] = None
|
||||
reference: Optional[str] = None
|
||||
definitions: Optional[Dict[str, "JSONSchema"]] = None
|
||||
default: Optional[Any] = field(default_factory=lambda: MISSING)
|
||||
deprecated: Optional[bool] = None
|
||||
examples: Optional[list[Any]] = None
|
||||
# Keywords for Objects
|
||||
properties: Optional[Dict[str, "JSONSchema"]] = None
|
||||
patternProperties: Optional[Dict[str, "JSONSchema"]] = None
|
||||
additionalProperties: Union["JSONSchema", bool, None] = None
|
||||
propertyNames: Optional["JSONSchema"] = None
|
||||
# Keywords for Arrays
|
||||
prefixItems: Optional[List["JSONSchema"]] = None
|
||||
items: Optional["JSONSchema"] = None
|
||||
contains: Optional["JSONSchema"] = None
|
||||
# Validation keywords for numeric instances
|
||||
multipleOf: Optional[Number] = None
|
||||
maximum: Optional[Number] = None
|
||||
exclusiveMaximum: Optional[Number] = None
|
||||
minimum: Optional[Number] = None
|
||||
exclusiveMinimum: Optional[Number] = None
|
||||
# Validation keywords for Strings
|
||||
maxLength: Optional[int] = None
|
||||
minLength: Optional[int] = None
|
||||
pattern: Optional[str] = None
|
||||
# Validation keywords for Arrays
|
||||
maxItems: Optional[int] = None
|
||||
minItems: Optional[int] = None
|
||||
uniqueItems: Optional[bool] = None
|
||||
maxContains: Optional[int] = None
|
||||
minContains: Optional[int] = None
|
||||
# Validation keywords for Objects
|
||||
maxProperties: Optional[int] = None
|
||||
minProperties: Optional[int] = None
|
||||
required: Optional[list[str]] = None
|
||||
dependentRequired: Optional[dict[str, set[str]]] = None
|
||||
|
||||
class Config(BaseConfig):
|
||||
omit_none = True
|
||||
serialize_by_alias = True
|
||||
aliases = {
|
||||
"schema": "$schema",
|
||||
"reference": "$ref",
|
||||
"definitions": "$defs",
|
||||
}
|
||||
serialization_strategy = {
|
||||
int: pass_through,
|
||||
float: pass_through,
|
||||
Null: pass_through,
|
||||
JSONSchemaInstanceFormat: {
|
||||
"deserialize": _deserialize_json_schema_instance_format,
|
||||
},
|
||||
}
|
||||
|
||||
def __pre_serialize__(self) -> Self:
|
||||
if self.const is None:
|
||||
self.const = Null
|
||||
if self.default is None:
|
||||
self.default = Null
|
||||
return self
|
||||
|
||||
def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]:
|
||||
const = d.get("const")
|
||||
if const is MISSING:
|
||||
d.pop("const")
|
||||
elif const is Null:
|
||||
d["const"] = None
|
||||
default = d.get("default")
|
||||
if default is MISSING:
|
||||
d.pop("default")
|
||||
elif default is Null:
|
||||
d["default"] = None
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class JSONObjectSchema(JSONSchema):
|
||||
type: Optional[JSONSchemaInstanceType] = JSONSchemaInstanceType.OBJECT
|
||||
|
||||
|
||||
@dataclass
|
||||
class JSONArraySchema(JSONSchema):
|
||||
type: Optional[JSONSchemaInstanceType] = JSONSchemaInstanceType.ARRAY
|
||||
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
dialect: JSONSchemaDialect = DRAFT_2020_12
|
||||
definitions: dict[str, JSONSchema] = field(default_factory=dict)
|
||||
all_refs: Optional[bool] = None
|
||||
ref_prefix: Optional[str] = None
|
||||
plugins: Sequence[BasePlugin] = ()
|
||||
@@ -0,0 +1,28 @@
|
||||
from dataclasses import is_dataclass
|
||||
from inspect import cleandoc
|
||||
from typing import Optional
|
||||
|
||||
from mashumaro.jsonschema.models import Context, JSONSchema
|
||||
from mashumaro.jsonschema.schema import Instance
|
||||
|
||||
|
||||
class BasePlugin:
|
||||
def get_schema(
|
||||
self,
|
||||
instance: Instance,
|
||||
ctx: Context,
|
||||
schema: Optional[JSONSchema] = None,
|
||||
) -> Optional[JSONSchema]:
|
||||
pass
|
||||
|
||||
|
||||
class DocstringDescriptionPlugin(BasePlugin):
|
||||
def get_schema(
|
||||
self,
|
||||
instance: Instance,
|
||||
ctx: Context,
|
||||
schema: Optional[JSONSchema] = None,
|
||||
) -> Optional[JSONSchema]:
|
||||
if schema and is_dataclass(instance.type) and instance.type.__doc__:
|
||||
schema.description = cleandoc(instance.type.__doc__)
|
||||
return None
|
||||
@@ -0,0 +1,886 @@
|
||||
import datetime
|
||||
import ipaddress
|
||||
import os
|
||||
import warnings
|
||||
from base64 import encodebytes
|
||||
from collections import ChainMap, Counter, deque
|
||||
from collections.abc import (
|
||||
ByteString,
|
||||
Callable,
|
||||
Collection,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Set,
|
||||
)
|
||||
from dataclasses import MISSING, dataclass, field, is_dataclass, replace
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from fractions import Fraction
|
||||
from functools import cached_property
|
||||
from typing import Any, ForwardRef, Optional, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from mashumaro.config import BaseConfig
|
||||
from mashumaro.core.const import PY_311_MIN
|
||||
from mashumaro.core.meta.code.builder import CodeBuilder
|
||||
from mashumaro.core.meta.helpers import (
|
||||
evaluate_forward_ref,
|
||||
get_args,
|
||||
get_forward_ref_referencing_globals,
|
||||
get_function_return_annotation,
|
||||
get_literal_values,
|
||||
get_type_origin,
|
||||
is_annotated,
|
||||
is_generic,
|
||||
is_literal,
|
||||
is_named_tuple,
|
||||
is_new_type,
|
||||
is_not_required,
|
||||
is_readonly,
|
||||
is_required,
|
||||
is_special_typing_primitive,
|
||||
is_type_var,
|
||||
is_type_var_any,
|
||||
is_type_var_tuple,
|
||||
is_typed_dict,
|
||||
is_union,
|
||||
is_unpack,
|
||||
resolve_type_params,
|
||||
type_name,
|
||||
)
|
||||
from mashumaro.core.meta.types.common import NoneType
|
||||
from mashumaro.helper import pass_through
|
||||
from mashumaro.jsonschema.annotations import (
|
||||
Annotation,
|
||||
Contains,
|
||||
DependentRequired,
|
||||
ExclusiveMaximum,
|
||||
ExclusiveMinimum,
|
||||
MaxContains,
|
||||
Maximum,
|
||||
MaxItems,
|
||||
MaxLength,
|
||||
MaxProperties,
|
||||
MinContains,
|
||||
Minimum,
|
||||
MinItems,
|
||||
MinLength,
|
||||
MinProperties,
|
||||
MultipleOf,
|
||||
Pattern,
|
||||
UniqueItems,
|
||||
)
|
||||
from mashumaro.jsonschema.models import (
|
||||
DATETIME_FORMATS,
|
||||
IPADDRESS_FORMATS,
|
||||
Context,
|
||||
JSONArraySchema,
|
||||
JSONObjectSchema,
|
||||
JSONSchema,
|
||||
JSONSchemaInstanceFormatExtension,
|
||||
JSONSchemaInstanceType,
|
||||
JSONSchemaStringFormat,
|
||||
)
|
||||
from mashumaro.types import SerializationStrategy
|
||||
|
||||
try:
|
||||
from mashumaro.mixins.orjson import (
|
||||
DataClassORJSONMixin as DataClassJSONMixin,
|
||||
)
|
||||
except ImportError: # pragma: no cover
|
||||
from mashumaro.mixins.json import DataClassJSONMixin # type: ignore
|
||||
|
||||
|
||||
UTC_OFFSET_PATTERN = r"^UTC([+-][0-2][0-9]:[0-5][0-9])?$"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Instance:
|
||||
type: Type
|
||||
name: Optional[str] = None
|
||||
|
||||
__owner_builder: Optional[CodeBuilder] = None
|
||||
__self_builder: Optional[CodeBuilder] = None
|
||||
|
||||
# Original type despite custom serialization. To be revised.
|
||||
_original_type: Type = field(init=False)
|
||||
|
||||
origin_type: Type = field(init=False)
|
||||
annotations: list[Annotation] = field(init=False, default_factory=list)
|
||||
|
||||
@cached_property
|
||||
def metadata(self) -> dict[str, Any]:
|
||||
if self.name and self.__owner_builder:
|
||||
return dict(**self.__owner_builder.metadatas.get(self.name, {}))
|
||||
else:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def _self_builder(self) -> CodeBuilder:
|
||||
assert self.__self_builder
|
||||
return self.__self_builder
|
||||
|
||||
@property
|
||||
def alias(self) -> Optional[str]:
|
||||
alias = self.metadata.get("alias")
|
||||
if alias is None:
|
||||
aliases_config = self.get_owner_config().aliases
|
||||
alias = aliases_config.get(self.name) # type: ignore
|
||||
if alias is None:
|
||||
alias = self.name
|
||||
return alias
|
||||
|
||||
@property
|
||||
def owner_class(self) -> Optional[Type]:
|
||||
if self.__owner_builder:
|
||||
return self.__owner_builder.cls
|
||||
return None
|
||||
|
||||
def derive(self, **changes: Any) -> "Instance":
|
||||
new_type = changes.get("type")
|
||||
if isinstance(new_type, ForwardRef):
|
||||
changes["type"] = evaluate_forward_ref(
|
||||
new_type,
|
||||
get_forward_ref_referencing_globals(new_type, self.type),
|
||||
self.__dict__,
|
||||
)
|
||||
new_instance = replace(self, **changes)
|
||||
if is_dataclass(self.origin_type):
|
||||
new_instance.__owner_builder = self.__self_builder
|
||||
return new_instance
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._original_type = self.type
|
||||
self.update_type(self.type)
|
||||
if is_annotated(self.type):
|
||||
self.annotations = getattr(self.type, "__metadata__", [])
|
||||
self.type = get_args(self.type)[0]
|
||||
self.origin_type = get_type_origin(self.type)
|
||||
|
||||
def update_type(self, new_type: Type) -> None:
|
||||
if self.__owner_builder:
|
||||
self.type = self.__owner_builder.get_real_type(
|
||||
field_name=self.name, # type: ignore
|
||||
field_type=new_type,
|
||||
)
|
||||
self.origin_type = get_type_origin(self.type)
|
||||
if is_dataclass(self.origin_type):
|
||||
type_args = get_args(self.type)
|
||||
self.__self_builder = CodeBuilder(self.origin_type, type_args)
|
||||
self.__self_builder.reset()
|
||||
else:
|
||||
self.__self_builder = None
|
||||
|
||||
def fields(self) -> Iterable[tuple[str, Type, bool, Any]]:
|
||||
for f_name, f_type in self._self_builder.get_field_types(
|
||||
include_extras=True
|
||||
).items():
|
||||
f = self._self_builder.dataclass_fields.get(f_name)
|
||||
if not f or f and not f.init:
|
||||
continue
|
||||
f_default = f.default
|
||||
if f_default is MISSING:
|
||||
f_default = self._self_builder.namespace.get(f_name, MISSING)
|
||||
if f_default is not MISSING:
|
||||
f_default = _default(f_type, f_default, self.get_self_config())
|
||||
|
||||
has_default = (
|
||||
f.default is not MISSING or f.default_factory is not MISSING
|
||||
)
|
||||
|
||||
yield f_name, f_type, has_default, f_default
|
||||
|
||||
def get_overridden_serialization_method(
|
||||
self,
|
||||
) -> Optional[Union[Callable, str]]:
|
||||
if not self.__owner_builder:
|
||||
return None
|
||||
serialize_option = self.metadata.get("serialize")
|
||||
if serialize_option is not None:
|
||||
if callable(serialize_option):
|
||||
self.metadata.pop("serialize", None) # prevent recursion
|
||||
return serialize_option
|
||||
for strategy in self.__owner_builder.iter_serialization_strategies(
|
||||
self.metadata, self.type
|
||||
):
|
||||
if strategy is pass_through:
|
||||
return pass_through
|
||||
elif isinstance(strategy, dict):
|
||||
serialize_option = strategy.get("serialize")
|
||||
elif isinstance(strategy, SerializationStrategy):
|
||||
serialize_option = strategy.serialize
|
||||
if serialize_option is not None:
|
||||
return serialize_option
|
||||
return None
|
||||
|
||||
def get_owner_config(self) -> Type[BaseConfig]:
|
||||
if self.__owner_builder:
|
||||
return self.__owner_builder.get_config()
|
||||
else:
|
||||
return BaseConfig
|
||||
|
||||
def get_owner_dialect_or_config_option(
|
||||
self, option: str, default: Any
|
||||
) -> Any:
|
||||
if self.__owner_builder:
|
||||
return self.__owner_builder.get_dialect_or_config_option(
|
||||
option, default
|
||||
)
|
||||
else:
|
||||
return default
|
||||
|
||||
def get_self_config(self) -> Type[BaseConfig]:
|
||||
if self.__self_builder:
|
||||
return self.__self_builder.get_config()
|
||||
else:
|
||||
return BaseConfig
|
||||
|
||||
|
||||
InstanceSchemaCreator: TypeAlias = Callable[
|
||||
[Instance, Context], Optional[JSONSchema]
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstanceSchemaCreatorRegistry:
|
||||
_registry: list[InstanceSchemaCreator] = field(default_factory=list)
|
||||
|
||||
def register(self, func: InstanceSchemaCreator) -> InstanceSchemaCreator:
|
||||
self._registry.append(func)
|
||||
return func
|
||||
|
||||
def iter(self) -> Iterable[InstanceSchemaCreator]:
|
||||
yield from self._registry
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmptyJSONSchema(JSONSchema):
|
||||
pass
|
||||
|
||||
|
||||
def get_schema(
|
||||
instance: Instance, ctx: Context, with_dialect_uri: bool = False
|
||||
) -> JSONSchema:
|
||||
schema = None
|
||||
for schema_creator in Registry.iter():
|
||||
schema = schema_creator(instance, ctx)
|
||||
if schema is not None:
|
||||
if with_dialect_uri:
|
||||
schema.schema = ctx.dialect.uri
|
||||
break
|
||||
for plugin in ctx.plugins:
|
||||
try:
|
||||
new_schema = plugin.get_schema(instance, ctx, schema)
|
||||
if new_schema:
|
||||
schema = new_schema
|
||||
except NotImplementedError:
|
||||
continue
|
||||
if schema:
|
||||
return schema
|
||||
raise NotImplementedError(
|
||||
f'Type {type_name(instance.type)} of field "{instance.name}" '
|
||||
f"in {type_name(instance.owner_class)} isn't supported"
|
||||
)
|
||||
|
||||
|
||||
def _get_schema_or_none(
|
||||
instance: Instance, ctx: Context
|
||||
) -> Optional[JSONSchema]:
|
||||
schema = get_schema(instance, ctx)
|
||||
if isinstance(schema, EmptyJSONSchema):
|
||||
return None
|
||||
return schema
|
||||
|
||||
|
||||
def _default(f_type: Type, f_value: Any, config_cls: Type[BaseConfig]) -> Any:
|
||||
@dataclass
|
||||
class CC(DataClassJSONMixin):
|
||||
x: f_type = f_value # type: ignore
|
||||
|
||||
class Config(config_cls): # type: ignore
|
||||
pass
|
||||
|
||||
return CC(f_value).to_dict()["x"]
|
||||
|
||||
|
||||
Registry = InstanceSchemaCreatorRegistry()
|
||||
register = Registry.register
|
||||
|
||||
|
||||
BASIC_TYPES = {str, int, float, bool}
|
||||
|
||||
|
||||
@register
|
||||
def on_type_with_overridden_serialization(
|
||||
instance: Instance, ctx: Context
|
||||
) -> Optional[JSONSchema]:
|
||||
def override_with_any(reason: Any) -> None:
|
||||
if instance.owner_class is not None:
|
||||
name = f"{type_name(instance.owner_class)}.{instance.name}"
|
||||
else: # pragma: no cover
|
||||
# we will have an owner class, but leave this here just in case
|
||||
name = type_name(instance.type)
|
||||
warnings.warn(
|
||||
f"Type Any will be used for {name} with "
|
||||
f"overridden serialization method: {reason}"
|
||||
)
|
||||
instance.update_type(Any) # type: ignore[arg-type]
|
||||
|
||||
overridden_method = instance.get_overridden_serialization_method()
|
||||
if overridden_method is pass_through:
|
||||
return None
|
||||
elif overridden_method in BASIC_TYPES:
|
||||
instance.update_type(overridden_method) # type: ignore
|
||||
elif callable(overridden_method):
|
||||
try:
|
||||
new_type = get_function_return_annotation(overridden_method)
|
||||
if new_type is instance.type:
|
||||
return None
|
||||
else:
|
||||
instance.update_type(new_type)
|
||||
except Exception as e:
|
||||
override_with_any(e)
|
||||
return get_schema(instance, ctx)
|
||||
|
||||
|
||||
@register
|
||||
def on_dataclass(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
# TODO: Self references might not work
|
||||
if is_dataclass(instance.origin_type):
|
||||
jsonschema_config = instance.get_self_config().json_schema
|
||||
schema = JSONObjectSchema(
|
||||
title=instance.origin_type.__name__,
|
||||
additionalProperties=jsonschema_config.get(
|
||||
"additionalProperties", False
|
||||
),
|
||||
)
|
||||
properties: dict[str, JSONSchema] = {}
|
||||
required = []
|
||||
field_schema_overrides = jsonschema_config.get("properties", {})
|
||||
for f_name, f_type, has_default, f_default in instance.fields():
|
||||
override = field_schema_overrides.get(f_name)
|
||||
f_instance = instance.derive(type=f_type, name=f_name)
|
||||
if override:
|
||||
f_schema = JSONSchema.from_dict(override)
|
||||
else:
|
||||
f_schema = get_schema(f_instance, ctx)
|
||||
if f_instance.alias:
|
||||
f_name = f_instance.alias
|
||||
if f_default is not MISSING:
|
||||
f_schema.default = f_default
|
||||
description = f_instance.metadata.get("description")
|
||||
if description:
|
||||
f_schema.description = description
|
||||
|
||||
if not has_default:
|
||||
required.append(f_name)
|
||||
|
||||
properties[f_name] = f_schema
|
||||
if properties:
|
||||
schema.properties = properties
|
||||
if required:
|
||||
schema.required = required
|
||||
if ctx.all_refs:
|
||||
ctx.definitions[instance.origin_type.__name__] = schema
|
||||
ref_prefix = ctx.ref_prefix or ctx.dialect.definitions_root_pointer
|
||||
return JSONSchema(
|
||||
reference=f"{ref_prefix}/{instance.origin_type.__name__}"
|
||||
)
|
||||
else:
|
||||
return schema
|
||||
|
||||
|
||||
@register
|
||||
def on_any(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.type is Any:
|
||||
return EmptyJSONSchema()
|
||||
|
||||
|
||||
def on_literal(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
enum_values = []
|
||||
for value in get_literal_values(instance.type):
|
||||
if isinstance(value, Enum):
|
||||
enum_values.append(value.value)
|
||||
elif isinstance(value, (int, str, bool, NoneType)): # type: ignore
|
||||
enum_values.append(value)
|
||||
elif isinstance(value, bytes):
|
||||
enum_values.append(encodebytes(value).decode())
|
||||
if len(enum_values) == 1:
|
||||
return JSONSchema(const=enum_values[0])
|
||||
else:
|
||||
return JSONSchema(enum=enum_values)
|
||||
|
||||
|
||||
@register
|
||||
def on_special_typing_primitive(
|
||||
instance: Instance, ctx: Context
|
||||
) -> Optional[JSONSchema]:
|
||||
if not is_special_typing_primitive(instance.origin_type):
|
||||
return None
|
||||
|
||||
args = get_args(instance.type)
|
||||
|
||||
if is_union(instance.type):
|
||||
return JSONSchema(
|
||||
anyOf=[get_schema(instance.derive(type=arg), ctx) for arg in args]
|
||||
)
|
||||
elif is_type_var_any(instance.type):
|
||||
return EmptyJSONSchema()
|
||||
elif is_type_var(instance.type):
|
||||
constraints = getattr(instance.type, "__constraints__")
|
||||
if constraints:
|
||||
return JSONSchema(
|
||||
anyOf=[
|
||||
get_schema(instance.derive(type=arg), ctx)
|
||||
for arg in constraints
|
||||
]
|
||||
)
|
||||
else:
|
||||
bound = getattr(instance.type, "__bound__")
|
||||
return get_schema(instance.derive(type=bound), ctx)
|
||||
elif is_new_type(instance.type):
|
||||
return get_schema(
|
||||
instance.derive(type=instance.type.__supertype__), ctx
|
||||
)
|
||||
elif is_literal(instance.type):
|
||||
return on_literal(instance, ctx)
|
||||
# elif is_self(instance.type):
|
||||
# raise NotImplementedError
|
||||
elif is_required(instance.type) or is_not_required(instance.type):
|
||||
return get_schema(instance.derive(type=args[0]), ctx)
|
||||
elif is_unpack(instance.type):
|
||||
return get_schema(
|
||||
instance.derive(type=get_args(instance.type)[0]), ctx
|
||||
)
|
||||
elif is_type_var_tuple(instance.type):
|
||||
return get_schema(instance.derive(type=tuple[Any, ...]), ctx)
|
||||
elif is_readonly(instance.type):
|
||||
return get_schema(instance.derive(type=args[0]), ctx)
|
||||
elif isinstance(instance.type, ForwardRef):
|
||||
evaluated = evaluate_forward_ref(
|
||||
instance.type,
|
||||
get_forward_ref_referencing_globals(instance.type),
|
||||
None,
|
||||
)
|
||||
if evaluated is not None:
|
||||
return get_schema(instance.derive(type=evaluated), ctx)
|
||||
|
||||
|
||||
@register
|
||||
def on_number(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is int:
|
||||
schema = JSONSchema(type=JSONSchemaInstanceType.INTEGER)
|
||||
elif instance.origin_type is float:
|
||||
schema = JSONSchema(type=JSONSchemaInstanceType.NUMBER)
|
||||
else:
|
||||
return None
|
||||
for annotation in instance.annotations:
|
||||
if isinstance(annotation, Maximum):
|
||||
schema.maximum = annotation.value
|
||||
elif isinstance(annotation, Minimum):
|
||||
schema.minimum = annotation.value
|
||||
elif isinstance(annotation, ExclusiveMaximum):
|
||||
schema.exclusiveMaximum = annotation.value
|
||||
elif isinstance(annotation, ExclusiveMinimum):
|
||||
schema.exclusiveMinimum = annotation.value
|
||||
elif isinstance(annotation, MultipleOf):
|
||||
schema.multipleOf = annotation.value
|
||||
return schema
|
||||
|
||||
|
||||
@register
|
||||
def on_bool(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is bool:
|
||||
return JSONSchema(type=JSONSchemaInstanceType.BOOLEAN)
|
||||
|
||||
|
||||
@register
|
||||
def on_none(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type in (NoneType, None):
|
||||
return JSONSchema(type=JSONSchemaInstanceType.NULL)
|
||||
|
||||
|
||||
@register
|
||||
def on_date_objects(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type in (
|
||||
datetime.datetime,
|
||||
datetime.date,
|
||||
datetime.time,
|
||||
):
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=DATETIME_FORMATS[instance.origin_type],
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_timedelta(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is datetime.timedelta:
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.NUMBER,
|
||||
format=JSONSchemaInstanceFormatExtension.TIMEDELTA,
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_timezone(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is datetime.timezone:
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING, pattern=UTC_OFFSET_PATTERN
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_zone_info(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is ZoneInfo:
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=JSONSchemaInstanceFormatExtension.TIME_ZONE,
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_uuid(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is UUID:
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=JSONSchemaStringFormat.UUID,
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_ipaddress(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type in (
|
||||
ipaddress.IPv4Address,
|
||||
ipaddress.IPv6Address,
|
||||
ipaddress.IPv4Network,
|
||||
ipaddress.IPv6Network,
|
||||
ipaddress.IPv4Interface,
|
||||
ipaddress.IPv6Interface,
|
||||
):
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=IPADDRESS_FORMATS[instance.origin_type], # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_decimal(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is Decimal:
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=JSONSchemaInstanceFormatExtension.DECIMAL,
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_fraction(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if instance.origin_type is Fraction:
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=JSONSchemaInstanceFormatExtension.FRACTION,
|
||||
)
|
||||
|
||||
|
||||
def on_tuple(instance: Instance, ctx: Context) -> JSONArraySchema:
|
||||
args = get_args(instance.type)
|
||||
if not args:
|
||||
if instance.type in (Tuple, tuple):
|
||||
args = [Any, ...] # type: ignore
|
||||
else:
|
||||
return JSONArraySchema(maxItems=0)
|
||||
elif len(args) == 1 and args[0] == ():
|
||||
if not PY_311_MIN:
|
||||
return JSONArraySchema(maxItems=0)
|
||||
if len(args) == 2 and args[1] is Ellipsis:
|
||||
items_schema = _get_schema_or_none(instance.derive(type=args[0]), ctx)
|
||||
return JSONArraySchema(items=items_schema)
|
||||
else:
|
||||
min_items = 0
|
||||
max_items = 0
|
||||
prefix_items = []
|
||||
items: Optional[JSONSchema] = None
|
||||
unpack_schema: Optional[JSONSchema] = None
|
||||
unpack_idx = 0
|
||||
for arg_idx, arg in enumerate(args, start=1):
|
||||
if not is_unpack(arg):
|
||||
min_items += 1
|
||||
if not unpack_schema:
|
||||
prefix_items.append(
|
||||
get_schema(instance.derive(type=arg), ctx)
|
||||
)
|
||||
else:
|
||||
unpack_schema = get_schema(instance.derive(type=arg), ctx)
|
||||
unpack_idx = arg_idx
|
||||
if unpack_schema:
|
||||
prefix_items.extend(unpack_schema.prefixItems or [])
|
||||
min_items += unpack_schema.minItems or 0
|
||||
max_items += unpack_schema.maxItems or 0
|
||||
if unpack_idx == len(args):
|
||||
items = unpack_schema.items
|
||||
else:
|
||||
min_items = len(args)
|
||||
max_items = len(args)
|
||||
return JSONArraySchema(
|
||||
prefixItems=prefix_items or None,
|
||||
items=items,
|
||||
minItems=min_items or None,
|
||||
maxItems=max_items or None,
|
||||
)
|
||||
|
||||
|
||||
def on_named_tuple(instance: Instance, ctx: Context) -> JSONSchema:
|
||||
resolved = resolve_type_params(
|
||||
instance.origin_type, get_args(instance.type)
|
||||
)[instance.origin_type]
|
||||
annotations = {
|
||||
k: resolved.get(v, v)
|
||||
for k, v in getattr(
|
||||
instance.origin_type, "__annotations__", {}
|
||||
).items()
|
||||
}
|
||||
fields = getattr(instance.type, "_fields", ())
|
||||
defaults = getattr(instance.type, "_field_defaults", {})
|
||||
as_dict = instance.get_owner_dialect_or_config_option(
|
||||
"namedtuple_as_dict", False
|
||||
)
|
||||
serialize_option = instance.get_overridden_serialization_method()
|
||||
if serialize_option == "as_dict":
|
||||
as_dict = True
|
||||
elif serialize_option == "as_list":
|
||||
as_dict = False
|
||||
properties = {}
|
||||
for f_name in fields:
|
||||
f_type = annotations.get(f_name, Any)
|
||||
f_schema = get_schema(instance.derive(type=f_type), ctx)
|
||||
f_default = defaults.get(f_name, MISSING)
|
||||
if f_default is not MISSING:
|
||||
if isinstance(f_schema, EmptyJSONSchema):
|
||||
f_schema = JSONSchema()
|
||||
f_schema.default = _default(
|
||||
f_type, f_default, instance.get_self_config()
|
||||
)
|
||||
properties[f_name] = f_schema
|
||||
if as_dict:
|
||||
return JSONObjectSchema(
|
||||
properties=properties or None,
|
||||
required=list(fields),
|
||||
additionalProperties=False,
|
||||
)
|
||||
else:
|
||||
return JSONArraySchema(
|
||||
prefixItems=list(properties.values()) or None,
|
||||
maxItems=len(properties) or None,
|
||||
minItems=len(properties) or None,
|
||||
)
|
||||
|
||||
|
||||
def on_typed_dict(instance: Instance, ctx: Context) -> JSONObjectSchema:
|
||||
resolved = resolve_type_params(
|
||||
instance.origin_type, get_args(instance.type)
|
||||
)[instance.origin_type]
|
||||
annotations = {
|
||||
k: resolved.get(v, v)
|
||||
for k, v in instance.origin_type.__annotations__.items()
|
||||
}
|
||||
all_keys = list(annotations.keys())
|
||||
required_keys = getattr(instance.type, "__required_keys__", all_keys)
|
||||
return JSONObjectSchema(
|
||||
properties={
|
||||
key: get_schema(instance.derive(type=annotations[key]), ctx)
|
||||
for key in all_keys
|
||||
}
|
||||
or None,
|
||||
required=sorted(required_keys) or None,
|
||||
additionalProperties=False,
|
||||
)
|
||||
|
||||
|
||||
def apply_array_constraints(
|
||||
instance: Instance,
|
||||
schema: JSONSchema,
|
||||
) -> JSONSchema:
|
||||
has_contains = False
|
||||
min_contains: Optional[int] = None
|
||||
max_contains: Optional[int] = None
|
||||
for annotation in instance.annotations:
|
||||
if isinstance(annotation, MinItems):
|
||||
schema.minItems = annotation.value
|
||||
elif isinstance(annotation, MaxItems):
|
||||
schema.maxItems = annotation.value
|
||||
elif isinstance(annotation, UniqueItems):
|
||||
schema.uniqueItems = annotation.value
|
||||
elif isinstance(annotation, Contains):
|
||||
schema.contains = annotation.value
|
||||
has_contains = True
|
||||
elif isinstance(annotation, MinContains):
|
||||
min_contains = annotation.value
|
||||
elif isinstance(annotation, MaxContains):
|
||||
max_contains = annotation.value
|
||||
if has_contains:
|
||||
if min_contains is not None:
|
||||
schema.minContains = min_contains
|
||||
if max_contains is not None:
|
||||
schema.maxContains = max_contains
|
||||
return schema
|
||||
|
||||
|
||||
def apply_object_constraints(
|
||||
instance: Instance, schema: JSONSchema
|
||||
) -> JSONSchema:
|
||||
for annotation in instance.annotations:
|
||||
if isinstance(annotation, MaxProperties):
|
||||
schema.maxProperties = annotation.value
|
||||
elif isinstance(annotation, MinProperties):
|
||||
schema.minProperties = annotation.value
|
||||
elif isinstance(annotation, DependentRequired):
|
||||
schema.dependentRequired = annotation.value
|
||||
return schema
|
||||
|
||||
|
||||
@register
|
||||
def on_collection(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if not issubclass(instance.origin_type, Collection):
|
||||
return None
|
||||
elif issubclass(instance.origin_type, Enum):
|
||||
return None
|
||||
|
||||
args = get_args(instance.type)
|
||||
|
||||
if issubclass(instance.origin_type, ByteString): # type: ignore[arg-type]
|
||||
return JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=JSONSchemaInstanceFormatExtension.BASE64,
|
||||
)
|
||||
elif issubclass(instance.origin_type, str):
|
||||
schema = JSONSchema(type=JSONSchemaInstanceType.STRING)
|
||||
for annotation in instance.annotations:
|
||||
if isinstance(annotation, MinLength):
|
||||
schema.minLength = annotation.value
|
||||
elif isinstance(annotation, MaxLength):
|
||||
schema.maxLength = annotation.value
|
||||
elif isinstance(annotation, Pattern):
|
||||
schema.pattern = annotation.value
|
||||
return schema
|
||||
elif is_generic(instance.type) and issubclass(
|
||||
instance.origin_type, (list, deque)
|
||||
):
|
||||
return apply_array_constraints(
|
||||
instance,
|
||||
JSONArraySchema(
|
||||
items=(
|
||||
_get_schema_or_none(instance.derive(type=args[0]), ctx)
|
||||
if args
|
||||
else None
|
||||
)
|
||||
),
|
||||
)
|
||||
elif issubclass(instance.origin_type, tuple):
|
||||
if is_named_tuple(instance.origin_type):
|
||||
return apply_array_constraints(
|
||||
instance, on_named_tuple(instance, ctx)
|
||||
)
|
||||
elif is_generic(instance.type):
|
||||
return apply_array_constraints(instance, on_tuple(instance, ctx))
|
||||
elif is_generic(instance.type) and issubclass(
|
||||
instance.origin_type, (frozenset, Set)
|
||||
):
|
||||
return apply_array_constraints(
|
||||
instance,
|
||||
JSONArraySchema(
|
||||
items=(
|
||||
_get_schema_or_none(instance.derive(type=args[0]), ctx)
|
||||
if args
|
||||
else None
|
||||
),
|
||||
uniqueItems=True,
|
||||
),
|
||||
)
|
||||
elif is_generic(instance.type) and issubclass(
|
||||
instance.origin_type, ChainMap
|
||||
):
|
||||
return apply_array_constraints(
|
||||
instance,
|
||||
JSONArraySchema(
|
||||
items=get_schema(
|
||||
instance=instance.derive(
|
||||
type=(
|
||||
dict[args[0], args[1]] # type: ignore
|
||||
if args
|
||||
else dict
|
||||
)
|
||||
),
|
||||
ctx=ctx,
|
||||
)
|
||||
),
|
||||
)
|
||||
elif is_generic(instance.type) and issubclass(
|
||||
instance.origin_type, Counter
|
||||
):
|
||||
schema = JSONObjectSchema(
|
||||
additionalProperties=get_schema(instance.derive(type=int), ctx),
|
||||
)
|
||||
if args:
|
||||
schema.propertyNames = _get_schema_or_none(
|
||||
instance.derive(type=args[0]), ctx
|
||||
)
|
||||
return apply_object_constraints(instance, schema)
|
||||
elif is_typed_dict(instance.origin_type):
|
||||
return on_typed_dict(instance, ctx)
|
||||
elif is_generic(instance.type) and issubclass(
|
||||
instance.origin_type, Mapping
|
||||
):
|
||||
schema = JSONObjectSchema(
|
||||
additionalProperties=(
|
||||
_get_schema_or_none(instance.derive(type=args[1]), ctx)
|
||||
if args
|
||||
else None
|
||||
),
|
||||
propertyNames=(
|
||||
_get_schema_or_none(instance.derive(type=args[0]), ctx)
|
||||
if args
|
||||
else None
|
||||
),
|
||||
)
|
||||
return apply_object_constraints(instance, schema)
|
||||
elif is_generic(instance.type) and issubclass(
|
||||
instance.origin_type, Sequence
|
||||
):
|
||||
return apply_array_constraints(
|
||||
instance,
|
||||
JSONArraySchema(
|
||||
items=(
|
||||
_get_schema_or_none(instance.derive(type=args[0]), ctx)
|
||||
if args
|
||||
else None
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@register
|
||||
def on_pathlike(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if issubclass(instance.origin_type, os.PathLike):
|
||||
schema = JSONSchema(
|
||||
type=JSONSchemaInstanceType.STRING,
|
||||
format=JSONSchemaInstanceFormatExtension.PATH,
|
||||
)
|
||||
for annotation in instance.annotations:
|
||||
if isinstance(annotation, MaxLength):
|
||||
schema.maxLength = annotation.value
|
||||
elif isinstance(annotation, MinLength):
|
||||
schema.minLength = annotation.value
|
||||
return schema
|
||||
|
||||
|
||||
@register
|
||||
def on_enum(instance: Instance, ctx: Context) -> Optional[JSONSchema]:
|
||||
if issubclass(instance.origin_type, Enum):
|
||||
return JSONSchema(enum=[m.value for m in instance.origin_type])
|
||||
|
||||
|
||||
__all__ = ["Instance", "get_schema"]
|
||||
Reference in New Issue
Block a user