checks: extend validator with 'alias'

When a property has 'alias' defined, and it's not present in instance,
if the alias property is present, add the property with value from alias
property before remove the alias property from instance.

Examples:

with schema:
{
        "$schema": "http://json-schema.org/draft-04/schema#",
        "title": "Pungi Configuration",
        "type": "object",
        "properties": {
            "release_name": {"type": "string", "alias": "product_name"},
        },
        "required": ["release_name"],
        "additionalProperties": False,
}

1. config = {"release_name": "dummy product"}:

   validate pass, config not changed after validation.

2. config = {"product_name": "dummy product"}:

   validate pass, config updated to the following after validation:

   config: {"release_name": "dummy product"}

3. config = {"name": "dummy product"}:

   validate fail, errror message is "Failed validation in : 'release_name' is a required property",
   and warning message is "WARNING: Unrecognized config option: name."

4. config = {"product_name": "dummy product", "release_name": "dummy product"}

   validate fail, error message is "Failed validation in : product_name is an alias of release_name, only one can be used."

Signed-off-by: Qixiang Wan <qwan@redhat.com>
This commit is contained in:
Qixiang Wan 2017-02-20 22:45:51 +08:00
parent 345308464f
commit c93207addb
2 changed files with 223 additions and 6 deletions

View File

