Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions yamale/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ def _validate(self, validator, data, path, strict):
elif isinstance(validator, val.Any):
errors += self._validate_any(validator, data, path, strict)

elif isinstance(validator, val.All):
errors += self._validate_all(validator, data, path, strict)

elif isinstance(validator, val.Subset):
errors += self._validate_subset(validator, data, path, strict)

Expand Down Expand Up @@ -177,6 +180,20 @@ def _validate_any(self, validator, data, path, strict):

return errors

def _validate_all(self, validator, data, path, strict):
if not validator.validators:
return []

errors = []
print("validator.validators", validator.validators)
Comment thread
qequ marked this conversation as resolved.
Outdated
# With 'all', every validator must succeed
for v in validator.validators:
err = self._validate(v, data, path, strict)
if err:
errors += err

return errors

def _validate_subset(self, validator, data, path, strict):
def _internal_validate(internal_data):
sub_errors = []
Expand Down
2 changes: 1 addition & 1 deletion yamale/syntax/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _validate_expr(call_node, validators):
elif isinstance(base_arg, ast.Call):
_validate_expr(base_arg, validators)
else:
raise SyntaxError("Argument values must either be constant literals, or else " "reference other validators.")
raise SyntaxError("Argument values must either be constant literals, or else reference other validators.")


def parse(validator_string, validators=None):
Expand Down
6 changes: 6 additions & 0 deletions yamale/tests/fixtures/all.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
number_range: all(num(min=5), num(max=10))
password: all(str(min=8), regex('^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$'))
email: all(str(), regex('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'))
mixed_list: list(all(num(min=0), num(max=100)))
nested:
value: all(int(min=10), int(max=20))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like each of these can be accomplished without all(), eg:

  • num(min=5, max=10)
  • str(min=8, matches={regex})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the examples in the tests might give the impression that all() is redundant with existing validators. My goal with those cases was more to confirm that the implementation was working correctly and integrating cleanly with the existing system, rather than to showcase complex validation logic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. To be clear, I don't think it's necessary to "showcase complexity", but rather to demonstrate utility. I agree there's a use case here involving composition of custom validators -- thanks for adding that to the README.

9 changes: 9 additions & 0 deletions yamale/tests/fixtures/all_bad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
number_range: 12 # Over max limit
password: "pass" # Too short and missing required characters
email: "invalid-email" # Not a valid email format
mixed_list:
- 42
- 150 # Over max limit
- 15
nested:
value: 5 # Under min limit
9 changes: 9 additions & 0 deletions yamale/tests/fixtures/all_good.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
number_range: 7
password: "Password123"
email: "test@example.com"
mixed_list:
- 42
- 78
- 15
nested:
value: 15
6 changes: 6 additions & 0 deletions yamale/tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

semver = {"schema": "semver_schema.yaml", "good": "semver_good.yaml", "bad": "semver_bad.yaml"}

alls = {"schema": "all.yaml", "good": "all_good.yaml", "bad": "all_bad.yaml"}

include_validator = {
"schema": "include_validator.yaml",
Expand Down Expand Up @@ -117,6 +118,7 @@
map_key_constraint,
numeric_bool_coercion,
semver,
alls,
subset,
subset_empty,
]
Expand Down Expand Up @@ -205,6 +207,10 @@ def test_bad_semver():
assert count_exception_lines(semver["schema"], semver["bad"]) == 1


def test_bad_all():
assert count_exception_lines(alls["schema"], alls["bad"]) == 6


def test_bad_regexes():
assert count_exception_lines(regexes["schema"], regexes["bad"]) == 4

Expand Down
22 changes: 22 additions & 0 deletions yamale/validators/tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,25 @@ def test_semver():
assert not v.is_valid("+justmeta")
assert not v.is_valid("9.8.7+meta+meta")
assert not v.is_valid("9.8.7-whatever+meta+meta")


def test_all():
"""Test the All validator"""
# Test with numeric validators
v = val.All(val.Number(min=5), val.Number(max=10))
assert v.is_valid(7) # Should pass both validators
assert not v.is_valid(4) # Fails min=5
assert not v.is_valid(11) # Fails max=10

# Test with string validators
v = val.All(val.String(min=2), val.String(max=5))
assert v.is_valid("abc") # Passes both
assert not v.is_valid("a") # Fails min=2
assert not v.is_valid("abcdef") # Fails max=5

# Test with mixed validators
v = val.All(val.String(), val.Regex(r"^[a-z]+$"))
assert v.is_valid("abc") # String that matches regex
assert not v.is_valid(123) # Not a string
assert not v.is_valid("123") # String but fails regex
assert not v.is_valid("abc123") # String but fails regex
25 changes: 25 additions & 0 deletions yamale/validators/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,31 @@ def _is_valid(self, value):
return True


class All(Validator):
"""All validators must succeed validator"""

tag = "all"

def __init__(self, *args, **kwargs):
self.validators = [val for val in args if isinstance(val, Validator)]
super(All, self).__init__(*args, **kwargs)

def _is_valid(self, value):
return True

def is_valid(self, value):
Comment thread
qequ marked this conversation as resolved.
Outdated
# Override to validate against all validators
if not self._is_valid(value):
return False

# All validators must succeed
for validator in self.validators:
if not validator.is_valid(value):
return False

return True


class Subset(Validator):
"""Subset of several types validator"""

Expand Down