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