Skip to content

Commit b536fff

Browse files
committed
Add passing implementation of invalid_schemas.json
0 parents  commit b536fff

File tree

7 files changed

+266
-0
lines changed

7 files changed

+266
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/venv
2+
__pycache__

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "json-typedef-spec"]
2+
path = json-typedef-spec
3+
url = https://github.com/jsontypedef/json-typedef-spec.git

json-typedef-spec

Submodule json-typedef-spec added at 71ca275

jtd/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .schema import Schema

jtd/schema.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import dataclasses
2+
import enum
3+
from typing import Any, Dict, List, Optional
4+
5+
class Form(enum.Enum):
6+
EMPTY = enum.auto()
7+
REF = enum.auto()
8+
TYPE = enum.auto()
9+
ENUM = enum.auto()
10+
ELEMENTS = enum.auto()
11+
PROPERTIES = enum.auto()
12+
VALUES = enum.auto()
13+
DISCRIMINATOR = enum.auto()
14+
15+
@dataclasses.dataclass
16+
class Schema:
17+
metadata: Optional[Dict[str, Any]]
18+
nullable: Optional[bool]
19+
definitions: Optional[Dict[str, 'Schema']]
20+
ref: Optional[str]
21+
type: Optional[str]
22+
enum: Optional[List[str]]
23+
elements: Optional['Schema']
24+
properties: Optional[Dict[str, 'Schema']]
25+
optional_properties: Optional[Dict[str, 'Schema']]
26+
additional_properties: Optional[bool]
27+
values: Optional['Schema']
28+
discriminator: Optional[str]
29+
mapping: Optional[Dict[str, 'Schema']]
30+
31+
KEYWORDS = [
32+
"metadata",
33+
"nullable",
34+
"definitions",
35+
"ref",
36+
"type",
37+
"enum",
38+
"elements",
39+
"properties",
40+
"optionalProperties",
41+
"additionalProperties",
42+
"values",
43+
"discriminator",
44+
"mapping",
45+
]
46+
47+
TYPE_VALUES = [
48+
'boolean',
49+
'int8',
50+
'uint8',
51+
'int16',
52+
'uint16',
53+
'int32',
54+
'uint32',
55+
'float32',
56+
'float64',
57+
'string',
58+
'timestamp',
59+
]
60+
61+
VALID_FORMS = [
62+
# Empty form
63+
[False, False, False, False, False, False, False, False, False, False],
64+
# Ref form
65+
[True, False, False, False, False, False, False, False, False, False],
66+
# Type form
67+
[False, True, False, False, False, False, False, False, False, False],
68+
# Enum form
69+
[False, False, True, False, False, False, False, False, False, False],
70+
# Elements form
71+
[False, False, False, True, False, False, False, False, False, False],
72+
# Properties form -- properties or optional properties or both, and
73+
# never additional properties on its own
74+
[False, False, False, False, True, False, False, False, False, False],
75+
[False, False, False, False, False, True, False, False, False, False],
76+
[False, False, False, False, True, True, False, False, False, False],
77+
[False, False, False, False, True, False, True, False, False, False],
78+
[False, False, False, False, False, True, True, False, False, False],
79+
[False, False, False, False, True, True, True, False, False, False],
80+
# Values form
81+
[False, False, False, False, False, False, False, True, False, False],
82+
# Discriminator form
83+
[False, False, False, False, False, False, False, False, True, True],
84+
]
85+
86+
@classmethod
87+
def from_dict(cls, dict: Dict[str, Any]) -> 'Schema':
88+
definitions = None
89+
if "definitions" in dict:
90+
definitions = { k: cls.from_dict(v) for k, v in dict["definitions"].items() }
91+
92+
elements = None
93+
if "elements" in dict:
94+
elements = cls.from_dict(dict["elements"])
95+
96+
properties = None
97+
if "properties" in dict:
98+
properties = { k: cls.from_dict(v) for k, v in dict["properties"].items() }
99+
100+
optional_properties = None
101+
if "optionalProperties" in dict:
102+
optional_properties = { k: cls.from_dict(v) for k, v in dict["optionalProperties"].items() }
103+
104+
values = None
105+
if "values" in dict:
106+
values = cls.from_dict(dict["values"])
107+
108+
mapping = None
109+
if "mapping" in dict:
110+
mapping = { k: cls.from_dict(v) for k, v in dict["mapping"].items() }
111+
112+
for k in dict.keys():
113+
if k not in cls.KEYWORDS:
114+
raise AttributeError("illegal keyword")
115+
116+
return Schema(
117+
metadata=dict.get("metadata"),
118+
nullable=dict.get("nullable"),
119+
definitions=definitions,
120+
ref=dict.get("ref"),
121+
type=dict.get("type"),
122+
enum=dict.get("enum"),
123+
elements=elements,
124+
properties=properties,
125+
optional_properties=optional_properties,
126+
additional_properties=dict.get("additionalProperties"),
127+
values=values,
128+
discriminator=dict.get("discriminator"),
129+
mapping=mapping,
130+
)
131+
132+
def validate(self, root=None):
133+
if root is None:
134+
root = self
135+
136+
if self.definitions is not None:
137+
if self is not root:
138+
raise TypeError("non-root definitions")
139+
140+
for v in self.definitions.values():
141+
v.validate(root)
142+
143+
if self.nullable is not None and type(self.nullable) is not bool:
144+
raise TypeError("nullable not bool")
145+
146+
if self.ref is not None:
147+
if type(self.ref) is not str:
148+
raise TypeError("ref not string")
149+
150+
if type(root.definitions) is not dict:
151+
raise TypeError("ref but no definitions")
152+
153+
if self.ref not in root.definitions:
154+
raise TypeError("ref to non-existent definition")
155+
156+
if self.type is not None and self.type not in self.TYPE_VALUES:
157+
raise TypeError("type not valid string value")
158+
159+
if self.enum is not None:
160+
if type(self.enum) is not list:
161+
raise TypeError("enum not list")
162+
163+
if len(self.enum) == 0:
164+
raise TypeError("enum is empty")
165+
166+
for v in self.enum:
167+
if type(v) is not str:
168+
raise TypeError("enum not list of strings")
169+
170+
if len(self.enum) != len(set(self.enum)):
171+
raise TypeError("enum contains duplicates")
172+
173+
if self.elements is not None:
174+
self.elements.validate(root)
175+
176+
if self.properties is not None:
177+
for v in self.properties.values():
178+
v.validate(root)
179+
180+
if self.optional_properties is not None:
181+
for v in self.optional_properties.values():
182+
v.validate(root)
183+
184+
if self.properties is not None and self.optional_properties is not None:
185+
if set(self.properties).intersection(self.optional_properties):
186+
raise TypeError("properties shares keys with optional_properties")
187+
188+
if self.additional_properties is not None:
189+
if type(self.additional_properties) is not str:
190+
raise TypeError("additional_properties not string")
191+
192+
if self.values is not None:
193+
self.values.validate(root)
194+
195+
if self.discriminator is not None:
196+
if type(self.discriminator) is not str:
197+
raise TypeError("discriminator not string")
198+
199+
if self.mapping is not None:
200+
for v in self.mapping.values():
201+
v.validate(root)
202+
203+
if v.nullable:
204+
raise TypeError("mapping value is nullable")
205+
206+
if v.form() != Form.PROPERTIES:
207+
raise TypeError("mapping value not of properties form")
208+
209+
if self.discriminator in (v.properties or {}):
210+
raise TypeError("mapping properties redefines discriminator")
211+
212+
if self.discriminator in (v.optional_properties or {}):
213+
raise TypeError("mapping optional_properties redefines discriminator")
214+
215+
form_signature = [
216+
self.ref is not None,
217+
self.type is not None,
218+
self.enum is not None,
219+
self.elements is not None,
220+
self.properties is not None,
221+
self.optional_properties is not None,
222+
self.additional_properties is not None,
223+
self.values is not None,
224+
self.discriminator is not None,
225+
self.mapping is not None,
226+
]
227+
228+
if form_signature not in self.VALID_FORMS:
229+
raise TypeError("invalid form")
230+
231+
def form(self) -> Form:
232+
if self.ref is not None:
233+
return Form.REF
234+
if self.type is not None:
235+
return Form.TYPE
236+
if self.enum is not None:
237+
return Form.ENUM
238+
if self.properties is not None or self.optional_properties is not None:
239+
return Form.PROPERTIES
240+
if self.values is not None:
241+
return Form.VALUES
242+
if self.discriminator is not None:
243+
return Form.DISCRIMINATOR
244+
return Form.EMPTY

jtd/tests/__init__.py

Whitespace-only changes.

jtd/tests/test_schema.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import unittest
2+
import jtd
3+
import json
4+
5+
class TestSchema(unittest.TestCase):
6+
def test_invalid_schemas(self):
7+
with open("json-typedef-spec/tests/invalid_schemas.json") as f:
8+
invalid_schemas = json.loads(f.read())
9+
for k, v in invalid_schemas.items():
10+
with self.subTest(k):
11+
with self.assertRaises(BaseException) as c:
12+
schema = jtd.Schema.from_dict(v)
13+
schema.validate()
14+
15+
self.assertIsInstance(c.exception, (AttributeError, TypeError))

0 commit comments

Comments
 (0)