python-jsonlogic 0.0.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jsonlogic/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ from .core import JSONLogicExpression, JSONLogicSyntaxError, Operator
2
+
3
+ __all__ = ("JSONLogicExpression", "JSONLogicSyntaxError", "Operator")
jsonlogic/_compat.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import sys
2
2
 
3
- if sys.version_info >= (3, 12):
4
- from typing import Self, TypeAlias, TypeAliasType
3
+ if sys.version_info >= (3, 13):
4
+ from typing import Self, TypeAlias, TypeIs, TypeVarTuple, Unpack
5
5
  else:
6
- from typing_extensions import Self, TypeAlias, TypeAliasType
6
+ from typing import TypeAlias
7
7
 
8
- __all__ = ("Self", "TypeAlias", "TypeAliasType")
8
+ from typing_extensions import Self, TypeIs, TypeVarTuple, Unpack
9
+
10
+
11
+ __all__ = ("Self", "TypeAlias", "TypeIs", "TypeVarTuple", "Unpack")
jsonlogic/core.py CHANGED
@@ -1,3 +1,7 @@
1
+ """Base structures of the library. The two classes defined, :class:`Operator` and :class:`JSONLogicExpression`,
2
+ can be extended to provide extra functionality.
3
+ """
4
+
1
5
  from __future__ import annotations
2
6
 
3
7
  from abc import ABC, abstractmethod
@@ -6,13 +10,12 @@ from typing import TYPE_CHECKING, Any
6
10
 
7
11
  from ._compat import Self, TypeAlias
8
12
  from .json_schema.types import AnyType, JSONSchemaType
9
- from .typing import JSON, JSONLogicPrimitive, OperatorArgument
13
+ from .typing import JSON, JSONLogicPrimitive, JSONObject, OperatorArgument
10
14
 
11
15
  if TYPE_CHECKING:
12
- # This is a hack to make pyright think `TypeAlias` comes from `typing`
13
- from typing import TypeAlias
14
-
16
+ from .evaluation import EvaluationContext
15
17
  from .registry import OperatorRegistry
18
+ from .typechecking import TypecheckContext
16
19
 
17
20
 
18
21
  @dataclass
@@ -31,43 +34,96 @@ class Operator(ABC):
31
34
  @classmethod
32
35
  @abstractmethod
33
36
  def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
34
- """Return an instance of the operator from the list of provided arguments."""
37
+ """Return an instance of the operator from the list of provided arguments.
38
+
39
+ Args:
40
+ operator: The ID of the operator, as provided by the :class:`~jsonlogic.registry.OperatorRegistry`.
41
+ arguments: The list of the arguments for this operator. Subclasses are responsible
42
+ for checking the correct number of arguments and optionally the types.
43
+ """
35
44
 
36
45
  @abstractmethod
37
- def apply(self, data: JSON) -> Any:
38
- pass
46
+ def evaluate(self, context: EvaluationContext) -> Any:
47
+ """Evaluate the operator with the provided data."""
39
48
 
40
- def typecheck(self, data_schema: dict[str, Any]) -> JSONSchemaType:
49
+ def typecheck(self, context: TypecheckContext) -> JSONSchemaType:
41
50
  """Typecheck the operator (and all children) given the data schema."""
42
51
 
43
52
  return AnyType()
44
53
 
45
54
 
46
- NormalizedExpression: TypeAlias = "dict[str, list[JSONLogicExpression]]"
55
+ class JSONLogicSyntaxError(Exception):
56
+ """A syntax error when building an operator tree from a :class:`JSONLogicExpression`."""
57
+
58
+ def __init__(self, message: str, /) -> None:
59
+ self.message = message
60
+
61
+
62
+ ExprArgument: TypeAlias = "JSONLogicPrimitive | JSONLogicExpression | list[ExprArgument]"
63
+
64
+ NormalizedExpression: TypeAlias = "dict[str, list[ExprArgument]]"
47
65
 
