summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYashvardhan Didwania <didwaniayashvardhan+github@gmail.com>2019-08-15 18:30:24 +0530
committerAlec Thomas <alec@swapoff.org>2019-08-15 23:00:24 +1000
commitf5e64dd7e26fb2de0ca4baec42c426c60cb061c2 (patch)
tree58acd9a8fe865b5212dc0a7e02dbe8c3c0182548
parent961f85fd809261cbe80ac037105a317db484cbe4 (diff)
downloadvoluptuous-f5e64dd7e26fb2de0ca4baec42c426c60cb061c2.tar.gz
Allow a discrminant field in validators (#368)
-rw-r--r--voluptuous/tests/tests.py26
-rw-r--r--voluptuous/validators.py51
2 files changed, 76 insertions, 1 deletions
diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py
index 1c144eb..3ff71e4 100644
--- a/voluptuous/tests/tests.py
+++ b/voluptuous/tests/tests.py
@@ -11,7 +11,7 @@ from voluptuous import (
Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA,
validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date,
Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self,
- raises)
+ raises, Union)
from voluptuous.humanize import humanize_error
from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper
@@ -1406,3 +1406,27 @@ def test_exclusive():
"two or more values in the same group of exclusion 'stuff' @ data[<stuff>]")
else:
assert False, "Did not raise Invalid for multiple values in Exclusive group"
+
+def test_any_with_discriminant():
+ schema = Schema({
+ 'implementation': Union({
+ 'type': 'A',
+ 'a-value': str,
+ }, {
+ 'type': 'B',
+ 'b-value': int,
+ }, {
+ 'type': 'C',
+ 'c-value': bool,
+ }, discriminant=lambda value, alternatives: filter(lambda v : v['type'] == value['type'], alternatives))
+ })
+ try:
+ schema({
+ 'implementation': {
+ 'type': 'C',
+ 'c-value': None,}
+ })
+ except MultipleInvalid as e:
+ assert_equal(str(e),'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']')
+ else:
+ assert False, "Did not raise correct Invalid"
diff --git a/voluptuous/validators.py b/voluptuous/validators.py
index 81566b9..328e2bf 100644
--- a/voluptuous/validators.py
+++ b/voluptuous/validators.py
@@ -193,15 +193,23 @@ class _WithSubValidators(object):
self.validators = validators
self.msg = kwargs.pop('msg', None)
self.required = kwargs.pop('required', False)
+ self.discriminant = kwargs.pop('discriminant', None)
def __voluptuous_compile__(self, schema):
self._compiled = []
+ self.schema = schema
for v in self.validators:
schema.required = self.required
self._compiled.append(schema._compile(v))
return self._run
def _run(self, path, value):
+ if self.discriminant is not None:
+ self._compiled = [
+ self.schema._compile(v)
+ for v in self.discriminant(value, self.validators)
+ ]
+
return self._exec(self._compiled, value, path)
def __call__(self, v):
@@ -262,6 +270,49 @@ class Any(_WithSubValidators):
# Convenience alias
Or = Any
+class Union(_WithSubValidators):
+ """Use the first validated value among those selected by discrminant.
+
+ :param msg: Message to deliver to user if validation fails.
+ :param discriminant(value, validators): Returns the filtered list of validators based on the value
+ :param kwargs: All other keyword arguments are passed to the sub-Schema constructors.
+ :returns: Return value of the first validator that passes.
+
+ >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'},
+ ... discriminant=lambda val, alt: filter(
+ ... lambda v : v['type'] == val['type'] , alt)))
+ >>> validate({'type':'a', 'a_val':'1'}) == {'type':'a', 'a_val':'1'}
+ True
+ >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"):
+ ... validate({'type':'b', 'b_val':'5'})
+
+ ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked
+
+ Without the discriminant, the exception would be "extra keys not allowed @ data['b_val']"
+ """
+
+ def _exec(self, funcs, v, path=None):
+ error = None
+ for func in funcs:
+ try:
+ if path is None:
+ return func(v)
+ else:
+ return func(path, v)
+ except Invalid as e:
+ if error is None or len(e.path) > len(error.path):
+ error = e
+ else:
+ if error:
+ raise error if self.msg is None else AnyInvalid(
+ self.msg, path=path)
+ raise AnyInvalid(self.msg or 'no valid value found',
+ path=path)
+
+
+# Convenience alias
+Switch = Union
+
class All(_WithSubValidators):
"""Value must pass all validators.