Skip to content

Commit

Permalink
Merge pull request #764 from eaguad1337/feat/custom-validator-messages
Browse files Browse the repository at this point in the history
feat: custom validation messages
  • Loading branch information
josephmancuso committed Dec 6, 2023
2 parents 0c473ba + cbdfa4d commit 2b36590
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 10 deletions.
4 changes: 2 additions & 2 deletions src/masonite/request/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class ValidatesRequest:
"""Request mixin to add inputs validation to requests."""

def validate(self, *rules: "str|dict|RuleEnclosure") -> "MessageBag":
def validate(self, *rules: "str|dict|RuleEnclosure", messages={}) -> "MessageBag":
"""Validate request inputs against the given rules."""
validator = Validator()
return validator.validate(self.all(), *rules)
return validator.validate(self.all(), *rules, messages=messages)
42 changes: 36 additions & 6 deletions src/masonite/validation/Validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def handle(self, dictionary):

for key in self.validations:
if self.negated:

if self.passes(self.find(key, dictionary), key, dictionary):
boolean = False
if hasattr(self, "negated_message"):
Expand Down Expand Up @@ -167,7 +166,7 @@ def negated_message(self, attribute):

class boolean(BaseValidation):
def passes(self, attribute, key, dictionary):
return (attribute in [True, False, 0, 1, '0', '1'])
return attribute in [True, False, 0, 1, "0", "1"]

def message(self, attribute):
return "The {} must be a boolean.".format(attribute)
Expand Down Expand Up @@ -580,7 +579,6 @@ def __init__(self, validations, min=1, max=255, messages={}, raises={}):
self.max = max

def passes(self, attribute, key, dictionary):

attribute = str(attribute)

if attribute.isalpha():
Expand Down Expand Up @@ -1330,16 +1328,19 @@ def negated_message(self, attribute):


class Validator:
messages = {}

def __init__(self):
pass

def validate(self, dictionary, *rules):
def validate(self, dictionary, *rules, messages={}):
self.messages = messages
rule_errors = {}
try:
for rule in rules:
if isinstance(rule, str):
rule = self.parse_string(rule)
# continue

elif isinstance(rule, dict):
rule = self.parse_dict(rule, dictionary, rule_errors)
continue
Expand All @@ -1350,6 +1351,7 @@ def validate(self, dictionary, *rules):

rule.handle(dictionary)
for error, message in rule.errors.items():
message = self.custom_message(error, rule) or message
if error not in rule_errors:
rule_errors.update({error: message})
else:
Expand All @@ -1365,6 +1367,31 @@ def validate(self, dictionary, *rules):

return MessageBag(rule_errors)

def custom_message(self, attribute, rule):
path = []
# Check if rule is negated
if rule.__class__.__name__ == "isnt":
for validation in rule.validations:
validation_name = (
validation.__class__.__name__
if type(validation) is not str
else validation
)

path.append(
f"isnt_{validation_name}"
if validation.negated
else validation_name
)
else:
path.append(rule.__class__.__name__)

dot_name = ".".join([attribute] + path)

if dot_name in self.messages:
return [self.messages[dot_name]]
return None

def parse_string(self, rule):
rule, parameters = rule.split(":")[0], rule.split(":")[1].split(",")
return ValidationFactory().registry[rule](parameters)
Expand All @@ -1377,6 +1404,8 @@ def parse_dict(self, rule, dictionary, rule_errors):

rule.handle(dictionary)
for error, message in rule.errors.items():
message = self.custom_message(value, rule) or message

if error not in rule_errors:
rule_errors.update({error: message})
else:
Expand All @@ -1389,6 +1418,8 @@ def run_enclosure(self, enclosure, dictionary):
for rule in enclosure.rules():
rule.handle(dictionary)
for error, message in rule.errors.items():
message = self.custom_message(error, rule) or message

if error not in rule_errors:
rule_errors.update({error: message})
else:
Expand Down Expand Up @@ -1419,7 +1450,6 @@ def __getattr__(self, name):


class ValidationFactory:

registry = {}

def __init__(self):
Expand Down
52 changes: 52 additions & 0 deletions tests/features/validation/test_request_validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from tests import TestCase
from masonite.validation import email, isnt, is_in, numeric
from src.masonite.validation import Validator


class TestValidation(TestCase):
def test_can_validate_request(self):
request = self.make_request(query_string="[email protected]")
Expand All @@ -22,6 +24,55 @@ def test_can_validate_request_with_no_inputs(self):

self.assertEqual(validation.all(), {"email": ["The email field is required."]})

def test_custom_messages(self):
request = self.make_request(post_data={"not_email": "[email protected]", "age": 20})
# validator = Validator()
validate = request.validate(
isnt(is_in("age", [20, 21]), numeric("age")),
isnt(email("not_email")),
{
"username": "required",
"valid_email": "required|email",
"secondary_email": "required|email",
},
messages={
"username.required": "Custom Message for required Username.",
"not_email.isnt_email": "Custom Message for Email not being an email.",
"valid_email.required": "Custom Message for required Email.",
"valid_email.email": "Custom Message for Email being a valid email.",
"age.isnt_is_in.isnt_numeric": "Custom: Age must not be in 20, 21 and must not be numeric.",
},
)

self.assertIn(
"Custom: Age must not be in 20, 21 and must not be numeric.",
validate.get("age"),
)
self.assertIn(
"Custom Message for required Email.",
validate.get("valid_email"),
)
self.assertIn(
"Custom Message for Email being a valid email.",
validate.get("valid_email"),
)
self.assertIn(
"Custom Message for Email not being an email.",
validate.get("not_email"),
)
self.assertIn(
"Custom Message for required Username.",
validate.get("username"),
)
self.assertIn(
"The secondary_email field is required.",
validate.get("secondary_email"),
)
self.assertIn(
"The secondary_email must be a valid email address.",
validate.get("secondary_email"),
)

def test_can_forward_validation_calls(self):
request = self.make_request(query_string="")
validate = Validator()
Expand All @@ -34,3 +85,4 @@ def test_can_forward_validation_calls(self):
self.assertIn("The user field is required.", errors.get("user"))
self.assertIn("The email field is required.", errors.get("email"))
self.assertIn("The terms must be accepted.", errors.get("terms"))

3 changes: 1 addition & 2 deletions tests/features/validation/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1306,8 +1306,7 @@ def test_distinct_with_simple_list(self):
self.assertEqual(
validate.get("emails"), ["The emails field has duplicate values."]
)



class TestDotNotationValidation(unittest.TestCase):
def setUp(self):
pass
Expand Down

0 comments on commit 2b36590

Please sign in to comment.