48
66
 
49
67
  @dataclass
50
68
  class JSONLogicExpression:
51
- expression: JSONLogicPrimitive | NormalizedExpression
69
+ """A parsed and normalized JSON Logic expression.
70
+
71
+ The underlying structure of an expression is a single item dictionary,
72
+ mapping the operator key to a list of arguments.
73
+
74
+ All JSON Logic expressions should be instantiated using the :meth:`from_json` constructor::
75
+
76
+ expr = JSONLogicExpression.from_json({"op": ...})
77
+ """
78
+
79
+ expression: NormalizedExpression
80
+
81
+ @classmethod
82
+ def _parse_impl(cls, json: JSON) -> ExprArgument:
83
+ if isinstance(json, dict):
84
+ return cls.from_json(json)
85
+ if isinstance(json, list):
86
+ return [cls._parse_impl(s) for s in json]
87
+ return json
52
88
 
53
89
  @classmethod
54
- def from_json(cls, json: JSON) -> Self: # TODO disallow list?
90
+ def from_json(cls, json: JSONObject) -> Self:
91
+ """Build a JSON Logic expression from JSON data.
92
+
93
+ Operator arguments are recursively normalized to a :class:`list`::
94
+
95
+ expr = JSONLogicExpression.from_json({"var": "varname"})
96
+ assert expr.expression == {"var": ["varname"]}
97
+ """
55
98
  if not isinstance(json, dict):
56
- return cls(expression=json)
99
+ raise ValueError("The root node of the expression must be a dict")
57
100
 
58
101
  operator, op_args = next(iter(json.items()))
59
102
  if not isinstance(op_args, list):
60
103
  op_args = [op_args]
61
104
 
62
- sub_expressions = [cls.from_json(op_arg) for op_arg in op_args]
105
+ return cls({operator: [cls._parse_impl(arg) for arg in op_args]})
106
+
107
+ def _as_op_impl(self, op_arg: ExprArgument, operator_registry: OperatorRegistry) -> OperatorArgument:
108
+ if isinstance(op_arg, JSONLogicExpression):
109
+ return op_arg.as_operator_tree(operator_registry)
110
+ if isinstance(op_arg, list):
111
+ return [self._as_op_impl(sub_arg, operator_registry) for sub_arg in op_arg]
112
+ return op_arg
113
+
114
+ def as_operator_tree(self, operator_registry: OperatorRegistry) -> Operator:
115
+ """Return a recursive tree of operators, using the provided registry as a reference.
63
116
 
64
- return cls({operator: sub_expressions})
117
+ Args:
118
+ operator_registry: The registry to use to resolve operator IDs.
65
119
 
66
- def as_operator_tree(self, operator_registry: OperatorRegistry) -> JSONLogicPrimitive | Operator:
120
+ Returns:
121
+ An :class:`Operator` instance.
122
+ """
67
123
  if not isinstance(self.expression, dict):
68
124
  return self.expression
69
125
 
70
126
  op_id, op_args = next(iter(self.expression.items()))
71
127
  OperatorCls = operator_registry.get(op_id)
72
128
 
