1import decimal
2from .draft04 import CodeGeneratorDraft04, JSON_TYPE_TO_PYTHON_TYPE
3from .exceptions import JsonSchemaDefinitionException
4from .generator import VALIDATION_EXCEPTIONS, enforce_list
5
6
7class CodeGeneratorDraft06(CodeGeneratorDraft04):
8 FORMAT_REGEXS = dict(CodeGeneratorDraft04.FORMAT_REGEXS, **{
9 'json-pointer': r'^(/(([^/~])|(~[01]))*)*\Z',
10 'uri-reference': r'^(\w+:(\/?\/?))?[^#\\\s]*(#[^\\\s]*)?\Z',
11 'uri-template': (
12 r'^(?:(?:[^\x00-\x20\"\'<>%\\^`{|}]|%[0-9a-f]{2})|'
13 r'\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+'
14 r'(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+'
15 r'(?::[1-9][0-9]{0,3}|\*)?)*\})*\Z'
16 ),
17 })
18
19 def __init__(
20 self,
21 definition,
22 resolver=None,
23 formats={},
24 use_default=True,
25 use_formats=True,
26 detailed_exceptions=True,
27 fast_fail=True,
28 ):
29 super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions, fast_fail)
30 self._json_keywords_to_function.update((
31 ('exclusiveMinimum', self.generate_exclusive_minimum),
32 ('exclusiveMaximum', self.generate_exclusive_maximum),
33 ('propertyNames', self.generate_property_names),
34 ('contains', self.generate_contains),
35 ('const', self.generate_const),
36 ))
37
38 def _generate_func_code_block(self, definition):
39 if isinstance(definition, bool):
40 return self.generate_boolean_schema()
41 elif '$ref' in definition:
42 # needed because ref overrides any sibling keywords
43 return self.generate_ref()
44 return self.run_generate_functions(definition)
45
46 def generate_boolean_schema(self):
47 """
48 Means that schema can be specified by boolean.
49 True means everything is valid, False everything is invalid.
50 """
51 if self._definition is True:
52 self.l('pass')
53 if self._definition is False:
54 self.exc('{name} must not be there')
55
56 def generate_type(self):
57 """
58 Validation of type. Can be one type or list of types.
59
60 Since draft 06 a float without fractional part is an integer.
61
62 .. code-block:: python
63
64 {'type': 'string'}
65 {'type': ['string', 'number']}
66 """
67 types = enforce_list(self._definition['type'])
68 try:
69 python_types = ', '.join(JSON_TYPE_TO_PYTHON_TYPE[t] for t in types)
70 except KeyError as exc:
71 raise JsonSchemaDefinitionException('Unknown type') from exc
72
73 extra = ''
74
75 if 'integer' in types:
76 extra += ' and not (isinstance({variable}, float) and {variable}.is_integer())'.format(
77 variable=self._variable,
78 )
79
80 if ('number' in types or 'integer' in types) and 'boolean' not in types:
81 extra += ' or isinstance({variable}, bool)'.format(variable=self._variable)
82
83 with self.l('if not isinstance({variable}, ({})){}:', python_types, extra):
84 self.exc('{name} must be {}', ' or '.join(types), rule='type')
85
86 def generate_minimum(self):
87 """
88 Since draft 06 exclusiveMinimum is a number validated on its own,
89 so minimum is always inclusive.
90 """
91 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
92 if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)):
93 raise JsonSchemaDefinitionException('minimum must be a number')
94 with self.l('if {variable} < {minimum}:'):
95 self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum')
96
97 def generate_maximum(self):
98 """
99 Since draft 06 exclusiveMaximum is a number validated on its own,
100 so maximum is always inclusive.
101 """
102 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
103 if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)):
104 raise JsonSchemaDefinitionException('maximum must be a number')
105 with self.l('if {variable} > {maximum}:'):
106 self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum')
107
108 def generate_exclusive_minimum(self):
109 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
110 if not isinstance(self._definition['exclusiveMinimum'], (int, float, decimal.Decimal)):
111 raise JsonSchemaDefinitionException('exclusiveMinimum must be an integer, a float or a decimal')
112 with self.l('if {variable} <= {exclusiveMinimum}:'):
113 self.exc('{name} must be bigger than {exclusiveMinimum}', rule='exclusiveMinimum')
114
115 def generate_exclusive_maximum(self):
116 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
117 if not isinstance(self._definition['exclusiveMaximum'], (int, float, decimal.Decimal)):
118 raise JsonSchemaDefinitionException('exclusiveMaximum must be an integer, a float or a decimal')
119 with self.l('if {variable} >= {exclusiveMaximum}:'):
120 self.exc('{name} must be smaller than {exclusiveMaximum}', rule='exclusiveMaximum')
121
122 def generate_property_names(self):
123 """
124 Means that keys of object must to follow this definition.
125
126 .. code-block:: python
127
128 {
129 'propertyNames': {
130 'maxLength': 3,
131 },
132 }
133
134 Valid keys of object for this definition are foo, bar, ... but not foobar for example.
135 """
136 property_names_definition = self._definition.get('propertyNames', {})
137 if property_names_definition is True:
138 pass
139 elif property_names_definition is False:
140 self.create_variable_is_dict()
141 with self.l('if {variable}_is_dict and {variable}:'):
142 self.exc('{name} must not be there', rule='propertyNames')
143 else:
144 self.create_variable_is_dict()
145 with self.l('if {variable}_is_dict:'):
146 self.create_variable_with_length()
147 with self.l('if {variable}_len != 0:'):
148 self.l('{variable}_property_names = True')
149 with self.l('for {variable}_key in {variable}:'):
150 with self.l('try:'):
151 code_len = len(self._code)
152 with self.trial_validation():
153 self.generate_func_code_block(
154 property_names_definition,
155 '{}_key'.format(self._variable),
156 self._variable_name,
157 clear_variables=True,
158 )
159 if len(self._code) == code_len:
160 self.l('pass')
161 with self.l('except {}:', VALIDATION_EXCEPTIONS):
162 self.l('{variable}_property_names = False')
163 with self.l('if not {variable}_property_names:'):
164 self.exc('{name} must be named by propertyName definition', rule='propertyNames')
165
166 def generate_contains(self):
167 """
168 Means that array must contain at least one defined item.
169
170 .. code-block:: python
171
172 {
173 'contains': {
174 'type': 'number',
175 },
176 }
177
178 Valid array is any with at least one number.
179 """
180 self.create_variable_is_list()
181 with self.l('if {variable}_is_list:'):
182 contains_definition = self._definition['contains']
183
184 if contains_definition is False:
185 self.exc('{name} is always invalid', rule='contains')
186 elif contains_definition is True:
187 with self.l('if not {variable}:'):
188 self.exc('{name} must not be empty', rule='contains')
189 else:
190 self.l('{variable}_contains = False')
191 with self.l('for {variable}_key in {variable}:'):
192 with self.l('try:'):
193 with self.trial_validation():
194 self.generate_func_code_block(
195 contains_definition,
196 '{}_key'.format(self._variable),
197 self._variable_name,
198 clear_variables=True,
199 )
200 self.l('{variable}_contains = True')
201 self.l('break')
202 self.l('except {}: pass', VALIDATION_EXCEPTIONS)
203
204 with self.l('if not {variable}_contains:'):
205 self.exc('{name} must contain one of contains definition', rule='contains')
206
207 def generate_const(self):
208 """
209 Means that value is valid when is equeal to const definition.
210
211 .. code-block:: python
212
213 {
214 'const': 42,
215 }
216
217 Only valid value is 42 in this example.
218 """
219 const = self._definition['const']
220 match = self._enum_value_matches(self._variable, const)
221 with self.l('if not ({}):', match):
222 self.exc('{name} must be same as const definition: {definition_rule}', rule='const')