Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit 80b2ed9

Browse files
Add union operator, and JSONSchema field
1 parent 2d51970 commit 80b2ed9

4 files changed

Lines changed: 102 additions & 19 deletions

File tree

tests/test_fields.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ def test_boolean():
346346
value, error = validator.validate_or_error(2)
347347
assert error == ValidationError(text="Must be a boolean.", code="type")
348348

349+
validator = Boolean()
350+
value, error = validator.validate_or_error([])
351+
assert error == ValidationError(text="Must be a boolean.", code="type")
352+
349353
validator = Boolean(allow_null=True)
350354
value, error = validator.validate_or_error(None)
351355
assert value is None
@@ -747,6 +751,14 @@ def test_union():
747751
text="Must be less than or equal to 1000.", code="maximum"
748752
)
749753

754+
validator = Integer() | String() | Boolean()
755+
value, error = validator.validate_or_error(123)
756+
assert value == 123
757+
758+
validator = Integer() | (String() | Boolean())
759+
value, error = validator.validate_or_error(123)
760+
assert value == 123
761+
750762

751763
def test_const():
752764
validator = Const(const=None)

tests/test_json_schema.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
import typesystem
8-
from typesystem.json_schema import from_json_schema, to_json_schema
8+
from typesystem.json_schema import from_json_schema, to_json_schema, JSONSchema
99

1010
filenames = [
1111
"additionalItems.json",
@@ -85,6 +85,14 @@ def test_json_schema(schema, data, is_valid, description):
8585
assert error is not None, description
8686

8787

88+
@pytest.mark.parametrize("schema,data,is_valid,description", test_cases)
89+
def test_json_schema_validator(schema, data, is_valid, description):
90+
"""
91+
Use the `JSONSchema` field to validate all the test case schemas.
92+
"""
93+
JSONSchema.validate(schema)
94+
95+
8896
@pytest.mark.parametrize("schema,data,is_valid,description", test_cases)
8997
def test_to_from_json_schema(schema, data, is_valid, description):
9098
"""

typesystem/fields.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,18 @@ def validation_error(self, code: str) -> ValidationError:
7777
def get_error_text(self, code: str) -> str:
7878
return self.errors[code].format(**self.__dict__)
7979

80+
def __or__(self, other: "Field") -> "Union":
81+
if isinstance(self, Union):
82+
any_of = self.any_of
83+
else:
84+
any_of = [self]
8085

81-
def normalize_regex(
82-
pattern: typing.Union[str, typing.Pattern] = None
83-
) -> typing.Tuple[typing.Optional[str], typing.Optional[typing.Pattern]]:
84-
"""
85-
Normalise and validate a regular expression-like input.
86-
87-
Returns a 2-tuple with a pattern string and a compiled regular expression.
88-
"""
89-
if pattern is None:
90-
return (None, None)
86+
if isinstance(other, Union):
87+
any_of += other.any_of
88+
else:
89+
any_of += [other]
9190

92-
if isinstance(pattern, str):
93-
pattern_regex = re.compile(pattern)
94-
else:
95-
pattern_regex = pattern
96-
97-
return pattern_regex.pattern, pattern_regex
91+
return Union(any_of=any_of)
9892

9993

10094
class String(Field):
@@ -133,9 +127,18 @@ def __init__(
133127
self.trim_whitespace = trim_whitespace
134128
self.max_length = max_length
135129
self.min_length = min_length
136-
self.pattern, self.pattern_regex = normalize_regex(pattern)
137130
self.format = format
138131

132+
if pattern is None:
133+
self.pattern = None
134+
self.pattern_regex = None
135+
elif isinstance(pattern, str):
136+
self.pattern = pattern
137+
self.pattern_regex = re.compile(pattern)
138+
else:
139+
self.pattern = pattern.pattern
140+
self.pattern_regex = pattern
141+
139142
def validate(self, value: typing.Any, *, strict: bool = False) -> typing.Any:
140143
if value is None and self.allow_null:
141144
return None
@@ -343,7 +346,7 @@ def validate(self, value: typing.Any, *, strict: bool = False) -> typing.Any:
343346

344347
try:
345348
value = self.coerce_values[value]
346-
except KeyError:
349+
except (KeyError, TypeError):
347350
raise self.validation_error("type")
348351

349352
return value

typesystem/json_schema.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
"""
2+
Provides 'typesystem.from_json_schema()' and 'typesystem.to_json_schema()'.
3+
"""
14
import re
25
import typing
36

@@ -13,6 +16,7 @@
1316
Field,
1417
Float,
1518
Integer,
19+
Number,
1620
Object,
1721
String,
1822
Union,
@@ -47,6 +51,62 @@
4751
}
4852

4953

54+
definitions = SchemaDefinitions()
55+
56+
JSONSchema = (
57+
Object(
58+
properties={
59+
"$ref": String(),
60+
"type": String() | Array(items=String()),
61+
"enum": Array(unique_items=True, min_items=1),
62+
"definitions": Object(
63+
additional_properties=Reference("JSONSchema", definitions=definitions)
64+
),
65+
# String
66+
"minLength": Integer(minimum=0),
67+
"maxLength": Integer(minimum=0),
68+
"pattern": String(format="regex"),
69+
"format": String(),
70+
# Numeric
71+
"minimum": Number(),
72+
"maximum": Number(),
73+
"exclusiveMinimum": Number(),
74+
"exclusiveMaximum": Number(),
75+
"multipleOf": Number(exclusive_minimum=0),
76+
# Object
77+
"properties": Object(
78+
additional_properties=Reference("JSONSchema", definitions=definitions)
79+
),
80+
"minProperties": Integer(minimum=0),
81+
"maxProperties": Integer(minimum=0),
82+
"patternProperties": Object(
83+
additional_properties=Reference("JSONSchema", definitions=definitions)
84+
),
85+
"additionalProperties": (
86+
Reference("JSONSchema", definitions=definitions) | Boolean()
87+
),
88+
"required": Array(items=String(), unique_items=True),
89+
# Array
90+
"items": (
91+
Reference("JSONSchema", definitions=definitions)
92+
| Array(
93+
items=Reference("JSONSchema", definitions=definitions), min_items=1
94+
)
95+
),
96+
"additionalItems": (
97+
Reference("JSONSchema", definitions=definitions) | Boolean()
98+
),
99+
"minItems": Integer(minimum=0),
100+
"maxItems": Integer(minimum=0),
101+
"uniqueItems": Boolean(),
102+
}
103+
)
104+
| Boolean()
105+
)
106+
107+
definitions["JSONSchema"] = JSONSchema
108+
109+
50110
def from_json_schema(
51111
data: typing.Union[bool, dict], definitions: SchemaDefinitions = None
52112
) -> Field:

0 commit comments

Comments
 (0)