diff --git a/README.md b/README.md index 84b4cb6e2..284092b53 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Baked-in Validations | btc_addr | Bitcoin Address | | btc_addr_bech32 | Bitcoin Bech32 Address (segwit) | | credit_card | Credit Card Number | +| cron | Cron | | datetime | Datetime | | e164 | e164 formatted phone number | | email | E-mail String diff --git a/baked_in.go b/baked_in.go index eafc2a02c..e8d17a778 100644 --- a/baked_in.go +++ b/baked_in.go @@ -218,6 +218,7 @@ var ( "semver": isSemverFormat, "dns_rfc1035_label": isDnsRFC1035LabelFormat, "credit_card": isCreditCard, + "cron": isCron, } ) @@ -2579,3 +2580,9 @@ func isCreditCard(fl FieldLevel) bool { } return (sum % 10) == 0 } + +// isCron is the validation function for validating if the current field's value is a valid cron expression +func isCron(fl FieldLevel) bool { + cronString := fl.Field().String() + return cronRegex.MatchString(cronString) +} diff --git a/doc.go b/doc.go index 7f0a853cc..ba4639ca2 100644 --- a/doc.go +++ b/doc.go @@ -1323,6 +1323,12 @@ This validates that a string value contains a valid credit card number using Luh Usage: credit_card +Cron + +This validates that a string value contains a valid cron expression. + + Usage: cron + Alias Validators and Tags NOTE: When returning an error, the tag returned in "FieldError" will be diff --git a/regexes.go b/regexes.go index fb2ea18f5..5436e3f26 100644 --- a/regexes.go +++ b/regexes.go @@ -64,6 +64,7 @@ const ( bicRegexString = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$` semverRegexString = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/ dnsRegexStringRFC1035Label = "^[a-z]([-a-z0-9]*[a-z0-9]){0,62}$" + cronRegexString = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})` ) var ( @@ -126,4 +127,5 @@ var ( bicRegex = regexp.MustCompile(bicRegexString) semverRegex = regexp.MustCompile(semverRegexString) dnsRegexRFC1035Label = regexp.MustCompile(dnsRegexStringRFC1035Label) + cronRegex = regexp.MustCompile(cronRegexString) ) diff --git a/translations/en/en.go b/translations/en/en.go index f007d342f..0668bb28c 100644 --- a/translations/en/en.go +++ b/translations/en/en.go @@ -1281,6 +1281,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} must be a valid color", override: false, }, + { + tag: "cron", + translation: "{0} must be a valid cron expression", + override: false, + }, { tag: "oneof", translation: "{0} must be one of [{1}]", diff --git a/translations/it/it.go b/translations/it/it.go index 0b46fc434..8f999a4fc 100644 --- a/translations/it/it.go +++ b/translations/it/it.go @@ -124,7 +124,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er { tag: "min", customRegisFunc: func(ut ut.Translator) (err error) { - + if err = ut.Add("min-string", "{0} deve essere lungo almeno {1}", false); err != nil { return } @@ -432,7 +432,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er { tag: "lte", customRegisFunc: func(ut ut.Translator) (err error) { - + if err = ut.Add("lte-string", "{0} deve essere lungo al massimo {1}", false); err != nil { return } @@ -1132,6 +1132,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} deve essere un colore valido", override: false, }, + { + tag: "cron", + translation: "{0} deve essere una stringa cron valida", + override: false, + }, { tag: "oneof", translation: "{0} deve essere uno di [{1}]", diff --git a/validator_test.go b/validator_test.go index 79cfb553b..b8ebe6412 100644 --- a/validator_test.go +++ b/validator_test.go @@ -12524,3 +12524,37 @@ func TestMultiOrOperatorGroup(t *testing.T) { } } } + +func TestCronExpressionValidation(t *testing.T) { + tests := []struct { + value string `validate:"cron"` + tag string + expected bool + }{ + {"0 0 12 * * ?", "cron", true}, + {"0 15 10 ? * *", "cron", true}, + {"0 15 10 * * ?", "cron", true}, + {"0 15 10 * * ? 2005", "cron", true}, + {"0 15 10 ? * 6L", "cron", true}, + {"0 15 10 ? * 6L 2002-2005", "cron", true}, + {"*/20 * * * *", "cron", true}, + {"0 15 10 ? * MON-FRI", "cron", true}, + {"0 15 10 ? * 6#3", "cron", true}, + {"wrong", "cron", false}, + } + + validate := New() + + for i, test := range tests { + errs := validate.Var(test.value, test.tag) + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf(`Index: %d cron "%s" failed Error: %s`, i, test.value, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf(`Index: %d cron "%s" should have errs`, i, test.value) + } + } + } +}