blob: 3b02fef5be97d5064b4185c3e6e4f01eb11fd689 [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
Joel Kitchinga8be7962016-06-08 17:35:01 +080052from .type_utils import MakeList
Dean Liao452c6ee2013-03-11 16:23:33 +080053
Chih-Wei Ning8da12ec2017-08-29 12:10:02 +080054# To simplify portability issues, validating JSON schema is optional.
55try:
56 import jsonschema
57 _HAVE_JSONSCHEMA = True
58except ImportError:
59 _HAVE_JSONSCHEMA = False
60
Ricky Liang1b72ccf2013-03-01 10:23:01 +080061
62class SchemaException(Exception):
63 pass
64
65
66class BaseType(object):
67 """Base type class for schema classes.
68 """
Hung-Te Lin56b18402015-01-16 14:52:30 +080069
Ricky Liang1b72ccf2013-03-01 10:23:01 +080070 def __init__(self, label):
71 self._label = label
72
Dean Liao604e62b2013-03-11 19:12:50 +080073 def __repr__(self):
74 return 'BaseType(%r)' % self._label
75
Ricky Liang1b72ccf2013-03-01 10:23:01 +080076 def Validate(self, data):
77 raise NotImplementedError
78
79
80class Scalar(BaseType):
81 """Scalar schema class.
82
83 Attributes:
84 label: A human-readable string to describe this Scalar.
85 element_type: The Python type of this Scalar. Cannot be a iterable type.
Jon Salz05fffde2014-07-14 12:56:47 +080086 choices: A set of allowable choices for the scalar, or None to allow
87 any values of the given type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +080088
89 Raises:
90 SchemaException if argument format is incorrect.
91 """
Hung-Te Lin56b18402015-01-16 14:52:30 +080092
Jon Salz05fffde2014-07-14 12:56:47 +080093 def __init__(self, label, element_type, choices=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +080094 super(Scalar, self).__init__(label)
95 if getattr(element_type, '__iter__', None):
Dean Liao604e62b2013-03-11 19:12:50 +080096 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +080097 'element_type %r of Scalar %r is not a scalar type' % (element_type,
98 label))
Ricky Liang1b72ccf2013-03-01 10:23:01 +080099 self._element_type = element_type
Jon Salz05fffde2014-07-14 12:56:47 +0800100 self._choices = set(choices) if choices else None
101
Dean Liao604e62b2013-03-11 19:12:50 +0800102 def __repr__(self):
Jon Salz05fffde2014-07-14 12:56:47 +0800103 return 'Scalar(%r, %r%s)' % (
Hung-Te Lin56b18402015-01-16 14:52:30 +0800104 self._label, self._element_type,
105 ', choices=%r' % sorted(self._choices) if self._choices else '')
Dean Liao604e62b2013-03-11 19:12:50 +0800106
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800107 def Validate(self, data):
108 """Validates the given data against the Scalar schema.
109
110 It checks if the data's type matches the Scalar's element type. Also, it
111 checks if the data's value matches the Scalar's value if the required value
112 is specified.
113
114 Args:
115 data: A Python data structure to be validated.
116
117 Raises:
118 SchemaException if validation fails.
119 """
120 if not isinstance(data, self._element_type):
121 raise SchemaException('Type mismatch on %r: expected %r, got %r' %
122 (data, self._element_type, type(data)))
Jon Salz05fffde2014-07-14 12:56:47 +0800123 if self._choices and data not in self._choices:
124 raise SchemaException('Value mismatch on %r: expected one of %r' %
125 (data, sorted(self._choices)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800126
127
128class Dict(BaseType):
129 """Dict schema class.
130
131 This schema class is used to verify simple dict. Only the key type and value
132 type are validated.
133
134 Attributes:
135 label: A human-readable string to describe this Scalar.
Ricky Liangf5386b32013-03-11 16:40:45 +0800136 key_type: A schema object indicating the schema of the keys of this Dict. It
137 can be a Scalar or an AnyOf with possible values being all Scalars.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800138 value_type: A schema object indicating the schema of the values of this
139 Dict.
140
141 Raises:
142 SchemaException if argument format is incorrect.
143 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800144
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800145 def __init__(self, label, key_type, value_type):
146 super(Dict, self).__init__(label)
Ricky Liangf5386b32013-03-11 16:40:45 +0800147 if not (isinstance(key_type, Scalar) or
Hung-Te Lin56b18402015-01-16 14:52:30 +0800148 (isinstance(key_type, AnyOf) and
149 key_type.CheckTypeOfPossibleValues(Scalar))):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800150 raise SchemaException('key_type %r of Dict %r is not Scalar' %
151 (key_type, self._label))
152 self._key_type = key_type
153 if not isinstance(value_type, BaseType):
154 raise SchemaException('value_type %r of Dict %r is not Schema object' %
155 (value_type, self._label))
156 self._value_type = value_type
157
Dean Liao604e62b2013-03-11 19:12:50 +0800158 def __repr__(self):
159 return 'Dict(%r, key_type=%r, value_type=%r)' % (self._label,
160 self._key_type,
161 self._value_type)
162
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800163 def Validate(self, data):
164 """Validates the given data against the Dict schema.
165
166 It checks that all the keys in data matches the schema defined by key_type,
167 and all the values in data matches the schema defined by value_type.
168
169 Args:
170 data: A Python data structure to be validated.
171
172 Raises:
173 SchemaException if validation fails.
174 """
175 if not isinstance(data, dict):
176 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
177 (self._label, type(data)))
178 for k, v in data.iteritems():
179 self._key_type.Validate(k)
180 self._value_type.Validate(v)
181
182
183class FixedDict(BaseType):
184 """FixedDict schema class.
185
186 FixedDict is a Dict with predefined allowed keys. And each key corresponds to
187 a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues
188 in XML.
189
190 An example FixedDict schema:
191 FixedDict('foo',
192 items={
193 'a': Scalar('bar', str),
194 'b': Scalar('buz', int)
195 }, optional_items={
196 'c': Scalar('boo', int)
197 })
198
199 Attributes:
200 label: A human-readable string to describe this dict.
201 items: A dict of required items that must be specified.
202 optional_items: A dict of optional items.
203
204 Raises:
205 SchemaException if argument format is incorrect.
206 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800207
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800208 def __init__(self, label, items=None, optional_items=None):
209 super(FixedDict, self).__init__(label)
210 if items and not isinstance(items, dict):
211 raise SchemaException('items of FixedDict %r should be a dict' %
212 self._label)
213 self._items = copy.deepcopy(items) if items is not None else {}
214 if optional_items and not isinstance(optional_items, dict):
215 raise SchemaException('optional_items of FixedDict %r should be a dict' %
216 self._label)
217 self._optional_items = (
218 copy.deepcopy(optional_items) if optional_items is not None else {})
219
Dean Liao604e62b2013-03-11 19:12:50 +0800220 def __repr__(self):
221 return 'FixedDict(%r, items=%r, optional_items=%r)' % (self._label,
222 self._items,
223 self._optional_items)
Hung-Te Lin56b18402015-01-16 14:52:30 +0800224
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800225 def Validate(self, data):
226 """Validates the given data and all its key-value pairs against the Dict
227 schema.
228
229 If a key of Dict's type is required, then it must exist in the data's keys.
230
231 Args:
232 data: A Python data structure to be validated.
233
234 Raises:
235 SchemaException if validation fails.
236 """
237 if not isinstance(data, dict):
238 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
239 (self._label, type(data)))
240 data_key_list = data.keys()
241 # Check that every key-value pair in items exists in data
242 for key, value_schema in self._items.iteritems():
243 if key not in data:
244 raise SchemaException(
245 'Required item %r does not exist in FixedDict %r' %
246 (key, data))
247 value_schema.Validate(data[key])
248 data_key_list.remove(key)
249 # Check that all the remaining unmatched key-value pairs matches any
250 # definition in items or optional_items.
251 for key, value_schema in self._optional_items.iteritems():
252 if key not in data:
253 continue
254 value_schema.Validate(data[key])
255 data_key_list.remove(key)
256 if data_key_list:
257 raise SchemaException('Keys %r are undefined in FixedDict %r' %
258 (data_key_list, self._label))
259
260
Chih-wei Ning5fbaf442017-08-15 21:34:53 +0800261class JSONSchemaDict(BaseType):
262 """JSON schema class.
263
264 This schema class allows mixing JSON schema with other schema types.
265
266 Attributes:
267 label: A human-readable string to describe this JSON schema.
268 schema: a JSON schema object.
269
270 Raises:
271 SchemaException if given schema is invalid.
272 ValidationError if argument format is incorrect.
273 """
274 def __init__(self, label, schema):
275 super(JSONSchemaDict, self).__init__(label)
276 self._label = label
Chih-Wei Ning8da12ec2017-08-29 12:10:02 +0800277 if _HAVE_JSONSCHEMA:
278 jsonschema.Draft4Validator.check_schema(schema)
Chih-wei Ning5fbaf442017-08-15 21:34:53 +0800279 self._schema = schema
280
281 def __repr__(self):
282 return 'JSONSchemaDict(%r, %r)' % (self._label, self._schema)
283
284 def Validate(self, data):
Chih-Wei Ning8da12ec2017-08-29 12:10:02 +0800285 if _HAVE_JSONSCHEMA:
286 jsonschema.validate(data, self._schema)
Chih-wei Ning5fbaf442017-08-15 21:34:53 +0800287
288
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800289class List(BaseType):
290 """List schema class.
291
292 Attributes:
293 label: A string to describe this list.
Dean Liao604e62b2013-03-11 19:12:50 +0800294 element_type: Optional schema object to validate the elements of the list.
295 Default None means no validation of elements' type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800296
297 Raises:
298 SchemaException if argument format is incorrect.
299 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800300
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800301 def __init__(self, label, element_type=None):
302 super(List, self).__init__(label)
Ricky Liang3e5342b2013-03-08 12:16:25 +0800303 if element_type and not isinstance(element_type, BaseType):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800304 raise SchemaException(
305 'element_type %r of List %r is not a Schema object' %
306 (element_type, self._label))
Ricky Liang3e5342b2013-03-08 12:16:25 +0800307 self._element_type = copy.deepcopy(element_type)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800308
Dean Liao604e62b2013-03-11 19:12:50 +0800309 def __repr__(self):
310 return 'List(%r, %r)' % (self._label, self._element_type)
311
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800312 def Validate(self, data):
Ricky Liang3e5342b2013-03-08 12:16:25 +0800313 """Validates the given data and all its elements against the List schema.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800314
315 Args:
316 data: A Python data structure to be validated.
317
318 Raises:
319 SchemaException if validation fails.
320 """
321 if not isinstance(data, list):
322 raise SchemaException('Type mismatch on %r: expected list, got %r' %
Hung-Te Lin56b18402015-01-16 14:52:30 +0800323 (self._label, type(data)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800324 if self._element_type:
325 for data_value in data:
326 self._element_type.Validate(data_value)
327
328
Ricky Liang3e5342b2013-03-08 12:16:25 +0800329class Tuple(BaseType):
330 """Tuple schema class.
331
332 Comparing to List, the Tuple schema makes sure that every element exactly
333 matches the defined position and schema.
334
335 Attributes:
336 label: A string to describe this tuple.
337 element_types: Optional list or tuple schema object to describe the
338 types of the Tuple.
339
340 Raises:
341 SchemaException if argument format is incorrect.
342 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800343
Ricky Liang3e5342b2013-03-08 12:16:25 +0800344 def __init__(self, label, element_types=None):
345 super(Tuple, self).__init__(label)
346 if (element_types and
347 (not isinstance(element_types, (tuple, list))) or
348 (not all([isinstance(x, BaseType)] for x in element_types))):
349 raise SchemaException(
350 'element_types %r of Tuple %r is not a tuple or list' %
351 (element_types, self._label))
352 self._element_types = copy.deepcopy(element_types)
353
Dean Liao604e62b2013-03-11 19:12:50 +0800354 def __repr__(self):
355 return 'Tuple(%r, %r)' % (self._label, self._element_types)
356
Ricky Liang3e5342b2013-03-08 12:16:25 +0800357 def Validate(self, data):
358 """Validates the given data and all its elements against the Tuple schema.
359
360 Args:
361 data: A Python data structure to be validated.
362
363 Raises:
364 SchemaException if validation fails.
365 """
366 if not isinstance(data, tuple):
367 raise SchemaException('Type mismatch on %r: expected tuple, got %r' %
368 (self._label, type(data)))
369 if self._element_types and len(self._element_types) != len(data):
370 raise SchemaException(
371 'Number of elements in tuple %r does not match that defined '
372 'in Tuple schema %r' % (str(data), self._label))
373 for data, element_type in zip(data, self._element_types):
374 element_type.Validate(data)
375
376
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800377class AnyOf(BaseType):
Dean Liao452c6ee2013-03-11 16:23:33 +0800378 """A Schema class which accepts any one of the given Schemas.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800379
380 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800381 types: A list of Schema objects to be matched.
382 label: An optional string to describe this AnyOf type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800383 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800384
Dean Liao604e62b2013-03-11 19:12:50 +0800385 def __init__(self, types, label=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800386 super(AnyOf, self).__init__(label)
Dean Liao604e62b2013-03-11 19:12:50 +0800387 if (not isinstance(types, list) or
388 not all([isinstance(x, BaseType) for x in types])):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800389 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800390 'types in AnyOf(types=%r%s) should be a list of Schemas' %
391 (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800392 self._types = list(types)
393
394 def __repr__(self):
395 label = '' if self._label is None else ', label=%r' % self._label
396 return 'AnyOf(%r%s)' % (self._types, label)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800397
Ricky Liangf5386b32013-03-11 16:40:45 +0800398 def CheckTypeOfPossibleValues(self, schema_type):
399 """Checks if the acceptable types are of the same type as schema_type.
400
401 Args:
402 schema_type: The schema type to check against with.
403 """
Dean Liao604e62b2013-03-11 19:12:50 +0800404 return all([isinstance(k, schema_type) for k in self._types])
Ricky Liangf5386b32013-03-11 16:40:45 +0800405
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800406 def Validate(self, data):
Dean Liao604e62b2013-03-11 19:12:50 +0800407 """Validates if the given data matches any schema in types
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800408
409 Args:
410 data: A Python data structue to be validated.
Dean Liao452c6ee2013-03-11 16:23:33 +0800411
412 Raises:
Dean Liao604e62b2013-03-11 19:12:50 +0800413 SchemaException if no schemas in types validates the input data.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800414 """
415 match = False
Dean Liao604e62b2013-03-11 19:12:50 +0800416 for schema_type in self._types:
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800417 try:
418 schema_type.Validate(data)
419 except SchemaException:
420 continue
421 match = True
422 break
423 if not match:
Dean Liao604e62b2013-03-11 19:12:50 +0800424 raise SchemaException('%r does not match any type in %r' % (data,
425 self._types))
Dean Liao452c6ee2013-03-11 16:23:33 +0800426
427
428class Optional(AnyOf):
429 """A Schema class which accepts either None or given Schemas.
430
431 It is a special case of AnyOf class: in addition of given schema(s), it also
432 accepts None.
433
434 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800435 types: A (or a list of) Schema object(s) to be matched.
436 label: An optional string to describe this Optional type.
Dean Liao452c6ee2013-03-11 16:23:33 +0800437 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800438
Dean Liao604e62b2013-03-11 19:12:50 +0800439 def __init__(self, types, label=None):
440 try:
441 super(Optional, self).__init__(MakeList(types), label=label)
442 except SchemaException:
443 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800444 'types in Optional(types=%r%s) should be a Schema or a list of '
445 'Schemas' % (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800446
447 def __repr__(self):
448 label = '' if self._label is None else ', label=%r' % self._label
449 return 'Optional(%r%s)' % (self._types, label)
Dean Liao452c6ee2013-03-11 16:23:33 +0800450
451 def Validate(self, data):
452 """Validates if the given data is None or matches any schema in types.
453
454 Args:
455 data: A Python data structue to be validated.
456
457 Raises:
458 SchemaException if data is not None and no schemas in types validates the
459 input data.
460 """
461 if data is None:
462 return
463 try:
464 super(Optional, self).Validate(data)
465 except SchemaException:
Dean Liao604e62b2013-03-11 19:12:50 +0800466 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800467 '%r is not None and does not match any type in %r' % (data,
468 self._types))