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 | |
Peter Shih | 8643049 | 2018-02-26 14:51:58 +0800 | [diff] [blame] | 5 | # pylint: disable=protected-access,redefined-builtin |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 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 |
Yilin Yang | ea78466 | 2019-09-26 13:51:03 +0800 | [diff] [blame] | 52 | |
Joel Kitching | a8be796 | 2016-06-08 17:35:01 +0800 | [diff] [blame] | 53 | from .type_utils import MakeList |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 54 | |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 55 | # To simplify portability issues, validating JSON schema is optional. |
| 56 | try: |
Peter Shih | 533566a | 2018-09-05 17:48:03 +0800 | [diff] [blame] | 57 | # pylint: disable=wrong-import-order |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 58 | import jsonschema |
| 59 | _HAVE_JSONSCHEMA = True |
| 60 | except ImportError: |
| 61 | _HAVE_JSONSCHEMA = False |
| 62 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 63 | |
| 64 | class SchemaException(Exception): |
| 65 | pass |
| 66 | |
| 67 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 68 | class BaseType: |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 69 | """Base type class for schema classes. |
| 70 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 71 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 72 | def __init__(self, label): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 73 | self.label = label |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 74 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 75 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 76 | return 'BaseType(%r)' % self.label |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 77 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 78 | def Validate(self, data): |
| 79 | raise NotImplementedError |
| 80 | |
| 81 | |
| 82 | class Scalar(BaseType): |
| 83 | """Scalar schema class. |
| 84 | |
| 85 | Attributes: |
| 86 | label: A human-readable string to describe this Scalar. |
| 87 | 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] | 88 | choices: A set of allowable choices for the scalar, or None to allow |
| 89 | any values of the given type. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 90 | |
| 91 | Raises: |
| 92 | SchemaException if argument format is incorrect. |
| 93 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 94 | |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 95 | def __init__(self, label, element_type, choices=None): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 96 | super(Scalar, self).__init__(label) |
Yilin Yang | 4d4bcab | 2019-11-15 13:37:28 +0800 | [diff] [blame] | 97 | if getattr(element_type, '__iter__', None) and element_type not in ( |
| 98 | str, bytes): |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 99 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 100 | 'element_type %r of Scalar %r is not a scalar type' % (element_type, |
| 101 | label)) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 102 | self.element_type = element_type |
Yilin Yang | ed5d7e2 | 2020-07-23 11:14:21 +0800 | [diff] [blame] | 103 | self.choices = set(choices) if choices else set() |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 104 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 105 | def __repr__(self): |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 106 | return 'Scalar(%r, %r%s)' % ( |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 107 | self.label, self.element_type, |
| 108 | ', choices=%r' % sorted(self.choices) if self.choices else '') |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 109 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 110 | def Validate(self, data): |
| 111 | """Validates the given data against the Scalar schema. |
| 112 | |
| 113 | It checks if the data's type matches the Scalar's element type. Also, it |
| 114 | checks if the data's value matches the Scalar's value if the required value |
| 115 | is specified. |
| 116 | |
| 117 | Args: |
| 118 | data: A Python data structure to be validated. |
| 119 | |
| 120 | Raises: |
| 121 | SchemaException if validation fails. |
| 122 | """ |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 123 | if not isinstance(data, self.element_type): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 124 | raise SchemaException('Type mismatch on %r: expected %r, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 125 | (data, self.element_type, type(data))) |
| 126 | if self.choices and data not in self.choices: |
Jon Salz | 05fffde | 2014-07-14 12:56:47 +0800 | [diff] [blame] | 127 | raise SchemaException('Value mismatch on %r: expected one of %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 128 | (data, sorted(self.choices))) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 129 | |
| 130 | |
Yong Hong | 5961ad3 | 2019-05-14 17:43:18 +0800 | [diff] [blame] | 131 | class RegexpStr(Scalar): |
| 132 | """Schema class for a string which matches the specific regular expression. |
| 133 | |
| 134 | Attributes: |
| 135 | label: A human-readable string to describe this Scalar. |
| 136 | regexp: A regular expression object to match. |
| 137 | |
| 138 | Raises: |
| 139 | SchemaException if argument format is incorrect. |
| 140 | """ |
| 141 | |
| 142 | def __init__(self, label, regexp): |
| 143 | super(RegexpStr, self).__init__(label, str) |
| 144 | self.regexp = regexp |
| 145 | |
| 146 | def __repr__(self): |
| 147 | return 'RegexpStr(%r, %s)' % (self.label, self.regexp.pattern) |
| 148 | |
| 149 | def __deepcopy__(self, memo): |
| 150 | return RegexpStr(self.label, self.regexp) |
| 151 | |
| 152 | def Validate(self, data): |
| 153 | """Validates the given data against the RegexpStr schema. |
| 154 | |
| 155 | It first checks if the data's type is `str`. Then, it checks if the |
| 156 | value matches the regular expression. |
| 157 | |
| 158 | Args: |
| 159 | data: A Python data structure to be validated. |
| 160 | |
| 161 | Raises: |
| 162 | SchemaException if validation fails. |
| 163 | """ |
| 164 | super(RegexpStr, self).Validate(data) |
| 165 | if not self.regexp.match(data): |
| 166 | raise SchemaException("Value %r doesn't match regeular expression %s" % |
| 167 | (data, self.regexp.pattern)) |
| 168 | |
| 169 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 170 | class Dict(BaseType): |
| 171 | """Dict schema class. |
| 172 | |
| 173 | This schema class is used to verify simple dict. Only the key type and value |
| 174 | type are validated. |
| 175 | |
| 176 | Attributes: |
| 177 | label: A human-readable string to describe this Scalar. |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 178 | key_type: A schema object indicating the schema of the keys of this Dict. It |
| 179 | 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] | 180 | value_type: A schema object indicating the schema of the values of this |
| 181 | Dict. |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 182 | min_size: The minimum size of the elements, default to 0. |
| 183 | max_size: None or the maximum size of the elements. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 184 | |
| 185 | Raises: |
| 186 | SchemaException if argument format is incorrect. |
| 187 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 188 | |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 189 | 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] | 190 | super(Dict, self).__init__(label) |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 191 | if not (isinstance(key_type, Scalar) or |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 192 | (isinstance(key_type, AnyOf) and |
| 193 | key_type.CheckTypeOfPossibleValues(Scalar))): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 194 | raise SchemaException('key_type %r of Dict %r is not Scalar' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 195 | (key_type, self.label)) |
| 196 | self.key_type = key_type |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 197 | if not isinstance(value_type, BaseType): |
| 198 | raise SchemaException('value_type %r of Dict %r is not Schema object' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 199 | (value_type, self.label)) |
| 200 | self.value_type = value_type |
| 201 | self.min_size = min_size |
| 202 | self.max_size = max_size |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 203 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 204 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 205 | size_expr = '[%d, %s]' % (self.min_size, ('inf' if self.max_size is None |
| 206 | else '%d' % self.max_size)) |
| 207 | return 'Dict(%r, key_type=%r, value_type=%r, size=%s)' % ( |
| 208 | self.label, self.key_type, self.value_type, size_expr) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 209 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 210 | def Validate(self, data): |
| 211 | """Validates the given data against the Dict schema. |
| 212 | |
| 213 | It checks that all the keys in data matches the schema defined by key_type, |
| 214 | and all the values in data matches the schema defined by value_type. |
| 215 | |
| 216 | Args: |
| 217 | data: A Python data structure to be validated. |
| 218 | |
| 219 | Raises: |
| 220 | SchemaException if validation fails. |
| 221 | """ |
| 222 | if not isinstance(data, dict): |
| 223 | raise SchemaException('Type mismatch on %r: expected dict, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 224 | (self.label, type(data))) |
| 225 | |
| 226 | if len(data) < self.min_size: |
| 227 | raise SchemaException('Size mismatch on %r: expected size >= %r' % |
| 228 | (self.label, self.min_size)) |
| 229 | |
| 230 | if self.max_size is not None and self.max_size < len(data): |
| 231 | raise SchemaException('Size mismatch on %r: expected size <= %r' % |
| 232 | (self.label, self.max_size)) |
| 233 | |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame] | 234 | for k, v in data.items(): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 235 | self.key_type.Validate(k) |
| 236 | self.value_type.Validate(v) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 237 | |
| 238 | |
| 239 | class FixedDict(BaseType): |
| 240 | """FixedDict schema class. |
| 241 | |
| 242 | FixedDict is a Dict with predefined allowed keys. And each key corresponds to |
| 243 | a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues |
| 244 | in XML. |
| 245 | |
| 246 | An example FixedDict schema: |
| 247 | FixedDict('foo', |
| 248 | items={ |
| 249 | 'a': Scalar('bar', str), |
| 250 | 'b': Scalar('buz', int) |
| 251 | }, optional_items={ |
| 252 | 'c': Scalar('boo', int) |
| 253 | }) |
| 254 | |
| 255 | Attributes: |
| 256 | label: A human-readable string to describe this dict. |
| 257 | items: A dict of required items that must be specified. |
| 258 | optional_items: A dict of optional items. |
Yong Hong | 5961ad3 | 2019-05-14 17:43:18 +0800 | [diff] [blame] | 259 | allow_undefined_keys: A boolean that indicates whether additional items |
| 260 | that is not recorded in both `items` and `optional_items` are allowed |
| 261 | or not. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 262 | |
| 263 | Raises: |
| 264 | SchemaException if argument format is incorrect. |
| 265 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 266 | |
Yong Hong | 5961ad3 | 2019-05-14 17:43:18 +0800 | [diff] [blame] | 267 | def __init__(self, label, items=None, optional_items=None, |
| 268 | allow_undefined_keys=False): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 269 | super(FixedDict, self).__init__(label) |
| 270 | if items and not isinstance(items, dict): |
| 271 | raise SchemaException('items of FixedDict %r should be a dict' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 272 | self.label) |
| 273 | self.items = copy.deepcopy(items) if items is not None else {} |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 274 | if optional_items and not isinstance(optional_items, dict): |
| 275 | raise SchemaException('optional_items of FixedDict %r should be a dict' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 276 | self.label) |
| 277 | self.optional_items = ( |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 278 | copy.deepcopy(optional_items) if optional_items is not None else {}) |
Yong Hong | 5961ad3 | 2019-05-14 17:43:18 +0800 | [diff] [blame] | 279 | self.allow_undefined_keys = allow_undefined_keys |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 280 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 281 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 282 | return 'FixedDict(%r, items=%r, optional_items=%r)' % (self.label, |
| 283 | self.items, |
| 284 | self.optional_items) |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 285 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 286 | def Validate(self, data): |
| 287 | """Validates the given data and all its key-value pairs against the Dict |
| 288 | schema. |
| 289 | |
| 290 | If a key of Dict's type is required, then it must exist in the data's keys. |
Yong Hong | 5961ad3 | 2019-05-14 17:43:18 +0800 | [diff] [blame] | 291 | If `self.allow_undefined_keys` is `False` and some items in the given data |
| 292 | are not in either `self.items` or `self.optional_items`, the method will |
| 293 | raise `SchemaException`. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 294 | |
| 295 | Args: |
| 296 | data: A Python data structure to be validated. |
| 297 | |
| 298 | Raises: |
| 299 | SchemaException if validation fails. |
| 300 | """ |
| 301 | if not isinstance(data, dict): |
| 302 | raise SchemaException('Type mismatch on %r: expected dict, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 303 | (self.label, type(data))) |
Yilin Yang | 78fa12e | 2019-09-25 14:21:10 +0800 | [diff] [blame] | 304 | data_key_list = list(data) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 305 | # Check that every key-value pair in items exists in data |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame] | 306 | for key, value_schema in self.items.items(): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 307 | if key not in data: |
| 308 | raise SchemaException( |
| 309 | 'Required item %r does not exist in FixedDict %r' % |
| 310 | (key, data)) |
| 311 | value_schema.Validate(data[key]) |
| 312 | data_key_list.remove(key) |
| 313 | # Check that all the remaining unmatched key-value pairs matches any |
| 314 | # definition in items or optional_items. |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame] | 315 | for key, value_schema in self.optional_items.items(): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 316 | if key not in data: |
| 317 | continue |
| 318 | value_schema.Validate(data[key]) |
| 319 | data_key_list.remove(key) |
Yong Hong | 5961ad3 | 2019-05-14 17:43:18 +0800 | [diff] [blame] | 320 | if not self.allow_undefined_keys and data_key_list: |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 321 | raise SchemaException('Keys %r are undefined in FixedDict %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 322 | (data_key_list, self.label)) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 323 | |
| 324 | |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 325 | class JSONSchemaDict(BaseType): |
| 326 | """JSON schema class. |
| 327 | |
| 328 | This schema class allows mixing JSON schema with other schema types. |
| 329 | |
| 330 | Attributes: |
| 331 | label: A human-readable string to describe this JSON schema. |
| 332 | schema: a JSON schema object. |
| 333 | |
| 334 | Raises: |
Fei Shao | 186d25b | 2018-11-09 16:55:48 +0800 | [diff] [blame] | 335 | SchemaException if given schema is invalid (SchemaError) or fail |
| 336 | to validate data using the schema (ValidationError). |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 337 | """ |
| 338 | def __init__(self, label, schema): |
| 339 | super(JSONSchemaDict, self).__init__(label) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 340 | self.label = label |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 341 | if _HAVE_JSONSCHEMA: |
Fei Shao | 186d25b | 2018-11-09 16:55:48 +0800 | [diff] [blame] | 342 | try: |
| 343 | jsonschema.Draft4Validator.check_schema(schema) |
| 344 | except Exception as e: |
| 345 | raise SchemaException('Schema %r is invalid: %r' % (schema, e)) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 346 | self.schema = schema |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 347 | |
| 348 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 349 | return 'JSONSchemaDict(%r, %r)' % (self.label, self.schema) |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 350 | |
| 351 | def Validate(self, data): |
Chih-Wei Ning | 8da12ec | 2017-08-29 12:10:02 +0800 | [diff] [blame] | 352 | if _HAVE_JSONSCHEMA: |
Fei Shao | 186d25b | 2018-11-09 16:55:48 +0800 | [diff] [blame] | 353 | try: |
| 354 | jsonschema.validate(data, self.schema) |
| 355 | except Exception as e: |
| 356 | raise SchemaException('Fail to validate %r with JSON schema %r: %r' % |
| 357 | (data, self.schema, e)) |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 358 | |
Cheng Yueh | f5bf58f | 2020-12-30 15:49:47 +0800 | [diff] [blame] | 359 | def CreateOptional(self): |
| 360 | """Creates a new schema that accepts null and itself.""" |
| 361 | return JSONSchemaDict(f'{self.label} or null', |
| 362 | {'anyOf': [ |
| 363 | { |
| 364 | 'type': 'null' |
| 365 | }, |
| 366 | self.schema, |
| 367 | ]}) |
| 368 | |
Chih-wei Ning | 5fbaf44 | 2017-08-15 21:34:53 +0800 | [diff] [blame] | 369 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 370 | class List(BaseType): |
| 371 | """List schema class. |
| 372 | |
| 373 | Attributes: |
| 374 | label: A string to describe this list. |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 375 | element_type: Optional schema object to validate the elements of the list. |
| 376 | Default None means no validation of elements' type. |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 377 | min_length: The expected minimum length of the list. Default to 0. |
| 378 | max_length: None or the limit of the length. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 379 | |
| 380 | Raises: |
| 381 | SchemaException if argument format is incorrect. |
| 382 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 383 | |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 384 | 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] | 385 | super(List, self).__init__(label) |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 386 | if element_type and not isinstance(element_type, BaseType): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 387 | raise SchemaException( |
| 388 | 'element_type %r of List %r is not a Schema object' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 389 | (element_type, self.label)) |
| 390 | self.element_type = copy.deepcopy(element_type) |
| 391 | self.min_length = min_length |
| 392 | self.max_length = max_length |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 393 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 394 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 395 | max_bound_repr = ('inf' if self.max_length is None |
| 396 | else '%d' % self.max_length) |
| 397 | return 'List(%r, %r, [%r, %s])' % ( |
| 398 | self.label, self.element_type, self.min_length, max_bound_repr) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 399 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 400 | def Validate(self, data): |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 401 | """Validates the given data and all its elements against the List schema. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 402 | |
| 403 | Args: |
| 404 | data: A Python data structure to be validated. |
| 405 | |
| 406 | Raises: |
| 407 | SchemaException if validation fails. |
| 408 | """ |
| 409 | if not isinstance(data, list): |
| 410 | raise SchemaException('Type mismatch on %r: expected list, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 411 | (self.label, type(data))) |
| 412 | |
| 413 | if len(data) < self.min_length: |
| 414 | raise SchemaException('Length mismatch on %r: expected length >= %d' % |
| 415 | (self.label, self.min_length)) |
| 416 | |
| 417 | if self.max_length is not None and self.max_length < len(data): |
| 418 | raise SchemaException('Length mismatch on %r: expected length <= %d' % |
| 419 | (self.label, self.max_length)) |
| 420 | |
| 421 | if self.element_type: |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 422 | for data_value in data: |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 423 | self.element_type.Validate(data_value) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 424 | |
| 425 | |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 426 | class Tuple(BaseType): |
| 427 | """Tuple schema class. |
| 428 | |
| 429 | Comparing to List, the Tuple schema makes sure that every element exactly |
| 430 | matches the defined position and schema. |
| 431 | |
| 432 | Attributes: |
| 433 | label: A string to describe this tuple. |
| 434 | element_types: Optional list or tuple schema object to describe the |
| 435 | types of the Tuple. |
| 436 | |
| 437 | Raises: |
| 438 | SchemaException if argument format is incorrect. |
| 439 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 440 | |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 441 | def __init__(self, label, element_types=None): |
| 442 | super(Tuple, self).__init__(label) |
| 443 | if (element_types and |
| 444 | (not isinstance(element_types, (tuple, list))) or |
| 445 | (not all([isinstance(x, BaseType)] for x in element_types))): |
| 446 | raise SchemaException( |
| 447 | 'element_types %r of Tuple %r is not a tuple or list' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 448 | (element_types, self.label)) |
| 449 | self.element_types = copy.deepcopy(element_types) |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 450 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 451 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 452 | return 'Tuple(%r, %r)' % (self.label, self.element_types) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 453 | |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 454 | def Validate(self, data): |
| 455 | """Validates the given data and all its elements against the Tuple schema. |
| 456 | |
| 457 | Args: |
| 458 | data: A Python data structure to be validated. |
| 459 | |
| 460 | Raises: |
| 461 | SchemaException if validation fails. |
| 462 | """ |
| 463 | if not isinstance(data, tuple): |
| 464 | raise SchemaException('Type mismatch on %r: expected tuple, got %r' % |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 465 | (self.label, type(data))) |
| 466 | if self.element_types and len(self.element_types) != len(data): |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 467 | raise SchemaException( |
| 468 | 'Number of elements in tuple %r does not match that defined ' |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 469 | 'in Tuple schema %r' % (str(data), self.label)) |
Yilin Yang | d3d97b0 | 2020-01-14 16:46:33 +0800 | [diff] [blame] | 470 | for content, element_type in zip(data, self.element_types): |
| 471 | element_type.Validate(content) |
Ricky Liang | 3e5342b | 2013-03-08 12:16:25 +0800 | [diff] [blame] | 472 | |
| 473 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 474 | class AnyOf(BaseType): |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 475 | """A Schema class which accepts any one of the given Schemas. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 476 | |
| 477 | Attributes: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 478 | types: A list of Schema objects to be matched. |
| 479 | label: An optional string to describe this AnyOf type. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 480 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 481 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 482 | def __init__(self, types, label=None): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 483 | super(AnyOf, self).__init__(label) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 484 | if (not isinstance(types, list) or |
| 485 | not all([isinstance(x, BaseType) for x in types])): |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 486 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 487 | 'types in AnyOf(types=%r%s) should be a list of Schemas' % |
| 488 | (types, '' if label is None else ', label=%r' % label)) |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 489 | self.types = list(types) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 490 | |
| 491 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 492 | label = '' if self.label is None else ', label=%r' % self.label |
| 493 | return 'AnyOf(%r%s)' % (self.types, label) |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 494 | |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 495 | def CheckTypeOfPossibleValues(self, schema_type): |
| 496 | """Checks if the acceptable types are of the same type as schema_type. |
| 497 | |
| 498 | Args: |
| 499 | schema_type: The schema type to check against with. |
| 500 | """ |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 501 | return all([isinstance(k, schema_type) for k in self.types]) |
Ricky Liang | f5386b3 | 2013-03-11 16:40:45 +0800 | [diff] [blame] | 502 | |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 503 | def Validate(self, data): |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 504 | """Validates if the given data matches any schema in types |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 505 | |
| 506 | Args: |
| 507 | data: A Python data structue to be validated. |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 508 | |
| 509 | Raises: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 510 | SchemaException if no schemas in types validates the input data. |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 511 | """ |
| 512 | match = False |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 513 | for schema_type in self.types: |
Ricky Liang | 1b72ccf | 2013-03-01 10:23:01 +0800 | [diff] [blame] | 514 | try: |
| 515 | schema_type.Validate(data) |
| 516 | except SchemaException: |
| 517 | continue |
| 518 | match = True |
| 519 | break |
| 520 | if not match: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 521 | raise SchemaException('%r does not match any type in %r' % (data, |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 522 | self.types)) |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 523 | |
| 524 | |
| 525 | class Optional(AnyOf): |
| 526 | """A Schema class which accepts either None or given Schemas. |
| 527 | |
| 528 | It is a special case of AnyOf class: in addition of given schema(s), it also |
| 529 | accepts None. |
| 530 | |
| 531 | Attributes: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 532 | types: A (or a list of) Schema object(s) to be matched. |
| 533 | label: An optional string to describe this Optional type. |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 534 | """ |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 535 | |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 536 | def __init__(self, types, label=None): |
| 537 | try: |
| 538 | super(Optional, self).__init__(MakeList(types), label=label) |
| 539 | except SchemaException: |
| 540 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 541 | 'types in Optional(types=%r%s) should be a Schema or a list of ' |
| 542 | 'Schemas' % (types, '' if label is None else ', label=%r' % label)) |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 543 | |
| 544 | def __repr__(self): |
Yong Hong | 3532ae8 | 2017-12-29 16:05:46 +0800 | [diff] [blame] | 545 | label = '' if self.label is None else ', label=%r' % self.label |
| 546 | return 'Optional(%r%s)' % (self.types, label) |
Dean Liao | 452c6ee | 2013-03-11 16:23:33 +0800 | [diff] [blame] | 547 | |
| 548 | def Validate(self, data): |
| 549 | """Validates if the given data is None or matches any schema in types. |
| 550 | |
| 551 | Args: |
| 552 | data: A Python data structue to be validated. |
| 553 | |
| 554 | Raises: |
| 555 | SchemaException if data is not None and no schemas in types validates the |
| 556 | input data. |
| 557 | """ |
| 558 | if data is None: |
| 559 | return |
| 560 | try: |
| 561 | super(Optional, self).Validate(data) |
| 562 | except SchemaException: |
Dean Liao | 604e62b | 2013-03-11 19:12:50 +0800 | [diff] [blame] | 563 | raise SchemaException( |
Hung-Te Lin | 56b1840 | 2015-01-16 14:52:30 +0800 | [diff] [blame] | 564 | '%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] | 565 | self.types)) |