diff --git a/baked_in.go b/baked_in.go index ac9145d..4151926 100644 --- a/baked_in.go +++ b/baked_in.go @@ -180,6 +180,7 @@ var ( "lowercase": isLowercase, "uppercase": isUppercase, "datetime": isDatetime, + "timezone": isTimeZone, } ) @@ -2211,3 +2212,29 @@ func isDatetime(fl FieldLevel) bool { panic(fmt.Sprintf("Bad field type %T", field.Interface())) } + +// isTimeZone is the validation function for validating if the current field's value is a valid time zone string. +func isTimeZone(fl FieldLevel) bool { + field := fl.Field() + + if field.Kind() == reflect.String { + // empty value is converted to UTC by time.LoadLocation but disallow it as it is not a valid time zone name + if field.String() == "" { + return false + } + + // Local value is converted to the current system time zone by time.LoadLocation but disallow it as it is not a valid time zone name + if strings.ToLower(field.String()) == "local" { + return false + } + + _, err := time.LoadLocation(field.String()) + if err != nil { + return false + } + + return true + } + + panic(fmt.Sprintf("Bad field type %T", field.Interface())) +} diff --git a/doc.go b/doc.go index d4d0186..aeeedd8 100644 --- a/doc.go +++ b/doc.go @@ -1194,6 +1194,14 @@ Supplied format must match the official Go time format layout as documented in h Usage: datetime=2006-01-02 +TimeZone + +This validates that a string value is a valid time zone based on the time zone database present on the system. +Although empty value and Local value are allowed by time.LoadLocation golang function, they are not allowed by this validator. +More information on https://golang.org/pkg/time/#LoadLocation + + Usage: timezone + Alias Validators and Tags NOTE: When returning an error, the tag returned in "FieldError" will be diff --git a/validator_test.go b/validator_test.go index 0e1e9f7..23ee021 100644 --- a/validator_test.go +++ b/validator_test.go @@ -10835,3 +10835,44 @@ func TestDatetimeValidation(t *testing.T) { _ = validate.Var(2, "datetime") }, "Bad field type int") } + +func TestTimeZoneValidation(t *testing.T) { + tests := []struct { + value string `validate:"timezone"` + tag string + expected bool + }{ + // systems may have different time zone database, some systems time zone are case insensitive + {"America/New_York", `timezone`, true}, + {"UTC", `timezone`, true}, + {"", `timezone`, false}, + {"Local", `timezone`, false}, + {"Unknown", `timezone`, 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 time zone failed Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d time zone failed Error: %s", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "timezone" { + t.Fatalf("Index: %d time zone failed Error: %s", i, errs) + } + } + } + } + + PanicMatches(t, func() { + _ = validate.Var(2, "timezone") + }, "Bad field type int") +}