Hung-Te Lin | 1990b74 | 2017-08-09 17:34:57 +0800 | [diff] [blame] | 1 | # Copyright 2012 The Chromium OS Authors. All rights reserved. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 2 | # 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 | |
| 9 | For 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 | |
| 51 | import copy |
Joel Kitching | a8be796 | 2016-06-08 17:35:01 +0800 | [diff] [blame] | 52 | from .type_utils import MakeList |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 53 | |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 54 | # To simplify portability issues, validating JSON schema is optional. |
| 55 | try: |
| 56 | import jsonschema |
| 57 | _HAVE_JSONSCHEMA = True |
| 58 | except ImportError: |
| 59 | _HAVE_JSONSCHEMA = False |
| 60 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 61 | |
| 62 | class SchemaException(Exception): |
| 63 | pass |
| 64 | |
| 65 | |
| 66 | class BaseType(object): |
| 67 | """Base type class for schema classes. |
| 68 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 69 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 70 | def __init__(self, label): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 71 | self.label = label |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 72 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 73 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 74 | return 'BaseType(%r)' % self.label |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 75 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 76 | def Validate(self, data): |
| 77 | raise NotImplementedError |
| 78 | |
| 79 | |
| 80 | class 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 Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 86 | choices: A set of allowable choices for the scalar, or None to allow |
| 87 | any values of the given type. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 88 | |
| 89 | Raises: |
| 90 | SchemaException if argument format is incorrect. |
| 91 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 92 | |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 93 | def __init__(self, label, element_type, choices=None): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 94 | super(Scalar, self).__init__(label) |
| 95 | if getattr(element_type, '__iter__', None): |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 96 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 97 | 'element_type %r of Scalar %r is not a scalar type' % (element_type, |
| 98 | label)) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 99 | self.element_type = element_type |
| 100 | self.choices = set(choices) if choices else None |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 101 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 102 | def __repr__(self): |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 103 | return 'Scalar(%r, %r%s)' % ( |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 104 | self.label, self.element_type, |
| 105 | ', choices=%r' % sorted(self.choices) if self.choices else '') |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 106 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 107 | 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 | """ |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 120 | if not isinstance(data, self.element_type): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 121 | raise SchemaException('Type mismatch on %r: expected %r, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 122 | (data, self.element_type, type(data))) |
| 123 | if self.choices and data not in self.choices: |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 124 | raise SchemaException('Value mismatch on %r: expected one of %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 125 | (data, sorted(self.choices))) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 126 | |
| 127 | |
| 128 | class 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 Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 136 | 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 Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 138 | value_type: A schema object indicating the schema of the values of this |
| 139 | Dict. |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 140 | min_size: The minimum size of the elements, default to 0. |
| 141 | max_size: None or the maximum size of the elements. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 142 | |
| 143 | Raises: |
| 144 | SchemaException if argument format is incorrect. |
| 145 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 146 | |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 147 | def __init__(self, label, key_type, value_type, min_size=0, max_size=None): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 148 | super(Dict, self).__init__(label) |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 149 | if not (isinstance(key_type, Scalar) or |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 150 | (isinstance(key_type, AnyOf) and |
| 151 | key_type.CheckTypeOfPossibleValues(Scalar))): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 152 | raise SchemaException('key_type %r of Dict %r is not Scalar' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 153 | (key_type, self.label)) |
| 154 | self.key_type = key_type |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 155 | if not isinstance(value_type, BaseType): |
| 156 | raise SchemaException('value_type %r of Dict %r is not Schema object' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 157 | (value_type, self.label)) |
| 158 | self.value_type = value_type |
| 159 | self.min_size = min_size |
| 160 | self.max_size = max_size |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 161 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 162 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 163 | size_expr = '[%d, %s]' % (self.min_size, ('inf' if self.max_size is None |
| 164 | else '%d' % self.max_size)) |
| 165 | return 'Dict(%r, key_type=%r, value_type=%r, size=%s)' % ( |
| 166 | self.label, self.key_type, self.value_type, size_expr) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 167 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 168 | def Validate(self, data): |
| 169 | """Validates the given data against the Dict schema. |
| 170 | |
| 171 | It checks that all the keys in data matches the schema defined by key_type, |
| 172 | and all the values in data matches the schema defined by value_type. |
| 173 | |
| 174 | Args: |
| 175 | data: A Python data structure to be validated. |
| 176 | |
| 177 | Raises: |
| 178 | SchemaException if validation fails. |
| 179 | """ |
| 180 | if not isinstance(data, dict): |
| 181 | raise SchemaException('Type mismatch on %r: expected dict, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 182 | (self.label, type(data))) |
| 183 | |
| 184 | if len(data) < self.min_size: |
| 185 | raise SchemaException('Size mismatch on %r: expected size >= %r' % |
| 186 | (self.label, self.min_size)) |
| 187 | |
| 188 | if self.max_size is not None and self.max_size < len(data): |
| 189 | raise SchemaException('Size mismatch on %r: expected size <= %r' % |
| 190 | (self.label, self.max_size)) |
| 191 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 192 | for k, v in data.iteritems(): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 193 | self.key_type.Validate(k) |
| 194 | self.value_type.Validate(v) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 195 | |
| 196 | |
| 197 | class FixedDict(BaseType): |
| 198 | """FixedDict schema class. |
| 199 | |
| 200 | FixedDict is a Dict with predefined allowed keys. And each key corresponds to |
| 201 | a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues |
| 202 | in XML. |
| 203 | |
| 204 | An example FixedDict schema: |
| 205 | FixedDict('foo', |
| 206 | items={ |
| 207 | 'a': Scalar('bar', str), |
| 208 | 'b': Scalar('buz', int) |
| 209 | }, optional_items={ |
| 210 | 'c': Scalar('boo', int) |
| 211 | }) |
| 212 | |
| 213 | Attributes: |
| 214 | label: A human-readable string to describe this dict. |
| 215 | items: A dict of required items that must be specified. |
| 216 | optional_items: A dict of optional items. |
| 217 | |
| 218 | Raises: |
| 219 | SchemaException if argument format is incorrect. |
| 220 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 221 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 222 | def __init__(self, label, items=None, optional_items=None): |
| 223 | super(FixedDict, self).__init__(label) |
| 224 | if items and not isinstance(items, dict): |
| 225 | raise SchemaException('items of FixedDict %r should be a dict' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 226 | self.label) |
| 227 | self.items = copy.deepcopy(items) if items is not None else {} |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 228 | if optional_items and not isinstance(optional_items, dict): |
| 229 | raise SchemaException('optional_items of FixedDict %r should be a dict' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 230 | self.label) |
| 231 | self.optional_items = ( |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 232 | copy.deepcopy(optional_items) if optional_items is not None else {}) |
| 233 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 234 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 235 | return 'FixedDict(%r, items=%r, optional_items=%r)' % (self.label, |
| 236 | self.items, |
| 237 | self.optional_items) |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 238 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 239 | def Validate(self, data): |
| 240 | """Validates the given data and all its key-value pairs against the Dict |
| 241 | schema. |
| 242 | |
| 243 | If a key of Dict's type is required, then it must exist in the data's keys. |
| 244 | |
| 245 | Args: |
| 246 | data: A Python data structure to be validated. |
| 247 | |
| 248 | Raises: |
| 249 | SchemaException if validation fails. |
| 250 | """ |
| 251 | if not isinstance(data, dict): |
| 252 | raise SchemaException('Type mismatch on %r: expected dict, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 253 | (self.label, type(data))) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 254 | data_key_list = data.keys() |
| 255 | # Check that every key-value pair in items exists in data |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 256 | for key, value_schema in self.items.iteritems(): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 257 | if key not in data: |
| 258 | raise SchemaException( |
| 259 | 'Required item %r does not exist in FixedDict %r' % |
| 260 | (key, data)) |
| 261 | value_schema.Validate(data[key]) |
| 262 | data_key_list.remove(key) |
| 263 | # Check that all the remaining unmatched key-value pairs matches any |
| 264 | # definition in items or optional_items. |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 265 | for key, value_schema in self.optional_items.iteritems(): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 266 | if key not in data: |
| 267 | continue |
| 268 | value_schema.Validate(data[key]) |
| 269 | data_key_list.remove(key) |
| 270 | if data_key_list: |
| 271 | raise SchemaException('Keys %r are undefined in FixedDict %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 272 | (data_key_list, self.label)) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 273 | |
| 274 | |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 275 | class JSONSchemaDict(BaseType): |
| 276 | """JSON schema class. |
| 277 | |
| 278 | This schema class allows mixing JSON schema with other schema types. |
| 279 | |
| 280 | Attributes: |
| 281 | label: A human-readable string to describe this JSON schema. |
| 282 | schema: a JSON schema object. |
| 283 | |
| 284 | Raises: |
| 285 | SchemaException if given schema is invalid. |
| 286 | ValidationError if argument format is incorrect. |
| 287 | """ |
| 288 | def __init__(self, label, schema): |
| 289 | super(JSONSchemaDict, self).__init__(label) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 290 | self.label = label |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 291 | if _HAVE_JSONSCHEMA: |
| 292 | jsonschema.Draft4Validator.check_schema(schema) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 293 | self.schema = schema |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 294 | |
| 295 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 296 | return 'JSONSchemaDict(%r, %r)' % (self.label, self.schema) |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 297 | |
| 298 | def Validate(self, data): |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 299 | if _HAVE_JSONSCHEMA: |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 300 | jsonschema.validate(data, self.schema) |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 301 | |
| 302 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 303 | class List(BaseType): |
| 304 | """List schema class. |
| 305 | |
| 306 | Attributes: |
| 307 | label: A string to describe this list. |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 308 | element_type: Optional schema object to validate the elements of the list. |
| 309 | Default None means no validation of elements' type. |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 310 | min_length: The expected minimum length of the list. Default to 0. |
| 311 | max_length: None or the limit of the length. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 312 | |
| 313 | Raises: |
| 314 | SchemaException if argument format is incorrect. |
| 315 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 316 | |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 317 | def __init__(self, label, element_type=None, min_length=0, max_length=None): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 318 | super(List, self).__init__(label) |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 319 | if element_type and not isinstance(element_type, BaseType): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 320 | raise SchemaException( |
| 321 | 'element_type %r of List %r is not a Schema object' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 322 | (element_type, self.label)) |
| 323 | self.element_type = copy.deepcopy(element_type) |
| 324 | self.min_length = min_length |
| 325 | self.max_length = max_length |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 326 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 327 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 328 | max_bound_repr = ('inf' if self.max_length is None |
| 329 | else '%d' % self.max_length) |
| 330 | return 'List(%r, %r, [%r, %s])' % ( |
| 331 | self.label, self.element_type, self.min_length, max_bound_repr) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 332 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 333 | def Validate(self, data): |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 334 | """Validates the given data and all its elements against the List schema. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 335 | |
| 336 | Args: |
| 337 | data: A Python data structure to be validated. |
| 338 | |
| 339 | Raises: |
| 340 | SchemaException if validation fails. |
| 341 | """ |
| 342 | if not isinstance(data, list): |
| 343 | raise SchemaException('Type mismatch on %r: expected list, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 344 | (self.label, type(data))) |
| 345 | |
| 346 | if len(data) < self.min_length: |
| 347 | raise SchemaException('Length mismatch on %r: expected length >= %d' % |
| 348 | (self.label, self.min_length)) |
| 349 | |
| 350 | if self.max_length is not None and self.max_length < len(data): |
| 351 | raise SchemaException('Length mismatch on %r: expected length <= %d' % |
| 352 | (self.label, self.max_length)) |
| 353 | |
| 354 | if self.element_type: |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 355 | for data_value in data: |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 356 | self.element_type.Validate(data_value) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 357 | |
| 358 | |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 359 | class Tuple(BaseType): |
| 360 | """Tuple schema class. |
| 361 | |
| 362 | Comparing to List, the Tuple schema makes sure that every element exactly |
| 363 | matches the defined position and schema. |
| 364 | |
| 365 | Attributes: |
| 366 | label: A string to describe this tuple. |
| 367 | element_types: Optional list or tuple schema object to describe the |
| 368 | types of the Tuple. |
| 369 | |
| 370 | Raises: |
| 371 | SchemaException if argument format is incorrect. |
| 372 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 373 | |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 374 | def __init__(self, label, element_types=None): |
| 375 | super(Tuple, self).__init__(label) |
| 376 | if (element_types and |
| 377 | (not isinstance(element_types, (tuple, list))) or |
| 378 | (not all([isinstance(x, BaseType)] for x in element_types))): |
| 379 | raise SchemaException( |
| 380 | 'element_types %r of Tuple %r is not a tuple or list' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 381 | (element_types, self.label)) |
| 382 | self.element_types = copy.deepcopy(element_types) |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 383 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 384 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 385 | return 'Tuple(%r, %r)' % (self.label, self.element_types) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 386 | |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 387 | def Validate(self, data): |
| 388 | """Validates the given data and all its elements against the Tuple schema. |
| 389 | |
| 390 | Args: |
| 391 | data: A Python data structure to be validated. |
| 392 | |
| 393 | Raises: |
| 394 | SchemaException if validation fails. |
| 395 | """ |
| 396 | if not isinstance(data, tuple): |
| 397 | raise SchemaException('Type mismatch on %r: expected tuple, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 398 | (self.label, type(data))) |
| 399 | if self.element_types and len(self.element_types) != len(data): |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 400 | raise SchemaException( |
| 401 | 'Number of elements in tuple %r does not match that defined ' |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 402 | 'in Tuple schema %r' % (str(data), self.label)) |
| 403 | for data, element_type in zip(data, self.element_types): |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 404 | element_type.Validate(data) |
| 405 | |
| 406 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 407 | class AnyOf(BaseType): |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 408 | """A Schema class which accepts any one of the given Schemas. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 409 | |
| 410 | Attributes: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 411 | types: A list of Schema objects to be matched. |
| 412 | label: An optional string to describe this AnyOf type. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 413 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 414 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 415 | def __init__(self, types, label=None): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 416 | super(AnyOf, self).__init__(label) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 417 | if (not isinstance(types, list) or |
| 418 | not all([isinstance(x, BaseType) for x in types])): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 419 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 420 | 'types in AnyOf(types=%r%s) should be a list of Schemas' % |
| 421 | (types, '' if label is None else ', label=%r' % label)) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 422 | self.types = list(types) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 423 | |
| 424 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 425 | label = '' if self.label is None else ', label=%r' % self.label |
| 426 | return 'AnyOf(%r%s)' % (self.types, label) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 427 | |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 428 | def CheckTypeOfPossibleValues(self, schema_type): |
| 429 | """Checks if the acceptable types are of the same type as schema_type. |
| 430 | |
| 431 | Args: |
| 432 | schema_type: The schema type to check against with. |
| 433 | """ |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 434 | return all([isinstance(k, schema_type) for k in self.types]) |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 435 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 436 | def Validate(self, data): |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 437 | """Validates if the given data matches any schema in types |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 438 | |
| 439 | Args: |
| 440 | data: A Python data structue to be validated. |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 441 | |
| 442 | Raises: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 443 | SchemaException if no schemas in types validates the input data. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 444 | """ |
| 445 | match = False |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 446 | for schema_type in self.types: |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 447 | try: |
| 448 | schema_type.Validate(data) |
| 449 | except SchemaException: |
| 450 | continue |
| 451 | match = True |
| 452 | break |
| 453 | if not match: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 454 | raise SchemaException('%r does not match any type in %r' % (data, |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 455 | self.types)) |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 456 | |
| 457 | |
| 458 | class Optional(AnyOf): |
| 459 | """A Schema class which accepts either None or given Schemas. |
| 460 | |
| 461 | It is a special case of AnyOf class: in addition of given schema(s), it also |
| 462 | accepts None. |
| 463 | |
| 464 | Attributes: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 465 | types: A (or a list of) Schema object(s) to be matched. |
| 466 | label: An optional string to describe this Optional type. |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 467 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 468 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 469 | def __init__(self, types, label=None): |
| 470 | try: |
| 471 | super(Optional, self).__init__(MakeList(types), label=label) |
| 472 | except SchemaException: |
| 473 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 474 | 'types in Optional(types=%r%s) should be a Schema or a list of ' |
| 475 | 'Schemas' % (types, '' if label is None else ', label=%r' % label)) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 476 | |
| 477 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 478 | label = '' if self.label is None else ', label=%r' % self.label |
| 479 | return 'Optional(%r%s)' % (self.types, label) |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 480 | |
| 481 | def Validate(self, data): |
| 482 | """Validates if the given data is None or matches any schema in types. |
| 483 | |
| 484 | Args: |
| 485 | data: A Python data structue to be validated. |
| 486 | |
| 487 | Raises: |
| 488 | SchemaException if data is not None and no schemas in types validates the |
| 489 | input data. |
| 490 | """ |
| 491 | if data is None: |
| 492 | return |
| 493 | try: |
| 494 | super(Optional, self).Validate(data) |
| 495 | except SchemaException: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 496 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 497 | '%r is not None and does not match any type in %r' % (data, |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame^] | 498 | self.types)) |