blob: 37a4488579aacd5d3f75a4eff1673827f516e81b [file] [log] [blame]
Hung-Te Lin1990b742017-08-09 17:34:57 +08001# Copyright 2012 The Chromium OS Authors. All rights reserved.
Ricky Liang1b72ccf2013-03-01 10:23:01 +08002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# pylint: disable=W0212, W0622
6
7"""A function to create a schema tree from the given schema expression.
8
9For example:
10
11 1. This is the schema of the encoded_fields in component database.
12
13 Dict('encoded_fields', Scalar('encoded_field', str),
14 Dict('encoded_indices', Scalar('encoded_index', int),
15 Dict('component_classes', Scalar('component_class', str),
16 AnyOf('component_names', [
17 Scalar('component_name', str),
18 List('list_of_component_names', Scalar('component_name', str)),
19 Scalar('none', type(None))
20 ])
21 )
22 )
23 )
24
25 2. This is the schema of the pattern in component database.
26
27 List('pattern',
28 Dict('pattern_field', key_type=Scalar('encoded_index', str),
29 value_type=Scalar('bit_offset', int))
30 )
31
32 3. This is the schema of the components in component database.
33
34 Dict('components', Scalar('component_class', str),
35 Dict('component_names', Scalar('component_name', str),
36 FixedDict('component_attributes',
37 items={
38 'value': AnyOf('probed_value', [
39 Scalar('probed_value', str),
40 List('list_of_probed_values', Scalar('probed_value', str))
41 ])
42 },
43 optional_items={
44 'labels': List('list_of_labels', Scalar('label', str))
45 }
46 )
47 )
48 )
49"""
50
51import copy
Chih-wei Ning5fbaf442017-08-15 21:34:53 +080052import jsonschema
Joel Kitchinga8be7962016-06-08 17:35:01 +080053from .type_utils import MakeList
Dean Liao452c6ee2013-03-11 16:23:33 +080054
Ricky Liang1b72ccf2013-03-01 10:23:01 +080055
56class SchemaException(Exception):
57 pass
58
59
60class BaseType(object):
61 """Base type class for schema classes.
62 """
Hung-Te Lin56b18402015-01-16 14:52:30 +080063
Ricky Liang1b72ccf2013-03-01 10:23:01 +080064 def __init__(self, label):
65 self._label = label
66
Dean Liao604e62b2013-03-11 19:12:50 +080067 def __repr__(self):
68 return 'BaseType(%r)' % self._label
69
Ricky Liang1b72ccf2013-03-01 10:23:01 +080070 def Validate(self, data):
71 raise NotImplementedError
72
73
74class Scalar(BaseType):
75 """Scalar schema class.
76
77 Attributes:
78 label: A human-readable string to describe this Scalar.
79 element_type: The Python type of this Scalar. Cannot be a iterable type.
Jon Salz05fffde2014-07-14 12:56:47 +080080 choices: A set of allowable choices for the scalar, or None to allow
81 any values of the given type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +080082
83 Raises:
84 SchemaException if argument format is incorrect.
85 """
Hung-Te Lin56b18402015-01-16 14:52:30 +080086
Jon Salz05fffde2014-07-14 12:56:47 +080087 def __init__(self, label, element_type, choices=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +080088 super(Scalar, self).__init__(label)
89 if getattr(element_type, '__iter__', None):
Dean Liao604e62b2013-03-11 19:12:50 +080090 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +080091 'element_type %r of Scalar %r is not a scalar type' % (element_type,
92 label))
Ricky Liang1b72ccf2013-03-01 10:23:01 +080093 self._element_type = element_type
Jon Salz05fffde2014-07-14 12:56:47 +080094 self._choices = set(choices) if choices else None
95
Dean Liao604e62b2013-03-11 19:12:50 +080096 def __repr__(self):
Jon Salz05fffde2014-07-14 12:56:47 +080097 return 'Scalar(%r, %r%s)' % (
Hung-Te Lin56b18402015-01-16 14:52:30 +080098 self._label, self._element_type,
99 ', choices=%r' % sorted(self._choices) if self._choices else '')
Dean Liao604e62b2013-03-11 19:12:50 +0800100
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800101 def Validate(self, data):
102 """Validates the given data against the Scalar schema.
103
104 It checks if the data's type matches the Scalar's element type. Also, it
105 checks if the data's value matches the Scalar's value if the required value
106 is specified.
107
108 Args:
109 data: A Python data structure to be validated.
110
111 Raises:
112 SchemaException if validation fails.
113 """
114 if not isinstance(data, self._element_type):
115 raise SchemaException('Type mismatch on %r: expected %r, got %r' %
116 (data, self._element_type, type(data)))
Jon Salz05fffde2014-07-14 12:56:47 +0800117 if self._choices and data not in self._choices:
118 raise SchemaException('Value mismatch on %r: expected one of %r' %
119 (data, sorted(self._choices)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800120
121
122class Dict(BaseType):
123 """Dict schema class.
124
125 This schema class is used to verify simple dict. Only the key type and value
126 type are validated.
127
128 Attributes:
129 label: A human-readable string to describe this Scalar.
Ricky Liangf5386b32013-03-11 16:40:45 +0800130 key_type: A schema object indicating the schema of the keys of this Dict. It
131 can be a Scalar or an AnyOf with possible values being all Scalars.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800132 value_type: A schema object indicating the schema of the values of this
133 Dict.
134
135 Raises:
136 SchemaException if argument format is incorrect.
137 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800138
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800139 def __init__(self, label, key_type, value_type):
140 super(Dict, self).__init__(label)
Ricky Liangf5386b32013-03-11 16:40:45 +0800141 if not (isinstance(key_type, Scalar) or
Hung-Te Lin56b18402015-01-16 14:52:30 +0800142 (isinstance(key_type, AnyOf) and
143 key_type.CheckTypeOfPossibleValues(Scalar))):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800144 raise SchemaException('key_type %r of Dict %r is not Scalar' %
145 (key_type, self._label))
146 self._key_type = key_type
147 if not isinstance(value_type, BaseType):
148 raise SchemaException('value_type %r of Dict %r is not Schema object' %
149 (value_type, self._label))
150 self._value_type = value_type
151
Dean Liao604e62b2013-03-11 19:12:50 +0800152 def __repr__(self):
153 return 'Dict(%r, key_type=%r, value_type=%r)' % (self._label,
154 self._key_type,
155 self._value_type)
156
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800157 def Validate(self, data):
158 """Validates the given data against the Dict schema.
159
160 It checks that all the keys in data matches the schema defined by key_type,
161 and all the values in data matches the schema defined by value_type.
162
163 Args:
164 data: A Python data structure to be validated.
165
166 Raises:
167 SchemaException if validation fails.
168 """
169 if not isinstance(data, dict):
170 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
171 (self._label, type(data)))
172 for k, v in data.iteritems():
173 self._key_type.Validate(k)
174 self._value_type.Validate(v)
175
176
177class FixedDict(BaseType):
178 """FixedDict schema class.
179
180 FixedDict is a Dict with predefined allowed keys. And each key corresponds to
181 a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues
182 in XML.
183
184 An example FixedDict schema:
185 FixedDict('foo',
186 items={
187 'a': Scalar('bar', str),
188 'b': Scalar('buz', int)
189 }, optional_items={
190 'c': Scalar('boo', int)
191 })
192
193 Attributes:
194 label: A human-readable string to describe this dict.
195 items: A dict of required items that must be specified.
196 optional_items: A dict of optional items.
197
198 Raises:
199 SchemaException if argument format is incorrect.
200 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800201
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800202 def __init__(self, label, items=None, optional_items=None):
203 super(FixedDict, self).__init__(label)
204 if items and not isinstance(items, dict):
205 raise SchemaException('items of FixedDict %r should be a dict' %
206 self._label)
207 self._items = copy.deepcopy(items) if items is not None else {}
208 if optional_items and not isinstance(optional_items, dict):
209 raise SchemaException('optional_items of FixedDict %r should be a dict' %
210 self._label)
211 self._optional_items = (
212 copy.deepcopy(optional_items) if optional_items is not None else {})
213
Dean Liao604e62b2013-03-11 19:12:50 +0800214 def __repr__(self):
215 return 'FixedDict(%r, items=%r, optional_items=%r)' % (self._label,
216 self._items,
217 self._optional_items)
Hung-Te Lin56b18402015-01-16 14:52:30 +0800218
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800219 def Validate(self, data):
220 """Validates the given data and all its key-value pairs against the Dict
221 schema.
222
223 If a key of Dict's type is required, then it must exist in the data's keys.
224
225 Args:
226 data: A Python data structure to be validated.
227
228 Raises:
229 SchemaException if validation fails.
230 """
231 if not isinstance(data, dict):
232 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
233 (self._label, type(data)))
234 data_key_list = data.keys()
235 # Check that every key-value pair in items exists in data
236 for key, value_schema in self._items.iteritems():
237 if key not in data:
238 raise SchemaException(
239 'Required item %r does not exist in FixedDict %r' %
240 (key, data))
241 value_schema.Validate(data[key])
242 data_key_list.remove(key)
243 # Check that all the remaining unmatched key-value pairs matches any
244 # definition in items or optional_items.
245 for key, value_schema in self._optional_items.iteritems():
246 if key not in data:
247 continue
248 value_schema.Validate(data[key])
249 data_key_list.remove(key)
250 if data_key_list:
251 raise SchemaException('Keys %r are undefined in FixedDict %r' %
252 (data_key_list, self._label))
253
254
Chih-wei Ning5fbaf442017-08-15 21:34:53 +0800255class JSONSchemaDict(BaseType):
256 """JSON schema class.
257
258 This schema class allows mixing JSON schema with other schema types.
259
260 Attributes:
261 label: A human-readable string to describe this JSON schema.
262 schema: a JSON schema object.
263
264 Raises:
265 SchemaException if given schema is invalid.
266 ValidationError if argument format is incorrect.
267 """
268 def __init__(self, label, schema):
269 super(JSONSchemaDict, self).__init__(label)
270 self._label = label
271 jsonschema.Draft4Validator.check_schema(schema)
272 self._schema = schema
273
274 def __repr__(self):
275 return 'JSONSchemaDict(%r, %r)' % (self._label, self._schema)
276
277 def Validate(self, data):
278 jsonschema.validate(data, self._schema)
279
280
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800281class List(BaseType):
282 """List schema class.
283
284 Attributes:
285 label: A string to describe this list.
Dean Liao604e62b2013-03-11 19:12:50 +0800286 element_type: Optional schema object to validate the elements of the list.
287 Default None means no validation of elements' type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800288
289 Raises:
290 SchemaException if argument format is incorrect.
291 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800292
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800293 def __init__(self, label, element_type=None):
294 super(List, self).__init__(label)
Ricky Liang3e5342b2013-03-08 12:16:25 +0800295 if element_type and not isinstance(element_type, BaseType):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800296 raise SchemaException(
297 'element_type %r of List %r is not a Schema object' %
298 (element_type, self._label))
Ricky Liang3e5342b2013-03-08 12:16:25 +0800299 self._element_type = copy.deepcopy(element_type)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800300
Dean Liao604e62b2013-03-11 19:12:50 +0800301 def __repr__(self):
302 return 'List(%r, %r)' % (self._label, self._element_type)
303
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800304 def Validate(self, data):
Ricky Liang3e5342b2013-03-08 12:16:25 +0800305 """Validates the given data and all its elements against the List schema.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800306
307 Args:
308 data: A Python data structure to be validated.
309
310 Raises:
311 SchemaException if validation fails.
312 """
313 if not isinstance(data, list):
314 raise SchemaException('Type mismatch on %r: expected list, got %r' %
Hung-Te Lin56b18402015-01-16 14:52:30 +0800315 (self._label, type(data)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800316 if self._element_type:
317 for data_value in data:
318 self._element_type.Validate(data_value)
319
320
Ricky Liang3e5342b2013-03-08 12:16:25 +0800321class Tuple(BaseType):
322 """Tuple schema class.
323
324 Comparing to List, the Tuple schema makes sure that every element exactly
325 matches the defined position and schema.
326
327 Attributes:
328 label: A string to describe this tuple.
329 element_types: Optional list or tuple schema object to describe the
330 types of the Tuple.
331
332 Raises:
333 SchemaException if argument format is incorrect.
334 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800335
Ricky Liang3e5342b2013-03-08 12:16:25 +0800336 def __init__(self, label, element_types=None):
337 super(Tuple, self).__init__(label)
338 if (element_types and
339 (not isinstance(element_types, (tuple, list))) or
340 (not all([isinstance(x, BaseType)] for x in element_types))):
341 raise SchemaException(
342 'element_types %r of Tuple %r is not a tuple or list' %
343 (element_types, self._label))
344 self._element_types = copy.deepcopy(element_types)
345
Dean Liao604e62b2013-03-11 19:12:50 +0800346 def __repr__(self):
347 return 'Tuple(%r, %r)' % (self._label, self._element_types)
348
Ricky Liang3e5342b2013-03-08 12:16:25 +0800349 def Validate(self, data):
350 """Validates the given data and all its elements against the Tuple schema.
351
352 Args:
353 data: A Python data structure to be validated.
354
355 Raises:
356 SchemaException if validation fails.
357 """
358 if not isinstance(data, tuple):
359 raise SchemaException('Type mismatch on %r: expected tuple, got %r' %
360 (self._label, type(data)))
361 if self._element_types and len(self._element_types) != len(data):
362 raise SchemaException(
363 'Number of elements in tuple %r does not match that defined '
364 'in Tuple schema %r' % (str(data), self._label))
365 for data, element_type in zip(data, self._element_types):
366 element_type.Validate(data)
367
368
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800369class AnyOf(BaseType):
Dean Liao452c6ee2013-03-11 16:23:33 +0800370 """A Schema class which accepts any one of the given Schemas.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800371
372 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800373 types: A list of Schema objects to be matched.
374 label: An optional string to describe this AnyOf type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800375 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800376
Dean Liao604e62b2013-03-11 19:12:50 +0800377 def __init__(self, types, label=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800378 super(AnyOf, self).__init__(label)
Dean Liao604e62b2013-03-11 19:12:50 +0800379 if (not isinstance(types, list) or
380 not all([isinstance(x, BaseType) for x in types])):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800381 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800382 'types in AnyOf(types=%r%s) should be a list of Schemas' %
383 (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800384 self._types = list(types)
385
386 def __repr__(self):
387 label = '' if self._label is None else ', label=%r' % self._label
388 return 'AnyOf(%r%s)' % (self._types, label)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800389
Ricky Liangf5386b32013-03-11 16:40:45 +0800390 def CheckTypeOfPossibleValues(self, schema_type):
391 """Checks if the acceptable types are of the same type as schema_type.
392
393 Args:
394 schema_type: The schema type to check against with.
395 """
Dean Liao604e62b2013-03-11 19:12:50 +0800396 return all([isinstance(k, schema_type) for k in self._types])
Ricky Liangf5386b32013-03-11 16:40:45 +0800397
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800398 def Validate(self, data):
Dean Liao604e62b2013-03-11 19:12:50 +0800399 """Validates if the given data matches any schema in types
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800400
401 Args:
402 data: A Python data structue to be validated.
Dean Liao452c6ee2013-03-11 16:23:33 +0800403
404 Raises:
Dean Liao604e62b2013-03-11 19:12:50 +0800405 SchemaException if no schemas in types validates the input data.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800406 """
407 match = False
Dean Liao604e62b2013-03-11 19:12:50 +0800408 for schema_type in self._types:
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800409 try:
410 schema_type.Validate(data)
411 except SchemaException:
412 continue
413 match = True
414 break
415 if not match:
Dean Liao604e62b2013-03-11 19:12:50 +0800416 raise SchemaException('%r does not match any type in %r' % (data,
417 self._types))
Dean Liao452c6ee2013-03-11 16:23:33 +0800418
419
420class Optional(AnyOf):
421 """A Schema class which accepts either None or given Schemas.
422
423 It is a special case of AnyOf class: in addition of given schema(s), it also
424 accepts None.
425
426 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800427 types: A (or a list of) Schema object(s) to be matched.
428 label: An optional string to describe this Optional type.
Dean Liao452c6ee2013-03-11 16:23:33 +0800429 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800430
Dean Liao604e62b2013-03-11 19:12:50 +0800431 def __init__(self, types, label=None):
432 try:
433 super(Optional, self).__init__(MakeList(types), label=label)
434 except SchemaException:
435 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800436 'types in Optional(types=%r%s) should be a Schema or a list of '
437 'Schemas' % (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800438
439 def __repr__(self):
440 label = '' if self._label is None else ', label=%r' % self._label
441 return 'Optional(%r%s)' % (self._types, label)
Dean Liao452c6ee2013-03-11 16:23:33 +0800442
443 def Validate(self, data):
444 """Validates if the given data is None or matches any schema in types.
445
446 Args:
447 data: A Python data structue to be validated.
448
449 Raises:
450 SchemaException if data is not None and no schemas in types validates the
451 input data.
452 """
453 if data is None:
454 return
455 try:
456 super(Optional, self).Validate(data)
457 except SchemaException:
Dean Liao604e62b2013-03-11 19:12:50 +0800458 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800459 '%r is not None and does not match any type in %r' % (data,
460 self._types))