73
- return OperatorCls.from_expression(op_id, [op_arg.as_operator_tree(operator_registry) for op_arg in op_args])
129
+ return OperatorCls.from_expression(op_id, [self._as_op_impl(op_arg, operator_registry) for op_arg in op_args])
@@ -0,0 +1,5 @@
1
+ from .evaluation_context import EvaluationContext
2
+ from .evaluation_settings import EvaluationSettings
3
+ from .utils import evaluate, get_value
4
+
5
+ __all__ = ("EvaluationContext", "EvaluationSettings", "evaluate", "get_value")
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal, overload
4
+
5
+ from jsonlogic.json_schema import cast_from_schema
6
+ from jsonlogic.resolving import resolve_data, resolve_json_schema
7
+ from jsonlogic.typing import JSON
8
+ from jsonlogic.utils import DataStack
9
+
10
+ from .evaluation_settings import EvaluationSettings, EvaluationSettingsDict
11
+
12
+
13
+ class EvaluationContext:
14
+ """A context object used when evaluating operators.
15
+
16
+ When evaluating an :class:`~jsonlogic.core.Operator`, an instance of this
17
+ class should be used.
18
+
19
+ .. code-block:: pycon
20
+
21
+ >>> expr = JSONLogicExpression.from_json({"var": "/a_date"})
22
+ >>> root_op = expr.as_operator_tree(operator_registry)
23
+ >>> context = EvaluationContext(
24
+ ... data={"a_date": "1970-01-01"},
25
+ ... data_schema={
26
+ ... "type": "object",
27
+ ... "properties": {
28
+ ... "a_date": {"type": "string", "format": "date"},
29
+ ... },
30
+ ... },
31
+ ... )
32
+ >>> root_op.evaluate(context)
33
+ datetime.date(1970, 1, 1)
34
+
35
+ Args:
36
+ root_data: The root data available during evaluation.
37
+ data_schema: The matching JSON Schema describing the root data. This should be the same JSON Schema
38
+ used during typechecking (see :paramref:`~jsonlogic.typechecking.TypecheckContext.root_data_schema`).
39
+ settings: Settings to be used when evaluating an :class:`~jsonlogic.core.Operator`.
40
+ See :class:`EvaluationSettings` for the available settings and default values.
41
+ """
42
+
43
+ def __init__(
44
+ self, root_data: JSON, data_schema: dict[str, Any] | None = None, settings: EvaluationSettingsDict | None = None
45
+ ) -> None:
46
+ self.data_stack = DataStack((root_data, data_schema))
47
+ self.settings = EvaluationSettings.from_dict(settings) if settings is not None else EvaluationSettings()
48
+
49
+ @overload
50
+ def resolve_variable(self, reference: str, *, bare: Literal[True]) -> JSON: ...
51
+
52
+ @overload
53
+ def resolve_variable(self, reference: str, *, bare: Literal[False] = ...) -> Any: ...
54
+
55
+ def resolve_variable(self, reference: str, *, bare: bool = False) -> JSON | Any:
56
+ """Resolve a variable given the string reference pointing to it.
57
+
58
+ The format of the reference should match the reference parser defined
59
+ in the :class:`EvaluationSettings`.
60
+
61
+ Args:
62
+ reference: The string reference of the variable.
63
+ bare: Whether the resolved value should be casted to a specific Python
64
+ type according to the matching JSON Schema. Note that this will only
65
+ be possible if a :paramref:`~EvaluationContext.data_schema` was provided.
66
+ """
67
+ parsed_reference, scope = self.settings.reference_parser(reference)
68
+ root_data, root_schema = self.data_stack.get(scope)
69
+ bare_value = resolve_data(parsed_reference, root_data)
70
+ if bare or root_schema is None:
71
+ return bare_value
72
+
73
+ schema = resolve_json_schema(parsed_reference, root_schema)
74
+ return cast_from_schema(bare_value, schema, self.settings.variable_casts)
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from datetime import date, datetime
6
+ from typing import Any, TypedDict
7
+
8
+ from jsonlogic._compat import Self
9
+ from jsonlogic.resolving import PointerReferenceParser, ReferenceParser
10
+
11
+
12
+ def _d_variable_casts() -> dict[str, Callable[[str], Any]]:
13
+ return {
14
+ "date": date.fromisoformat,
15
+ "date-time": datetime.fromisoformat,
16
+ }
17
+
18
+
19
+ @dataclass
20
+ class EvaluationSettings:
21
+ """Settings used when evaluating an :class:`~jsonlogic.core.Operator`."""
22
+
23
+ reference_parser: ReferenceParser = field(default_factory=PointerReferenceParser)
24
+ """A reference parser instance to use when resolving variables.
25
+
26
+ Default: :class:`~jsonlogic.resolving.PointerReferenceParser`.
27
+ """
28
+
29
+ variable_casts: dict[str, Callable[[str], Any]] = field(default_factory=_d_variable_casts)
30
+ """A mapping between `JSON Schema formats`_ and their corresponding conversion callable.
31
+
32
+ When an operator reads variables from the provided data (such as the ``"var"`` operator),
33
+ such variables of type :class:`str` may be converted to a specific Python type if
34
+ the corresponding JSON Schema of the data was provided during evaluation.
35
+
36
+ This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.variable_casts`
37
+ configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
38
+
39
+ Default: :python:`{"date": date.fromisoformat, "date-time": datetime.fromisoformat}`.
40
+
41
+ .. _JSON Schema formats: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.2
42
+ """
43
+
44
+ literal_casts: list[Callable[[str], Any]] = field(default_factory=list)
45
+ """A list of conversion callables to try when encountering a literal string value during evaluation.
46
+
47
+ When a literal string value is encountered in a JSON Logic expression, it might be
48
+ beneficial to convert it to a specific Python type.
49
+
50
+ This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.literal_casts`
51
+ configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
52
+
53
+ Default: :python:`[]` (no cast).
54
+
55
+ .. warning::
56
+
57
+ The order in which the conversion callables are defined matters. Each
58
+ callable will be applied one after the other until no exception is raised.
59
+ """
60
+
61
+ @classmethod
62
+ def from_dict(cls, dct: EvaluationSettingsDict, /) -> Self:
63
+ return cls(**dct)
64
+
65
+
66
+ class EvaluationSettingsDict(TypedDict, total=False):
67
+ """Settings used when evaluating an :class:`~jsonlogic.core.Operator`."""
68
+
69
+ reference_parser: ReferenceParser
70
+ """A reference parser instance to use when resolving variables.
71
+
72
+ Default: :class:`~jsonlogic.resolving.PointerReferenceParser`.
73
+ """
74
+
75
+ variable_casts: dict[str, Callable[[str], Any]]
76
+ """A mapping between `JSON Schema formats`_ and their corresponding conversion callable.
77
+
78
+ When an operator reads variables from the provided data (such as the ``"var"`` operator),
79
+ such variables of type :class:`str` may be converted to a specific Python type if
80
+ the corresponding JSON Schema of the data was provided during evaluation.
81
+
82
+ This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.variable_casts`
83
+ configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
84
+
85
+ Default: :python:`{"date": date.fromisoformat, "date-time": datetime.fromisoformat}`.
86
+
87
+ .. _JSON Schema formats: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.2
88
+ """
89
+
90
+ literal_casts: list[Callable[[str], Any]]
91
+ """A list of conversion callables to try when encountering a literal string value during evaluation.
92
+
93
+ When a literal string value is encountered in a JSON Logic expression, it might be
94
+ beneficial to convert it to a specific Python type.
95
+
96
+ This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.literal_casts`
97
+ configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
98
+
99
+ Default: :python:`[]` (no cast).
100
+
101
+ .. warning::
102
+
103
+ The order in which the conversion callables are defined matters. Each
104
+ callable will be applied one after the other until no exception is raised.
105
+ """
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from jsonlogic.core import Operator
7
+ from jsonlogic.typing import JSON, JSONLogicPrimitive, OperatorArgument
8
+
9
+ from .evaluation_context import EvaluationContext
10
+ from .evaluation_settings import EvaluationSettingsDict
11
+
12
+
13
+ def evaluate(
14
+ operator: Operator, data: JSON, data_schema: dict[str, Any] | None, settings: EvaluationSettingsDict | None = None
15
+ ) -> Any:
16
+ """Helper function to evaluate an :class:`~jsonlogic.core.Operator`.
17
+
18
+ Args:
19
+ operator: The operator to evaluate.
20
+ data: The root data available during evaluation.
21
+ data_schema: The matching JSON Schema describing the root data. This should be the same JSON Schema
22
+ used during typechecking (see :paramref:`~jsonlogic.typechecking.TypecheckContext.root_data_schema`).
23
+ settings: Settings to be used when evaluating an :class:`~jsonlogic.core.Operator`.
24
+ See :class:`EvaluationSettings` for the available settings and default values.
25
+ Returns:
26
+ The evaluated value.
27
+ """
28
+ context = EvaluationContext(data, data_schema, settings)
29
+ return operator.evaluate(context)
30
+
31
+
32
+ # Function analogous to :func:`jsonlogic.json_schema.from_value`
33
+ def _cast_value(value: JSONLogicPrimitive, literal_casts: list[Callable[[str], Any]]) -> Any:
34
+ if isinstance(value, str):
35
+ for func in literal_casts:
36
+ try:
37
+ casted_value = func(value)
38
+ except Exception:
39
+ pass
40
+ else:
41
+ return casted_value
42
+
43
+ if not isinstance(value, list):
44
+ return value
45
+
46
+ return [_cast_value(subval, literal_casts) for subval in value]
47
+
48
+
49
+ def get_value(obj: OperatorArgument, context: EvaluationContext) -> Any:
50
+ """Get the value of an operator argument.
51
+
52
+ Args:
53
+ obj: the object to evaluate. If this is an :class:`~jsonlogic.core.Operator`,
54
+ it is evaluated and the value is returned. Otherwise, it must be a
55
+ :data:`~jsonlogic.typing.JSONLogicPrimitive`, and the type is inferred from
56
+ the actual value according to the :attr:`~TypecheckSettings.literal_casts` setting.
57
+ context: The typecheck context.
58
+ """
59
+ if isinstance(obj, Operator):
60
+ return obj.evaluate(context)
61
+ if isinstance(obj, list):
62
+ return [get_value(sub_obj, context) for sub_obj in obj]
63
+ return _cast_value(obj, context.settings.literal_casts)
@@ -1,77 +1,189 @@
1
- from datetime import date, datetime
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
2
4
  from types import NoneType
