blob: c650303f38ddd64b28ad612ced4bd538ea33df96 [file] [log] [blame]
Ricky Liang1b72ccf2013-03-01 10:23:01 +08001#!/usr/bin/python -u
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8# pylint: disable=W0212, W0622
9
10"""A function to create a schema tree from the given schema expression.
11
12For example:
13
14 1. This is the schema of the encoded_fields in component database.
15
16 Dict('encoded_fields', Scalar('encoded_field', str),
17 Dict('encoded_indices', Scalar('encoded_index', int),
18 Dict('component_classes', Scalar('component_class', str),
19 AnyOf('component_names', [
20 Scalar('component_name', str),
21 List('list_of_component_names', Scalar('component_name', str)),
22 Scalar('none', type(None))
23 ])
24 )
25 )
26 )
27
28 2. This is the schema of the pattern in component database.
29
30 List('pattern',
31 Dict('pattern_field', key_type=Scalar('encoded_index', str),
32 value_type=Scalar('bit_offset', int))
33 )
34
35 3. This is the schema of the components in component database.
36
37 Dict('components', Scalar('component_class', str),
38 Dict('component_names', Scalar('component_name', str),
39 FixedDict('component_attributes',
40 items={
41 'value': AnyOf('probed_value', [
42 Scalar('probed_value', str),
43 List('list_of_probed_values', Scalar('probed_value', str))
44 ])
45 },
46 optional_items={
47 'labels': List('list_of_labels', Scalar('label', str))
48 }
49 )
50 )
51 )
52"""
53
54import copy
Ricky Liang1b72ccf2013-03-01 10:23:01 +080055
Joel Kitchinga8be7962016-06-08 17:35:01 +080056from .type_utils import MakeList
Dean Liao452c6ee2013-03-11 16:23:33 +080057
Ricky Liang1b72ccf2013-03-01 10:23:01 +080058
59class SchemaException(Exception):
60 pass
61
62
63class BaseType(object):
64 """Base type class for schema classes.
65 """
Hung-Te Lin56b18402015-01-16 14:52:30 +080066
Ricky Liang1b72ccf2013-03-01 10:23:01 +080067 def __init__(self, label):
68 self._label = label
69
Dean Liao604e62b2013-03-11 19:12:50 +080070 def __repr__(self):
71 return 'BaseType(%r)' % self._label
72
Ricky Liang1b72ccf2013-03-01 10:23:01 +080073 def Validate(self, data):
74 raise NotImplementedError
75
76
77class Scalar(BaseType):
78 """Scalar schema class.
79
80 Attributes:
81 label: A human-readable string to describe this Scalar.
82 element_type: The Python type of this Scalar. Cannot be a iterable type.
Jon Salz05fffde2014-07-14 12:56:47 +080083 choices: A set of allowable choices for the scalar, or None to allow
84 any values of the given type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +080085
86 Raises:
87 SchemaException if argument format is incorrect.
88 """
Hung-Te Lin56b18402015-01-16 14:52:30 +080089
Jon Salz05fffde2014-07-14 12:56:47 +080090 def __init__(self, label, element_type, choices=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +080091 super(Scalar, self).__init__(label)
92 if getattr(element_type, '__iter__', None):
Dean Liao604e62b2013-03-11 19:12:50 +080093 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +080094 'element_type %r of Scalar %r is not a scalar type' % (element_type,
95 label))
Ricky Liang1b72ccf2013-03-01 10:23:01 +080096 self._element_type = element_type
Jon Salz05fffde2014-07-14 12:56:47 +080097 self._choices = set(choices) if choices else None
98
Dean Liao604e62b2013-03-11 19:12:50 +080099 def __repr__(self):
Jon Salz05fffde2014-07-14 12:56:47 +0800100 return 'Scalar(%r, %r%s)' % (
Hung-Te Lin56b18402015-01-16 14:52:30 +0800101 self._label, self._element_type,
102 ', choices=%r' % sorted(self._choices) if self._choices else '')
Dean Liao604e62b2013-03-11 19:12:50 +0800103
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800104 def Validate(self, data):
105 """Validates the given data against the Scalar schema.
106
107 It checks if the data's type matches the Scalar's element type. Also, it
108 checks if the data's value matches the Scalar's value if the required value
109 is specified.
110
111 Args:
112 data: A Python data structure to be validated.
113
114 Raises:
115 SchemaException if validation fails.
116 """
117 if not isinstance(data, self._element_type):
118 raise SchemaException('Type mismatch on %r: expected %r, got %r' %
119 (data, self._element_type, type(data)))
Jon Salz05fffde2014-07-14 12:56:47 +0800120 if self._choices and data not in self._choices:
121 raise SchemaException('Value mismatch on %r: expected one of %r' %
122 (data, sorted(self._choices)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800123
124
125class Dict(BaseType):
126 """Dict schema class.
127
128 This schema class is used to verify simple dict. Only the key type and value
129 type are validated.
130
131 Attributes:
132 label: A human-readable string to describe this Scalar.
Ricky Liangf5386b32013-03-11 16:40:45 +0800133 key_type: A schema object indicating the schema of the keys of this Dict. It
134 can be a Scalar or an AnyOf with possible values being all Scalars.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800135 value_type: A schema object indicating the schema of the values of this
136 Dict.
137
138 Raises:
139 SchemaException if argument format is incorrect.
140 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800141
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800142 def __init__(self, label, key_type, value_type):
143 super(Dict, self).__init__(label)
Ricky Liangf5386b32013-03-11 16:40:45 +0800144 if not (isinstance(key_type, Scalar) or
Hung-Te Lin56b18402015-01-16 14:52:30 +0800145 (isinstance(key_type, AnyOf) and
146 key_type.CheckTypeOfPossibleValues(Scalar))):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800147 raise SchemaException('key_type %r of Dict %r is not Scalar' %
148 (key_type, self._label))
149 self._key_type = key_type
150 if not isinstance(value_type, BaseType):
151 raise SchemaException('value_type %r of Dict %r is not Schema object' %
152 (value_type, self._label))
153 self._value_type = value_type
154
Dean Liao604e62b2013-03-11 19:12:50 +0800155 def __repr__(self):
156 return 'Dict(%r, key_type=%r, value_type=%r)' % (self._label,
157 self._key_type,
158 self._value_type)
159
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800160 def Validate(self, data):
161 """Validates the given data against the Dict schema.
162
163 It checks that all the keys in data matches the schema defined by key_type,
164 and all the values in data matches the schema defined by value_type.
165
166 Args:
167 data: A Python data structure to be validated.
168
169 Raises:
170 SchemaException if validation fails.
171 """
172 if not isinstance(data, dict):
173 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
174 (self._label, type(data)))
175 for k, v in data.iteritems():
176 self._key_type.Validate(k)
177 self._value_type.Validate(v)
178
179
180class FixedDict(BaseType):
181 """FixedDict schema class.
182
183 FixedDict is a Dict with predefined allowed keys. And each key corresponds to
184 a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues
185 in XML.
186
187 An example FixedDict schema:
188 FixedDict('foo',
189 items={
190 'a': Scalar('bar', str),
191 'b': Scalar('buz', int)
192 }, optional_items={
193 'c': Scalar('boo', int)
194 })
195
196 Attributes:
197 label: A human-readable string to describe this dict.
198 items: A dict of required items that must be specified.
199 optional_items: A dict of optional items.
200
201 Raises:
202 SchemaException if argument format is incorrect.
203 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800204
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800205 def __init__(self, label, items=None, optional_items=None):
206 super(FixedDict, self).__init__(label)
207 if items and not isinstance(items, dict):
208 raise SchemaException('items of FixedDict %r should be a dict' %
209 self._label)
210 self._items = copy.deepcopy(items) if items is not None else {}
211 if optional_items and not isinstance(optional_items, dict):
212 raise SchemaException('optional_items of FixedDict %r should be a dict' %
213 self._label)
214 self._optional_items = (
215 copy.deepcopy(optional_items) if optional_items is not None else {})
216
Dean Liao604e62b2013-03-11 19:12:50 +0800217 def __repr__(self):
218 return 'FixedDict(%r, items=%r, optional_items=%r)' % (self._label,
219 self._items,
220 self._optional_items)
Hung-Te Lin56b18402015-01-16 14:52:30 +0800221
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800222 def Validate(self, data):
223 """Validates the given data and all its key-value pairs against the Dict
224 schema.
225
226 If a key of Dict's type is required, then it must exist in the data's keys.
227
228 Args:
229 data: A Python data structure to be validated.
230
231 Raises:
232 SchemaException if validation fails.
233 """
234 if not isinstance(data, dict):
235 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
236 (self._label, type(data)))
237 data_key_list = data.keys()
238 # Check that every key-value pair in items exists in data
239 for key, value_schema in self._items.iteritems():
240 if key not in data:
241 raise SchemaException(
242 'Required item %r does not exist in FixedDict %r' %
243 (key, data))
244 value_schema.Validate(data[key])
245 data_key_list.remove(key)
246 # Check that all the remaining unmatched key-value pairs matches any
247 # definition in items or optional_items.
248 for key, value_schema in self._optional_items.iteritems():
249 if key not in data:
250 continue
251 value_schema.Validate(data[key])
252 data_key_list.remove(key)
253 if data_key_list:
254 raise SchemaException('Keys %r are undefined in FixedDict %r' %
255 (data_key_list, self._label))
256
257
258class List(BaseType):
259 """List schema class.
260
261 Attributes:
262 label: A string to describe this list.
Dean Liao604e62b2013-03-11 19:12:50 +0800263 element_type: Optional schema object to validate the elements of the list.
264 Default None means no validation of elements' type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800265
266 Raises:
267 SchemaException if argument format is incorrect.
268 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800269
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800270 def __init__(self, label, element_type=None):
271 super(List, self).__init__(label)
Ricky Liang3e5342b2013-03-08 12:16:25 +0800272 if element_type and not isinstance(element_type, BaseType):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800273 raise SchemaException(
274 'element_type %r of List %r is not a Schema object' %
275 (element_type, self._label))
Ricky Liang3e5342b2013-03-08 12:16:25 +0800276 self._element_type = copy.deepcopy(element_type)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800277
Dean Liao604e62b2013-03-11 19:12:50 +0800278 def __repr__(self):
279 return 'List(%r, %r)' % (self._label, self._element_type)
280
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800281 def Validate(self, data):
Ricky Liang3e5342b2013-03-08 12:16:25 +0800282 """Validates the given data and all its elements against the List schema.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800283
284 Args:
285 data: A Python data structure to be validated.
286
287 Raises:
288 SchemaException if validation fails.
289 """
290 if not isinstance(data, list):
291 raise SchemaException('Type mismatch on %r: expected list, got %r' %
Hung-Te Lin56b18402015-01-16 14:52:30 +0800292 (self._label, type(data)))
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800293 if self._element_type:
294 for data_value in data:
295 self._element_type.Validate(data_value)
296
297
Ricky Liang3e5342b2013-03-08 12:16:25 +0800298class Tuple(BaseType):
299 """Tuple schema class.
300
301 Comparing to List, the Tuple schema makes sure that every element exactly
302 matches the defined position and schema.
303
304 Attributes:
305 label: A string to describe this tuple.
306 element_types: Optional list or tuple schema object to describe the
307 types of the Tuple.
308
309 Raises:
310 SchemaException if argument format is incorrect.
311 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800312
Ricky Liang3e5342b2013-03-08 12:16:25 +0800313 def __init__(self, label, element_types=None):
314 super(Tuple, self).__init__(label)
315 if (element_types and
316 (not isinstance(element_types, (tuple, list))) or
317 (not all([isinstance(x, BaseType)] for x in element_types))):
318 raise SchemaException(
319 'element_types %r of Tuple %r is not a tuple or list' %
320 (element_types, self._label))
321 self._element_types = copy.deepcopy(element_types)
322
Dean Liao604e62b2013-03-11 19:12:50 +0800323 def __repr__(self):
324 return 'Tuple(%r, %r)' % (self._label, self._element_types)
325
Ricky Liang3e5342b2013-03-08 12:16:25 +0800326 def Validate(self, data):
327 """Validates the given data and all its elements against the Tuple schema.
328
329 Args:
330 data: A Python data structure to be validated.
331
332 Raises:
333 SchemaException if validation fails.
334 """
335 if not isinstance(data, tuple):
336 raise SchemaException('Type mismatch on %r: expected tuple, got %r' %
337 (self._label, type(data)))
338 if self._element_types and len(self._element_types) != len(data):
339 raise SchemaException(
340 'Number of elements in tuple %r does not match that defined '
341 'in Tuple schema %r' % (str(data), self._label))
342 for data, element_type in zip(data, self._element_types):
343 element_type.Validate(data)
344
345
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800346class AnyOf(BaseType):
Dean Liao452c6ee2013-03-11 16:23:33 +0800347 """A Schema class which accepts any one of the given Schemas.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800348
349 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800350 types: A list of Schema objects to be matched.
351 label: An optional string to describe this AnyOf type.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800352 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800353
Dean Liao604e62b2013-03-11 19:12:50 +0800354 def __init__(self, types, label=None):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800355 super(AnyOf, self).__init__(label)
Dean Liao604e62b2013-03-11 19:12:50 +0800356 if (not isinstance(types, list) or
357 not all([isinstance(x, BaseType) for x in types])):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800358 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800359 'types in AnyOf(types=%r%s) should be a list of Schemas' %
360 (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800361 self._types = list(types)
362
363 def __repr__(self):
364 label = '' if self._label is None else ', label=%r' % self._label
365 return 'AnyOf(%r%s)' % (self._types, label)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800366
Ricky Liangf5386b32013-03-11 16:40:45 +0800367 def CheckTypeOfPossibleValues(self, schema_type):
368 """Checks if the acceptable types are of the same type as schema_type.
369
370 Args:
371 schema_type: The schema type to check against with.
372 """
Dean Liao604e62b2013-03-11 19:12:50 +0800373 return all([isinstance(k, schema_type) for k in self._types])
Ricky Liangf5386b32013-03-11 16:40:45 +0800374
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800375 def Validate(self, data):
Dean Liao604e62b2013-03-11 19:12:50 +0800376 """Validates if the given data matches any schema in types
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800377
378 Args:
379 data: A Python data structue to be validated.
Dean Liao452c6ee2013-03-11 16:23:33 +0800380
381 Raises:
Dean Liao604e62b2013-03-11 19:12:50 +0800382 SchemaException if no schemas in types validates the input data.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800383 """
384 match = False
Dean Liao604e62b2013-03-11 19:12:50 +0800385 for schema_type in self._types:
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800386 try:
387 schema_type.Validate(data)
388 except SchemaException:
389 continue
390 match = True
391 break
392 if not match:
Dean Liao604e62b2013-03-11 19:12:50 +0800393 raise SchemaException('%r does not match any type in %r' % (data,
394 self._types))
Dean Liao452c6ee2013-03-11 16:23:33 +0800395
396
397class Optional(AnyOf):
398 """A Schema class which accepts either None or given Schemas.
399
400 It is a special case of AnyOf class: in addition of given schema(s), it also
401 accepts None.
402
403 Attributes:
Dean Liao604e62b2013-03-11 19:12:50 +0800404 types: A (or a list of) Schema object(s) to be matched.
405 label: An optional string to describe this Optional type.
Dean Liao452c6ee2013-03-11 16:23:33 +0800406 """
Hung-Te Lin56b18402015-01-16 14:52:30 +0800407
Dean Liao604e62b2013-03-11 19:12:50 +0800408 def __init__(self, types, label=None):
409 try:
410 super(Optional, self).__init__(MakeList(types), label=label)
411 except SchemaException:
412 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800413 'types in Optional(types=%r%s) should be a Schema or a list of '
414 'Schemas' % (types, '' if label is None else ', label=%r' % label))
Dean Liao604e62b2013-03-11 19:12:50 +0800415
416 def __repr__(self):
417 label = '' if self._label is None else ', label=%r' % self._label
418 return 'Optional(%r%s)' % (self._types, label)
Dean Liao452c6ee2013-03-11 16:23:33 +0800419
420 def Validate(self, data):
421 """Validates if the given data is None or matches any schema in types.
422
423 Args:
424 data: A Python data structue to be validated.
425
426 Raises:
427 SchemaException if data is not None and no schemas in types validates the
428 input data.
429 """
430 if data is None:
431 return
432 try:
433 super(Optional, self).Validate(data)
434 except SchemaException:
Dean Liao604e62b2013-03-11 19:12:50 +0800435 raise SchemaException(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800436 '%r is not None and does not match any type in %r' % (data,
437 self._types))