blob: 9928950a817ed7ae221be95da9418e81d189b8a4 [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
Ricky Liang1b72ccf2013-03-01 10:23:01 +080052
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
255class List(BaseType):
256 """List schema class.
257
258 Attributes:
259 label: A string to describe this list.
Dean Liao604e62b2013-03-11 19:12:50 +0800260 element_type: Optional schema object to validate the elements of the list.
261 Default None means no validation of elements' type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800262
263 Raises:
264 SchemaException if argument format is incorrect.
265 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800266
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800267 def __init__(self, label, element_type=None):
268 super(List, self).__init__(label)
Ricky Liang3e5342b2013-03-08 12:16:25 +0800269 if element_type and not isinstance(element_type, BaseType):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800270 raise SchemaException(
271 'element_type %r of List %r is not a Schema object' %
272 (element_type, self._label))
Ricky Liang3e5342b2013-03-08 12:16:25 +0800273 self._element_type = copy.deepcopy(element_type)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800274
Dean Liao604e62b2013-03-11 19:12:50 +0800275 def __repr__(self):
276 return 'List(%r, %r)' % (self._label, self._element_type)
277
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800278 def Validate(self, data):
Ricky Liang3e5342b2013-03-08 12:16:25 +0800279 """Validates the given data and all its elements against the List schema.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800280
281 Args:
282 data: A Python data structure to be validated.
283
284 Raises:
285 SchemaException if validation fails.
286 """
287 if not isinstance(data, list):
288 raise SchemaException('Type mismatch on %r: expected list, got %r' %
Hung-Te Lin56b18402015-01-16 14:52:30 +0800289 (self._label, type(data)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800290 if self._element_type:
291 for data_value in data:
292 self._element_type.Validate(data_value)
293
294
Ricky Liang3e5342b2013-03-08 12:16:25 +0800295class Tuple(BaseType):
296 """Tuple schema class.
297
298 Comparing to List, the Tuple schema makes sure that every element exactly
299 matches the defined position and schema.
300
301 Attributes:
302 label: A string to describe this tuple.
303 element_types: Optional list or tuple schema object to describe the
304 types of the Tuple.
305
306 Raises:
307 SchemaException if argument format is incorrect.
308 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800309
Ricky Liang3e5342b2013-03-08 12:16:25 +0800310 def __init__(self, label, element_types=None):
311 super(Tuple, self).__init__(label)
312 if (element_types and
313 (not isinstance(element_types, (tuple, list))) or
314 (not all([isinstance(x, BaseType)] for x in element_types))):
315 raise SchemaException(
316 'element_types %r of Tuple %r is not a tuple or list' %
317 (element_types, self._label))
318 self._element_types = copy.deepcopy(element_types)
319
Dean Liao604e62b2013-03-11 19:12:50 +0800320 def __repr__(self):
321 return 'Tuple(%r, %r)' % (self._label, self._element_types)
322
Ricky Liang3e5342b2013-03-08 12:16:25 +0800323 def Validate(self, data):
324 """Validates the given data and all its elements against the Tuple schema.
325
326 Args:
327 data: A Python data structure to be validated.
328
329 Raises:
330 SchemaException if validation fails.
331 """
332 if not isinstance(data, tuple):
333 raise SchemaException('Type mismatch on %r: expected tuple, got %r' %
334 (self._label, type(data)))
335 if self._element_types and len(self._element_types) != len(data):
336 raise SchemaException(
337 'Number of elements in tuple %r does not match that defined '
338 'in Tuple schema %r' % (str(data), self._label))
339 for data, element_type in zip(data, self._element_types):
340 element_type.Validate(data)
341
342
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800343class AnyOf(BaseType):
Dean Liao452c6ee2013-03-11 16:23:33 +0800344 """A Schema class which accepts any one of the given Schemas.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800345
346 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800347 types: A list of Schema objects to be matched.
348 label: An optional string to describe this AnyOf type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800349 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800350
Dean Liao604e62b2013-03-11 19:12:50 +0800351 def __init__(self, types, label=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800352 super(AnyOf, self).__init__(label)
Dean Liao604e62b2013-03-11 19:12:50 +0800353 if (not isinstance(types, list) or
354 not all([isinstance(x, BaseType) for x in types])):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800355 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800356 'types in AnyOf(types=%r%s) should be a list of Schemas' %
357 (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800358 self._types = list(types)
359
360 def __repr__(self):
361 label = '' if self._label is None else ', label=%r' % self._label
362 return 'AnyOf(%r%s)' % (self._types, label)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800363
Ricky Liangf5386b32013-03-11 16:40:45 +0800364 def CheckTypeOfPossibleValues(self, schema_type):
365 """Checks if the acceptable types are of the same type as schema_type.
366
367 Args:
368 schema_type: The schema type to check against with.
369 """
Dean Liao604e62b2013-03-11 19:12:50 +0800370 return all([isinstance(k, schema_type) for k in self._types])
Ricky Liangf5386b32013-03-11 16:40:45 +0800371
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800372 def Validate(self, data):
Dean Liao604e62b2013-03-11 19:12:50 +0800373 """Validates if the given data matches any schema in types
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800374
375 Args:
376 data: A Python data structue to be validated.
Dean Liao452c6ee2013-03-11 16:23:33 +0800377
378 Raises:
Dean Liao604e62b2013-03-11 19:12:50 +0800379 SchemaException if no schemas in types validates the input data.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800380 """
381 match = False
Dean Liao604e62b2013-03-11 19:12:50 +0800382 for schema_type in self._types:
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800383 try:
384 schema_type.Validate(data)
385 except SchemaException:
386 continue
387 match = True
388 break
389 if not match:
Dean Liao604e62b2013-03-11 19:12:50 +0800390 raise SchemaException('%r does not match any type in %r' % (data,
391 self._types))
Dean Liao452c6ee2013-03-11 16:23:33 +0800392
393
394class Optional(AnyOf):
395 """A Schema class which accepts either None or given Schemas.
396
397 It is a special case of AnyOf class: in addition of given schema(s), it also
398 accepts None.
399
400 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800401 types: A (or a list of) Schema object(s) to be matched.
402 label: An optional string to describe this Optional type.
Dean Liao452c6ee2013-03-11 16:23:33 +0800403 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800404
Dean Liao604e62b2013-03-11 19:12:50 +0800405 def __init__(self, types, label=None):
406 try:
407 super(Optional, self).__init__(MakeList(types), label=label)
408 except SchemaException:
409 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800410 'types in Optional(types=%r%s) should be a Schema or a list of '
411 'Schemas' % (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800412
413 def __repr__(self):
414 label = '' if self._label is None else ', label=%r' % self._label
415 return 'Optional(%r%s)' % (self._types, label)
Dean Liao452c6ee2013-03-11 16:23:33 +0800416
417 def Validate(self, data):
418 """Validates if the given data is None or matches any schema in types.
419
420 Args:
421 data: A Python data structue to be validated.
422
423 Raises:
424 SchemaException if data is not None and no schemas in types validates the
425 input data.
426 """
427 if data is None:
428 return
429 try:
430 super(Optional, self).Validate(data)
431 except SchemaException:
Dean Liao604e62b2013-03-11 19:12:50 +0800432 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800433 '%r is not None and does not match any type in %r' % (data,
434 self._types))