@ -187,7 +187,7 @@ def validate(config):
Undefined values for which a default value exists will be filled in.
"""
schema = _make_schema()
DefaultValidator = _extend_with_default(jsonschema.Draft4Validator)
DefaultValidator = _extend_with_default_and_alias(jsonschema.Draft4Validator)
validator = DefaultValidator(schema,
{'array': (tuple, list),
'regex': (str, unicode)})
@ -235,22 +235,72 @@ UNKNOWN = 'WARNING: Unrecognized config option: {0}.'
UNKNOWN_SUGGEST = 'WARNING: Unrecognized config option: {0}. Did you mean {1}?'
def _extend_with_default(validator_class):
def _extend_with_default_and_alias(validator_class):
validate_properties = validator_class.VALIDATORS["properties"]
validate_type = validator_class.VALIDATORS['type']
validate_required = validator_class.VALIDATORS['required']
validate_additional_properties = validator_class.VALIDATORS['additionalProperties']
def set_defaults(validator, properties, instance, schema):
def _replace_alias(properties, instance, schema):
"""
If 'alias' is defined for a property, and the property is not present
in instance, add the property with value from the alias property to
instance before remove the alias property. If both the propery and its
alias are present, it will yield an error.
"""
for property, subschema in properties.iteritems():
if "alias" in subschema:
if property in instance and subschema['alias'] in instance:
# the order of validators is in random, so we remove the alias
# property at the first time when it's found, then validators
# won't raise the same error later.
instance.pop(subschema['alias'])
yield jsonschema.ValidationError(
"%s is an alias of %s, only one can be used." % (
subschema['alias'], property)
)
if property not in instance and subschema['alias'] in instance:
instance.setdefault(property, instance.pop(subschema['alias']))
def set_defaults_and_aliases(validator, properties, instance, schema):
"""
Assign default values to options that have them defined and are not
specified.
specified. And if a property has 'alias' defined and the property is
not specified, look for the alias property and copy alias property's
value to that property before remove the alias property.
"""
for property, subschema in properties.iteritems():
if "default" in subschema and property not in instance:
instance.setdefault(property, subschema["default"])
for error in _replace_alias(properties, instance, schema):
yield error
for error in validate_properties(validator, properties, instance, schema):
yield error
def ignore_alias_properties(validator, aP, instance, schema):
"""
If there is a property has alias defined in schema, and the property is not
present in instance, set the property with the value of alias property,
remove alias property from instance.
"""
properties = schema.get("properties", {})
for error in _replace_alias(properties, instance, schema):
yield error
for error in validate_additional_properties(validator, aP, instance, schema):
yield error
def validate_required_with_alias(validator, required, instance, schema):
properties = schema.get("properties", {})
for error in _replace_alias(properties, instance, schema):
yield error
for error in validate_required(validator, required, instance, schema):
yield error
def error_on_deprecated(validator, properties, instance, schema):
"""Unconditionally raise deprecation error if encountered."""
yield ConfigDeprecation(properties)
@ -273,9 +323,11 @@ def _extend_with_default(validator_class):
yield error
return jsonschema.validators.extend(
validator_class, {"properties": set_defaults,
validator_class, {"properties": set_defaults_and_aliases,
"deprecated": error_on_deprecated,
"type": validate_regex_type},
"type": validate_regex_type,
"required": validate_required_with_alias,
"additionalProperties": ignore_alias_properties},
)

View File

@ -10,6 +10,8 @@ import os
import sys
import StringIO
import kobo.conf
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from pungi import checks
@ -188,6 +190,169 @@ class CheckDependenciesTestCase(unittest.TestCase):
self.assertTrue(result)
class TestSchemaValidator(unittest.TestCase):
def _load_conf_from_string(self, string):
conf = kobo.conf.PyConfigParser()
conf.load_from_string(string)
return conf
@mock.patch('pungi.checks._make_schema')
def test_property(self, make_schema):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"type": "object",
"properties": {
"release_name": {"type": "string", "alias": "product_name"},
},
"additionalProperties": False,
"required": ["release_name"],
}
make_schema.return_value = schema
string = """
release_name = "dummy product"
"""
config = self._load_conf_from_string(string)
errors, warnings = checks.validate(config)
self.assertEqual(len(errors), 0)
self.assertEqual(len(warnings), 0)
self.assertEqual(config.get("release_name", None), "dummy product")
@mock.patch('pungi.checks._make_schema')
def test_alias_property(self, make_schema):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"type": "object",
"properties": {
"release_name": {"type": "string", "alias": "product_name"},
},
"additionalProperties": False,
}
make_schema.return_value = schema
string = """
product_name = "dummy product"
"""
config = self._load_conf_from_string(string)
errors, warnings = checks.validate(config)
self.assertEqual(len(errors), 0)
self.assertEqual(len(warnings), 0)
self.assertEqual(config.get("release_name", None), "dummy product")
@mock.patch('pungi.checks._make_schema')
def test_required_is_missing(self, make_schema):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"type": "object",
"properties": {
"release_name": {"type": "string", "alias": "product_name"},
},
"additionalProperties": False,
"required": ["release_name"],
}
make_schema.return_value = schema
string = """
name = "dummy product"
"""
config = self._load_conf_from_string(string)
errors, warnings = checks.validate(config)
self.assertEqual(len(errors), 1)
self.assertIn("Failed validation in : 'release_name' is a required property", errors)
self.assertEqual(len(warnings), 1)
self.assertIn("WARNING: Unrecognized config option: name.", warnings)
@mock.patch('pungi.checks._make_schema')
def test_required_is_in_alias(self, make_schema):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"type": "object",
"properties": {
"release_name": {"type": "string", "alias": "product_name"},
},
"additionalProperties": False,
"required": ["release_name"],
}
make_schema.return_value = schema
string = """
product_name = "dummy product"
"""
config = self._load_conf_from_string(string)
errors, warnings = checks.validate(config)
self.assertEqual(len(errors), 0)
self.assertEqual(len(warnings), 0)
self.assertEqual(config.get("release_name", None), "dummy product")
@mock.patch('pungi.checks._make_schema')
def test_redundant_alias(self, make_schema):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"type": "object",
"properties": {
"release_name": {"type": "string", "alias": "product_name"},
},
"additionalProperties": False,
"required": ["release_name"],
}
make_schema.return_value = schema
string = """
product_name = "dummy product"
release_name = "dummy product"
"""
config = self._load_conf_from_string(string)
errors, warnings = checks.validate(config)
self.assertEqual(len(errors), 1)
self.assertIn('Failed validation in : product_name is an alias of release_name, only one can be used.', errors)
self.assertEqual(len(warnings), 0)
self.assertEqual(config.get("release_name", None), "dummy product")
@mock.patch('pungi.checks._make_schema')
def test_properties_in_deep(self, make_schema):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"type": "object",
"properties": {
"release_name": {"type": "string", "alias": "product_name"},
"keys": {
"type": "array",
"items": {"type": "string"},
},
"foophase": {
"type": "object",
"properties": {
"repo": {"type": "string", "alias": "tree"},
},
"additionalProperties": False,
"required": ["repo"],
},
},
"additionalProperties": False,
"required": ["release_name"],
}
make_schema.return_value = schema
string = """
product_name = "dummy product"
foophase = {
"tree": "http://www.exampe.com/os"
}
"""
config = self._load_conf_from_string(string)
errors, warnings = checks.validate(config)
self.assertEqual(len(errors), 0)
self.assertEqual(len(warnings), 0)
self.assertEqual(config.get("release_name", None), "dummy product")
self.assertEqual(config.get("foophase", {}).get("repo", None), "http://www.exampe.com/os")
class TestUmask(unittest.TestCase):
def setUp(self):
self.orig_umask = os.umask(0)