blob: 59c1bfaf6f252c2b3fb6c6426203fd7df85fd595 [file] [log] [blame]
#!/usr/bin/python -u
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# pylint: disable=W0212, W0622
"""A function to create a schema tree from the given schema expression.
For example:
1. This is the schema of the encoded_fields in component database.
Dict('encoded_fields', Scalar('encoded_field', str),
Dict('encoded_indices', Scalar('encoded_index', int),
Dict('component_classes', Scalar('component_class', str),
AnyOf('component_names', [
Scalar('component_name', str),
List('list_of_component_names', Scalar('component_name', str)),
Scalar('none', type(None))
])
)
)
)
2. This is the schema of the pattern in component database.
List('pattern',
Dict('pattern_field', key_type=Scalar('encoded_index', str),
value_type=Scalar('bit_offset', int))
)
3. This is the schema of the components in component database.
Dict('components', Scalar('component_class', str),
Dict('component_names', Scalar('component_name', str),
FixedDict('component_attributes',
items={
'value': AnyOf('probed_value', [
Scalar('probed_value', str),
List('list_of_probed_values', Scalar('probed_value', str))
])
},
optional_items={
'labels': List('list_of_labels', Scalar('label', str))
}
)
)
)
"""
import copy
import factory_common # pylint: disable=W0611
from cros.factory.common import MakeList
class SchemaException(Exception):
pass
class BaseType(object):
"""Base type class for schema classes.
"""
def __init__(self, label):
self._label = label
def __repr__(self):
return 'BaseType(%r)' % self._label
def Validate(self, data):
raise NotImplementedError
class Scalar(BaseType):
"""Scalar schema class.
Attributes:
label: A human-readable string to describe this Scalar.
element_type: The Python type of this Scalar. Cannot be a iterable type.
choices: A set of allowable choices for the scalar, or None to allow
any values of the given type.
Raises:
SchemaException if argument format is incorrect.
"""
def __init__(self, label, element_type, choices=None):
super(Scalar, self).__init__(label)
if getattr(element_type, '__iter__', None):
raise SchemaException(
'element_type %r of Scalar %r is not a scalar type' % (element_type,
label))
self._element_type = element_type
self._choices = set(choices) if choices else None
def __repr__(self):
return 'Scalar(%r, %r%s)' % (
self._label, self._element_type,
', choices=%r' % sorted(self._choices) if self._choices else '')
def Validate(self, data):
"""Validates the given data against the Scalar schema.
It checks if the data's type matches the Scalar's element type. Also, it
checks if the data's value matches the Scalar's value if the required value
is specified.
Args:
data: A Python data structure to be validated.
Raises:
SchemaException if validation fails.
"""
if not isinstance(data, self._element_type):
raise SchemaException('Type mismatch on %r: expected %r, got %r' %
(data, self._element_type, type(data)))
if self._choices and data not in self._choices:
raise SchemaException('Value mismatch on %r: expected one of %r' %
(data, sorted(self._choices)))
class Dict(BaseType):
"""Dict schema class.
This schema class is used to verify simple dict. Only the key type and value
type are validated.
Attributes:
label: A human-readable string to describe this Scalar.
key_type: A schema object indicating the schema of the keys of this Dict. It
can be a Scalar or an AnyOf with possible values being all Scalars.
value_type: A schema object indicating the schema of the values of this
Dict.
Raises:
SchemaException if argument format is incorrect.
"""
def __init__(self, label, key_type, value_type):
super(Dict, self).__init__(label)
if not (isinstance(key_type, Scalar) or
(isinstance(key_type, AnyOf) and
key_type.CheckTypeOfPossibleValues(Scalar))):
raise SchemaException('key_type %r of Dict %r is not Scalar' %
(key_type, self._label))
self._key_type = key_type
if not isinstance(value_type, BaseType):
raise SchemaException('value_type %r of Dict %r is not Schema object' %
(value_type, self._label))
self._value_type = value_type
def __repr__(self):
return 'Dict(%r, key_type=%r, value_type=%r)' % (self._label,
self._key_type,
self._value_type)
def Validate(self, data):
"""Validates the given data against the Dict schema.
It checks that all the keys in data matches the schema defined by key_type,
and all the values in data matches the schema defined by value_type.
Args:
data: A Python data structure to be validated.
Raises:
SchemaException if validation fails.
"""
if not isinstance(data, dict):
raise SchemaException('Type mismatch on %r: expected dict, got %r' %
(self._label, type(data)))
for k, v in data.iteritems():
self._key_type.Validate(k)
self._value_type.Validate(v)
class FixedDict(BaseType):
"""FixedDict schema class.
FixedDict is a Dict with predefined allowed keys. And each key corresponds to
a value type. The analogy of Dict vs. FixedDict can be Elements vs. Attribues
in XML.
An example FixedDict schema:
FixedDict('foo',
items={
'a': Scalar('bar', str),
'b': Scalar('buz', int)
}, optional_items={
'c': Scalar('boo', int)
})
Attributes:
label: A human-readable string to describe this dict.
items: A dict of required items that must be specified.
optional_items: A dict of optional items.
Raises:
SchemaException if argument format is incorrect.
"""
def __init__(self, label, items=None, optional_items=None):
super(FixedDict, self).__init__(label)
if items and not isinstance(items, dict):
raise SchemaException('items of FixedDict %r should be a dict' %
self._label)
self._items = copy.deepcopy(items) if items is not None else {}
if optional_items and not isinstance(optional_items, dict):
raise SchemaException('optional_items of FixedDict %r should be a dict' %
self._label)
self._optional_items = (
copy.deepcopy(optional_items) if optional_items is not None else {})
def __repr__(self):
return 'FixedDict(%r, items=%r, optional_items=%r)' % (self._label,
self._items,
self._optional_items)
def Validate(self, data):
"""Validates the given data and all its key-value pairs against the Dict
schema.
If a key of Dict's type is required, then it must exist in the data's keys.
Args:
data: A Python data structure to be validated.
Raises:
SchemaException if validation fails.
"""
if not isinstance(data, dict):
raise SchemaException('Type mismatch on %r: expected dict, got %r' %
(self._label, type(data)))
data_key_list = data.keys()
# Check that every key-value pair in items exists in data
for key, value_schema in self._items.iteritems():
if key not in data:
raise SchemaException(
'Required item %r does not exist in FixedDict %r' %
(key, data))
value_schema.Validate(data[key])
data_key_list.remove(key)
# Check that all the remaining unmatched key-value pairs matches any
# definition in items or optional_items.
for key, value_schema in self._optional_items.iteritems():
if key not in data:
continue
value_schema.Validate(data[key])
data_key_list.remove(key)
if data_key_list:
raise SchemaException('Keys %r are undefined in FixedDict %r' %
(data_key_list, self._label))
class List(BaseType):
"""List schema class.
Attributes:
label: A string to describe this list.
element_type: Optional schema object to validate the elements of the list.
Default None means no validation of elements' type.
Raises:
SchemaException if argument format is incorrect.
"""
def __init__(self, label, element_type=None):
super(List, self).__init__(label)
if element_type and not isinstance(element_type, BaseType):
raise SchemaException(
'element_type %r of List %r is not a Schema object' %
(element_type, self._label))
self._element_type = copy.deepcopy(element_type)
def __repr__(self):
return 'List(%r, %r)' % (self._label, self._element_type)
def Validate(self, data):
"""Validates the given data and all its elements against the List schema.
Args:
data: A Python data structure to be validated.
Raises:
SchemaException if validation fails.
"""
if not isinstance(data, list):
raise SchemaException('Type mismatch on %r: expected list, got %r' %
(self._label, type(data)))
if self._element_type:
for data_value in data:
self._element_type.Validate(data_value)
class Tuple(BaseType):
"""Tuple schema class.
Comparing to List, the Tuple schema makes sure that every element exactly
matches the defined position and schema.
Attributes:
label: A string to describe this tuple.
element_types: Optional list or tuple schema object to describe the
types of the Tuple.
Raises:
SchemaException if argument format is incorrect.
"""
def __init__(self, label, element_types=None):
super(Tuple, self).__init__(label)
if (element_types and
(not isinstance(element_types, (tuple, list))) or
(not all([isinstance(x, BaseType)] for x in element_types))):
raise SchemaException(
'element_types %r of Tuple %r is not a tuple or list' %
(element_types, self._label))
self._element_types = copy.deepcopy(element_types)
def __repr__(self):
return 'Tuple(%r, %r)' % (self._label, self._element_types)
def Validate(self, data):
"""Validates the given data and all its elements against the Tuple schema.
Args:
data: A Python data structure to be validated.
Raises:
SchemaException if validation fails.
"""
if not isinstance(data, tuple):
raise SchemaException('Type mismatch on %r: expected tuple, got %r' %
(self._label, type(data)))
if self._element_types and len(self._element_types) != len(data):
raise SchemaException(
'Number of elements in tuple %r does not match that defined '
'in Tuple schema %r' % (str(data), self._label))
for data, element_type in zip(data, self._element_types):
element_type.Validate(data)
class AnyOf(BaseType):
"""A Schema class which accepts any one of the given Schemas.
Attributes:
types: A list of Schema objects to be matched.
label: An optional string to describe this AnyOf type.
"""
def __init__(self, types, label=None):
super(AnyOf, self).__init__(label)
if (not isinstance(types, list) or
not all([isinstance(x, BaseType) for x in types])):
raise SchemaException(
'types in AnyOf(types=%r%s) should be a list of Schemas' %
(types, '' if label is None else ', label=%r' % label))
self._types = list(types)
def __repr__(self):
label = '' if self._label is None else ', label=%r' % self._label
return 'AnyOf(%r%s)' % (self._types, label)
def CheckTypeOfPossibleValues(self, schema_type):
"""Checks if the acceptable types are of the same type as schema_type.
Args:
schema_type: The schema type to check against with.
"""
return all([isinstance(k, schema_type) for k in self._types])
def Validate(self, data):
"""Validates if the given data matches any schema in types
Args:
data: A Python data structue to be validated.
Raises:
SchemaException if no schemas in types validates the input data.
"""
match = False
for schema_type in self._types:
try:
schema_type.Validate(data)
except SchemaException:
continue
match = True
break
if not match:
raise SchemaException('%r does not match any type in %r' % (data,
self._types))
class Optional(AnyOf):
"""A Schema class which accepts either None or given Schemas.
It is a special case of AnyOf class: in addition of given schema(s), it also
accepts None.
Attributes:
types: A (or a list of) Schema object(s) to be matched.
label: An optional string to describe this Optional type.
"""
def __init__(self, types, label=None):
try:
super(Optional, self).__init__(MakeList(types), label=label)
except SchemaException:
raise SchemaException(
'types in Optional(types=%r%s) should be a Schema or a list of '
'Schemas' % (types, '' if label is None else ', label=%r' % label))
def __repr__(self):
label = '' if self._label is None else ', label=%r' % self._label
return 'Optional(%r%s)' % (self._types, label)
def Validate(self, data):
"""Validates if the given data is None or matches any schema in types.
Args:
data: A Python data structue to be validated.
Raises:
SchemaException if data is not None and no schemas in types validates the
input data.
"""
if data is None:
return
try:
super(Optional, self).Validate(data)
except SchemaException:
raise SchemaException(
'%r is not None and does not match any type in %r' % (data,
self._types))