From 30570bd89c04a56b35b91a0bc1d5fc00bb6ad266 Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 24 Sep 2012 22:21:50 +0900 Subject: add schema to JSONLeapConfig classes and a jsonvalidate function too, that calls to jsonchemea.validate(self, data) with self.schema We're using the specs to both purposes now: * providing a type casting system for our config options (work in progress for the type casting) * json schema validation --- src/leap/base/config.py | 58 ++++++++++++++++++---- src/leap/base/specs.py | 98 ++++++++++++++++++++------------------ src/leap/base/tests/test_config.py | 4 +- 3 files changed, 102 insertions(+), 58 deletions(-) (limited to 'src/leap/base') diff --git a/src/leap/base/config.py b/src/leap/base/config.py index 79185976..3854c2c2 100644 --- a/src/leap/base/config.py +++ b/src/leap/base/config.py @@ -12,6 +12,7 @@ logger = logging.getLogger(name=__name__) logger.setLevel('DEBUG') import configuration +import jsonschema import requests from leap.base import exceptions @@ -46,30 +47,58 @@ class BaseLeapConfig(object): raise NotImplementedError("abstract base class") +class SchemaEncoder(json.JSONEncoder): + def default(self, obj): + if obj is str: + return 'string' + if obj is unicode: + return 'string' + if obj is int: + return 'int' + if obj is list: + return 'array' + if obj is dict: + return object + + class MetaConfigWithSpec(type): """ metaclass for JSONLeapConfig classes. It creates a configuration spec out of - the `spec` dictionary. + the `spec` dictionary. The `properties` attribute + of the spec dict is turn into the `schema` attribute + of the new class (which will be used to validate against). """ # XXX in the near future, this is the # place where we want to enforce - # singletons, read-only and stuff. + # singletons, read-only and similar stuff. # TODO: # - add a error handler for missing options that # we can act easily upon (sys.exit is ugly, for $deity's sake) def __new__(meta, classname, bases, classDict): - spec_options = classDict.get('spec', None) + schema_obj = classDict.get('spec', None) + if schema_obj: + spec_options = schema_obj.get('properties', None) + schema_json = SchemaEncoder().encode(schema_obj) + schema = json.loads(schema_json) + else: + spec_options = None + schema = None # not quite happy with this workaround. # I want to raise if missing spec dict, but only # for grand-children of this metaclass. # maybe should use abc module for this. abcderived = ("JSONLeapConfig",) if spec_options is None and classname not in abcderived: - raise exceptions.ImproperlyConfigured( - "missing spec dict on your derived class") + if not schema_obj: + raise exceptions.ImproperlyConfigured( + "missing spec dict on your derived class (%s)" % classname) + if schema_obj and not spec_options: + raise exceptions.ImproperlyConfigured( + "missing properties attr in spec dict " + "on your derived class (%s)" % classname) # we create a configuration spec attribute from the spec dict config_class = type( @@ -77,6 +106,8 @@ class MetaConfigWithSpec(type): (configuration.Configuration, object), {'options': spec_options}) classDict['spec'] = config_class + # A shipped json-schema for validation + classDict['schema'] = schema return type.__new__(meta, classname, bases, classDict) @@ -96,8 +127,8 @@ class MetaConfigWithSpec(type): # - get_config (returns a optparse.OptionParser object) # TODO: +# [done] raise validation errors # - have a good type cast repertory (uris, version, hashes...) -# - raise validation errors # - multilingual objects ########################################################## @@ -151,9 +182,14 @@ class JSONLeapConfig(BaseLeapConfig): return if fromfile is None: fromfile = self.filename - newconfig = self._config.deserialize(fromfile) - # XXX check for no errors, etc - self._config.config = newconfig + if os.path.isfile(fromfile): + newconfig = self._config.deserialize(fromfile) + # XXX check for no errors, etc + # XXX could validate here! + self._config.config = newconfig + else: + logger.error('tried to load config from non-existent path') + logger.error('Not Found: %s', fromfile) def fetch(self, uri, fetcher=None, verify=True): if not fetcher: @@ -187,6 +223,10 @@ class JSONLeapConfig(BaseLeapConfig): def filename(self): return self.get_filename() + def jsonvalidate(self, data): + jsonschema.validate(data, self.schema) + return True + # private def _loadtemp(self, filename): diff --git a/src/leap/base/specs.py b/src/leap/base/specs.py index d88dc63f..e75eca70 100644 --- a/src/leap/base/specs.py +++ b/src/leap/base/specs.py @@ -1,49 +1,53 @@ leap_provider_spec = { - 'serial': { - 'type': int, - 'default': 1, - 'required': True, - }, - 'version': { - 'type': unicode, - 'default': '0.1.0' - #'required': True - }, - 'domain': { - 'type': unicode, # XXX define uri type - 'default': 'testprovider.example.org' - #'required': True, - }, - 'display_name': { - 'type': unicode, # XXX multilingual object? - 'default': 'test provider' - #'required': True - }, - 'description': { - 'default': 'test provider' - }, - 'enrollment_policy': { - 'type': unicode, # oneof ?? - 'default': 'open' - }, - 'services': { - 'type': list, # oneof ?? - 'default': ['eip'] - }, - 'api_version': { - 'type': unicode, - 'default': '0.1.0' # version regexp - }, - 'api_uri': { - 'type': unicode # uri - }, - 'public_key': { - 'type': unicode # fingerprint - }, - 'ca_cert': { - 'type': unicode - }, - 'ca_cert_uri': { - 'type': unicode - }, + 'description': 'provider definition', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': True, + }, + 'version': { + 'type': unicode, + 'default': '0.1.0' + #'required': True + }, + 'domain': { + 'type': unicode, # XXX define uri type + 'default': 'testprovider.example.org' + #'required': True, + }, + 'display_name': { + 'type': unicode, # XXX multilingual object? + 'default': 'test provider' + #'required': True + }, + 'description': { + 'default': 'test provider' + }, + 'enrollment_policy': { + 'type': unicode, # oneof ?? + 'default': 'open' + }, + 'services': { + 'type': list, # oneof ?? + 'default': ['eip'] + }, + 'api_version': { + 'type': unicode, + 'default': '0.1.0' # version regexp + }, + 'api_uri': { + 'type': unicode # uri + }, + 'public_key': { + 'type': unicode # fingerprint + }, + 'ca_cert': { + 'type': unicode + }, + 'ca_cert_uri': { + 'type': unicode + } + } } diff --git a/src/leap/base/tests/test_config.py b/src/leap/base/tests/test_config.py index bede5ea1..d03149b2 100644 --- a/src/leap/base/tests/test_config.py +++ b/src/leap/base/tests/test_config.py @@ -38,14 +38,14 @@ class JSONLeapConfigTest(BaseLeapTest): class DummyTestConfig(config.JSONLeapConfig): __metaclass__ = config.MetaConfigWithSpec - spec = {} + spec = {'properties': {}} with self.assertRaises(exceptions.ImproperlyConfigured) as exc: DummyTestConfig() exc.startswith("missing slug") class DummyTestConfig(config.JSONLeapConfig): __metaclass__ = config.MetaConfigWithSpec - spec = {} + spec = {'properties': {}} slug = "foo" DummyTestConfig() -- cgit v1.2.3