3
- from typing import Any, Callable, cast
5
+ from typing import Any, cast
4
6
 
5
- from jsonlogic.typing import JSONLogicPrimitive
7
+ from jsonlogic._compat import TypeIs
8
+ from jsonlogic.typing import JSON, JSONLogicPrimitive
6
9
 
7
10
  from .types import (
8
11
  AnyType,
12
+ ArrayType,
9
13
  BooleanType,
10
- DatetimeType,
11
- DateType,
12
14
  IntegerType,
13
15
  JSONSchemaType,
14
16
  NullType,
15
17
  NumberType,
16
18
  StringType,
19
+ TupleType,
20
+ UnionType,
21
+ )
22
+
23
+ __all__ = (
24
+ "as_json_schema",
25
+ "cast_from_schema",
26
+ "from_json_schema",
27
+ "from_value",
17
28
  )
18
29
 
19
- _VALUE_TYPE_MAP: dict[type[Any], JSONSchemaType] = {
20
- bool: BooleanType(),
21
- float: NumberType(),
22
- int: IntegerType(),
23
- NoneType: NullType(),
30
+ _VALUE_TYPE_MAP: dict[type[Any], type[JSONSchemaType]] = {
31
+ bool: BooleanType,
32
+ float: NumberType,
33
+ int: IntegerType,
34
+ NoneType: NullType,
24
35
  }
25
36
 
26
- _VALUE_FORMAT_MAP: dict[Callable[[str], Any], JSONSchemaType] = {
27
- datetime.fromisoformat: DatetimeType(),
28
- date.fromisoformat: DateType(),
37
+ _PRIMITIVES_TYPE_MAP: dict[str, type[JSONSchemaType]] = {
38
+ "boolean": BooleanType,
39
+ "number": NumberType,
40
+ "integer": IntegerType,
41
+ "null": NullType,
29
42
  }
30
43
 
44
+ _R_PRIMITIVES_TYPE_MAP: dict[type[JSONSchemaType], str] = {
45
+ **{v: k for k, v in _PRIMITIVES_TYPE_MAP.items()},
46
+ StringType: "string",
47
+ }
31
48
 
32
- def from_value(value: JSONLogicPrimitive) -> JSONSchemaType:
33
- if type(value) in _VALUE_TYPE_MAP:
34
- return _VALUE_TYPE_MAP[type(value)]
35
49
 
50
+ # Defined purely for type checking purposes:
51
+ def _is_primitive_not_str(value: object) -> TypeIs[bool | float | None]:
52
+ return type(value) in _VALUE_TYPE_MAP
53
+
54
+
55
+ def from_value(
56
+ value: JSONLogicPrimitive, literal_casts: dict[Callable[[str], Any], type[JSONSchemaType]]
57
+ ) -> JSONSchemaType:
36
58
  if isinstance(value, str):
37
- for func, js_type in _VALUE_FORMAT_MAP.items():
59
+ for func, js_type in literal_casts.items():
38
60
  try:
39
61
  func(value)
40
62
  except Exception:
41
63
  pass
42
64
  else:
43
- return js_type
65
+ return js_type()
44
66
 
45
67
  return StringType()
46
68
 
47
- return AnyType()
69
+ if _is_primitive_not_str(value):
70
+ return _VALUE_TYPE_MAP[type(value)]()
48
71
 
72
+ # A design decision had to be made: we infer arrays as arrays and not as tuples,
73
+ # even if types are different.
74
+ if value:
75
+ return ArrayType(UnionType(*(from_value(val, literal_casts) for val in value)))
49
76
 
50
- _TYPE_MAP: dict[str, JSONSchemaType] = {
51
- "boolean": BooleanType(),
52
- "number": NumberType(),
53
- "integer": IntegerType(),
54
- "null": NullType(),
55
- }
77
+ # Empty array: can't infer type
78
+ return ArrayType(AnyType())
56
79
 
57
- _FORMAT_MAP: dict[str, JSONSchemaType] = {
58
- "date-time": DatetimeType(),
59
- "date": DateType(),
60
- }
61
80
 
81
+ def from_json_schema(json_schema: dict[str, Any], variable_casts: dict[str, type[JSONSchemaType]]) -> JSONSchemaType:
82
+ js_types = cast("list[str] | str | None", json_schema.get("type"))
83
+ if js_types is None:
84
+ return AnyType()
62
85
 
63
- def from_json_schema(json_schema: dict[str, Any]) -> JSONSchemaType:
64
- # TODO support for unions
65
- js_type = cast(str | None, json_schema.get("type"))
66
- if js_type in _TYPE_MAP:
67
- return _TYPE_MAP[js_type]
86
+ if not isinstance(js_types, list):
87
+ js_types = [js_types]
68
88
 
69
- if js_type == "string":
70
- format = cast(str | None, json_schema.get("format"))
89
+ def _from_type(js_type: str, json_schema: dict[str, Any]) -> JSONSchemaType:
90
+ if js_type in _PRIMITIVES_TYPE_MAP:
91
+ return _PRIMITIVES_TYPE_MAP[js_type]()
71
92
 
72
- if format in _FORMAT_MAP:
73
- return _FORMAT_MAP[format]
93
+ if js_type == "string":
94
+ format = cast("str | None", json_schema.get("format"))
95
+ if format in variable_casts:
96
+ return variable_casts[format]()
74
97
 
75
- return StringType()
98
+ return StringType()
99
+
100
+ if js_type == "array":
101
+ items_type = cast("dict[str, Any] | None", json_schema.get("items"))
102
+ if items_type is not None:
103
+ return ArrayType(from_json_schema(items_type, variable_casts))
104
+
105
+ prefix_items = cast("list[dict[str, Any]] | None", json_schema.get("prefixItems"))
106
+ min_items = cast("int | None", json_schema.get("minItems"))
107
+ max_items = cast("int | None", json_schema.get("maxItems"))
108
+ if prefix_items is not None and min_items is not None and min_items == max_items:
109
+ return TupleType(tuple(from_json_schema(item, variable_casts) for item in prefix_items))
110
+
111
+ return ArrayType(AnyType())
112
+
113
+ return AnyType()
114
+
115
+ return UnionType(*(_from_type(js_type, json_schema) for js_type in js_types))
116
+
117
+
118
+ def as_json_schema(type: JSONSchemaType, variable_casts: dict[str, type[JSONSchemaType]]) -> dict[str, Any]:
119
+ type_class = type.__class__
120
+ if type_class in _R_PRIMITIVES_TYPE_MAP:
121
+ return {"type": _R_PRIMITIVES_TYPE_MAP[type_class]}
122
+
123
+ if isinstance(type, AnyType):
124
+ return {}
125
+
126
+ if isinstance(type, UnionType):
127
+ sub_schemas = [as_json_schema(subtype, variable_casts) for subtype in type.types]
128
+ types = [
129
+ sub_schema.pop("type") # UnionTypes can't have any, so `"type"` is guaranteed to be present
130
+ for sub_schema in sub_schemas
131
+ ]
132
+ schema = {
133
+ "type": types[0] if len(types) == 1 else types,
134
+ }
135
+ for sub_schema in sub_schemas:
136
+ schema.update(sub_schema)
137
+ return schema
138
+
139
+ if isinstance(type, ArrayType):
140
+ items_type = as_json_schema(type.elements_type, variable_casts)
141
+ if items_type:
142
+ return {"type": "array", "items": items_type}
143
+ return {"type": "array"}
144
+
145
+ if isinstance(type, TupleType):
146
+ return {
147
+ "type": "array",
148
+ "minItems": len(type.tuple_types),
149
+ "maxItems": len(type.tuple_types),
150
+ "prefixItems": [as_json_schema(subtype, variable_casts) for subtype in type.tuple_types],
151
+ }
152
+
153
+ r_variable_casts = {v: k for k, v in variable_casts.items()}
154
+
155
+ if type_class in r_variable_casts:
156
+ return {"type": "string", "format": r_variable_casts[type_class]}
157
+
158
+ raise RuntimeError(f"Unable to determine JSON Schema for type {type}")
159
+
160
+
161
+ def cast_from_schema(value: JSON, json_schema: dict[str, Any], variable_casts: dict[str, Callable[[str], Any]]) -> Any:
162
+ js_types = cast("list[str] | str | None", json_schema.get("type"))
163
+ if js_types is None:
164
+ return value
165
+
166
+ if not isinstance(js_types, list):
167
+ js_types = [js_types]
168
+
169
+ if (
170
+ isinstance(value, str)
171
+ and "string" in js_types
172
+ and (cast_func := variable_casts.get(json_schema.get("format"))) # type: ignore
173
+ ): # fmt: skip
174
+ return cast_func(value)
76
175
 
77
- return AnyType()
176
+ if isinstance(value, list) and "array" in js_types:
177
+ items_type = cast("dict[str, Any] | None", json_schema.get("items"))
178
+ if items_type is not None:
179
+ return [cast_from_schema(val, items_type, variable_casts) for val in value]
180
+
181
+ prefix_items = cast("list[dict[str, Any]] | None", json_schema.get("prefixItems"))
182
+ if prefix_items is not None: # TODO check for min/maxItems?
183
+ # TODO return a tuple instead? Needs decision
184
+ return [
185
+ cast_from_schema(val, item_type, variable_casts)
186
+ for val, item_type in zip(value, prefix_items, strict=True)
187
+ ]
188
+
189
+ return value