blob: bdb3e836b6d60f1af1cf943e436489bdd7a38b47 [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
55import factory_common # pylint: disable=W0611
56
Dean Liao452c6ee2013-03-11 16:23:33 +080057from cros.factory.common import MakeList
58
Ricky Liang1b72ccf2013-03-01 10:23:01 +080059
60class SchemaException(Exception):
61 pass
62
63
64class BaseType(object):
65 """Base type class for schema classes.
66 """
67 def __init__(self, label):
68 self._label = label
69
70 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.
80
81 Raises:
82 SchemaException if argument format is incorrect.
83 """
84 def __init__(self, label, element_type):
85 super(Scalar, self).__init__(label)
86 if getattr(element_type, '__iter__', None):
87 raise SchemaException('Scalar element type %r is iterable')
88 self._element_type = element_type
89
90 def Validate(self, data):
91 """Validates the given data against the Scalar schema.
92
93 It checks if the data's type matches the Scalar's element type. Also, it
94 checks if the data's value matches the Scalar's value if the required value
95 is specified.
96
97 Args:
98 data: A Python data structure to be validated.
99
100 Raises:
101 SchemaException if validation fails.
102 """
103 if not isinstance(data, self._element_type):
104 raise SchemaException('Type mismatch on %r: expected %r, got %r' %
105 (data, self._element_type, type(data)))
106
107
108class Dict(BaseType):
109 """Dict schema class.
110
111 This schema class is used to verify simple dict. Only the key type and value
112 type are validated.
113
114 Attributes:
115 label: A human-readable string to describe this Scalar.
116 key_type: A schema object indicating the schema of the keys of this Dict.
117 value_type: A schema object indicating the schema of the values of this
118 Dict.
119
120 Raises:
121 SchemaException if argument format is incorrect.
122 """
123 def __init__(self, label, key_type, value_type):
124 super(Dict, self).__init__(label)
125 if not isinstance(key_type, Scalar):
126 raise SchemaException('key_type %r of Dict %r is not Scalar' %
127 (key_type, self._label))
128 self._key_type = key_type
129 if not isinstance(value_type, BaseType):
130 raise SchemaException('value_type %r of Dict %r is not Schema object' %
131 (value_type, self._label))
132 self._value_type = value_type
133
134 def Validate(self, data):
135 """Validates the given data against the Dict schema.
136
137 It checks that all the keys in data matches the schema defined by key_type,
138 and all the values in data matches the schema defined by value_type.
139
140 Args:
141 data: A Python data structure to be validated.
142
143 Raises:
144 SchemaException if validation fails.
145 """
146 if not isinstance(data, dict):
147 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
148 (self._label, type(data)))
149 for k, v in data.iteritems():
150 self._key_type.Validate(k)
151 self._value_type.Validate(v)
152
153
154class FixedDict(BaseType):
155 """FixedDict schema class.
156
157 FixedDict is a Dict with predefined allowed keys. And each key corresponds to
158 a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues
159 in XML.
160
161 An example FixedDict schema:
162 FixedDict('foo',
163 items={
164 'a': Scalar('bar', str),
165 'b': Scalar('buz', int)
166 }, optional_items={
167 'c': Scalar('boo', int)
168 })
169
170 Attributes:
171 label: A human-readable string to describe this dict.
172 items: A dict of required items that must be specified.
173 optional_items: A dict of optional items.
174
175 Raises:
176 SchemaException if argument format is incorrect.
177 """
178 def __init__(self, label, items=None, optional_items=None):
179 super(FixedDict, self).__init__(label)
180 if items and not isinstance(items, dict):
181 raise SchemaException('items of FixedDict %r should be a dict' %
182 self._label)
183 self._items = copy.deepcopy(items) if items is not None else {}
184 if optional_items and not isinstance(optional_items, dict):
185 raise SchemaException('optional_items of FixedDict %r should be a dict' %
186 self._label)
187 self._optional_items = (
188 copy.deepcopy(optional_items) if optional_items is not None else {})
189
190 def Validate(self, data):
191 """Validates the given data and all its key-value pairs against the Dict
192 schema.
193
194 If a key of Dict's type is required, then it must exist in the data's keys.
195
196 Args:
197 data: A Python data structure to be validated.
198
199 Raises:
200 SchemaException if validation fails.
201 """
202 if not isinstance(data, dict):
203 raise SchemaException('Type mismatch on %r: expected dict, got %r' %
204 (self._label, type(data)))
205 data_key_list = data.keys()
206 # Check that every key-value pair in items exists in data
207 for key, value_schema in self._items.iteritems():
208 if key not in data:
209 raise SchemaException(
210 'Required item %r does not exist in FixedDict %r' %
211 (key, data))
212 value_schema.Validate(data[key])
213 data_key_list.remove(key)
214 # Check that all the remaining unmatched key-value pairs matches any
215 # definition in items or optional_items.
216 for key, value_schema in self._optional_items.iteritems():
217 if key not in data:
218 continue
219 value_schema.Validate(data[key])
220 data_key_list.remove(key)
221 if data_key_list:
222 raise SchemaException('Keys %r are undefined in FixedDict %r' %
223 (data_key_list, self._label))
224
225
226class List(BaseType):
227 """List schema class.
228
229 Attributes:
230 label: A string to describe this list.
231 element_type: Optional schema object to describe the type of the List.
232
233 Raises:
234 SchemaException if argument format is incorrect.
235 """
236 def __init__(self, label, element_type=None):
237 super(List, self).__init__(label)
Ricky Liang3e5342b2013-03-08 12:16:25 +0800238 if element_type and not isinstance(element_type, BaseType):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800239 raise SchemaException(
240 'element_type %r of List %r is not a Schema object' %
241 (element_type, self._label))
Ricky Liang3e5342b2013-03-08 12:16:25 +0800242 self._element_type = copy.deepcopy(element_type)
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800243
244 def Validate(self, data):
Ricky Liang3e5342b2013-03-08 12:16:25 +0800245 """Validates the given data and all its elements against the List schema.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800246
247 Args:
248 data: A Python data structure to be validated.
249
250 Raises:
251 SchemaException if validation fails.
252 """
253 if not isinstance(data, list):
254 raise SchemaException('Type mismatch on %r: expected list, got %r' %
255 (self._label, type(data)))
256 if self._element_type:
257 for data_value in data:
258 self._element_type.Validate(data_value)
259
260
Ricky Liang3e5342b2013-03-08 12:16:25 +0800261class Tuple(BaseType):
262 """Tuple schema class.
263
264 Comparing to List, the Tuple schema makes sure that every element exactly
265 matches the defined position and schema.
266
267 Attributes:
268 label: A string to describe this tuple.
269 element_types: Optional list or tuple schema object to describe the
270 types of the Tuple.
271
272 Raises:
273 SchemaException if argument format is incorrect.
274 """
275 def __init__(self, label, element_types=None):
276 super(Tuple, self).__init__(label)
277 if (element_types and
278 (not isinstance(element_types, (tuple, list))) or
279 (not all([isinstance(x, BaseType)] for x in element_types))):
280 raise SchemaException(
281 'element_types %r of Tuple %r is not a tuple or list' %
282 (element_types, self._label))
283 self._element_types = copy.deepcopy(element_types)
284
285 def Validate(self, data):
286 """Validates the given data and all its elements against the Tuple schema.
287
288 Args:
289 data: A Python data structure to be validated.
290
291 Raises:
292 SchemaException if validation fails.
293 """
294 if not isinstance(data, tuple):
295 raise SchemaException('Type mismatch on %r: expected tuple, got %r' %
296 (self._label, type(data)))
297 if self._element_types and len(self._element_types) != len(data):
298 raise SchemaException(
299 'Number of elements in tuple %r does not match that defined '
300 'in Tuple schema %r' % (str(data), self._label))
301 for data, element_type in zip(data, self._element_types):
302 element_type.Validate(data)
303
304
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800305class AnyOf(BaseType):
Dean Liao452c6ee2013-03-11 16:23:33 +0800306 """A Schema class which accepts any one of the given Schemas.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800307
308 Attributes:
309 label: A human-readable string to describe this object.
310 type_list: A list of Schema objects to be matched.
311 """
312 def __init__(self, label, type_list):
313 super(AnyOf, self).__init__(label)
Dean Liao452c6ee2013-03-11 16:23:33 +0800314 if (not isinstance(type_list, list) or
315 not all([isinstance(x, BaseType) for x in type_list])):
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800316 raise SchemaException(
317 'type_list of AnyOf %r should be a list of Schema types' %
318 self._label)
319 self._type_list = type_list
320
321 def Validate(self, data):
322 """Validates if the given data matches any schema in type_list
323
324 Args:
325 data: A Python data structue to be validated.
Dean Liao452c6ee2013-03-11 16:23:33 +0800326
327 Raises:
328 SchemaException if no schemas in type_list validates the input data.
Ricky Liang1b72ccf2013-03-01 10:23:01 +0800329 """
330 match = False
331 for schema_type in self._type_list:
332 try:
333 schema_type.Validate(data)
334 except SchemaException:
335 continue
336 match = True
337 break
338 if not match:
339 raise SchemaException('%r does not match any type in %r' %
340 (data, self._label))
Dean Liao452c6ee2013-03-11 16:23:33 +0800341
342
343class Optional(AnyOf):
344 """A Schema class which accepts either None or given Schemas.
345
346 It is a special case of AnyOf class: in addition of given schema(s), it also
347 accepts None.
348
349 Attributes:
350 label: A human-readable string to describe this object.
351 types: A (a list of) Schema object(s) to be matched.
352 """
353 def __init__(self, label, types):
354 super(Optional, self).__init__(label, MakeList(types))
355
356 def Validate(self, data):
357 """Validates if the given data is None or matches any schema in types.
358
359 Args:
360 data: A Python data structue to be validated.
361
362 Raises:
363 SchemaException if data is not None and no schemas in types validates the
364 input data.
365 """
366 if data is None:
367 return
368 try:
369 super(Optional, self).Validate(data)
370 except SchemaException:
371 raise SchemaException('%r is not None and does not match any type in %r' %
372 (data, self._label))