Repository: go-playground/validator
Pull request #575 (2020-02-06 18:38:21)

Add datetime validation
- [x] Tests exist or have been written that cover this particular change.
- [x]  doc.go was updated
- [x]  en translation and translation tests were updated

Change Details:

This PR adds a new validation that checks that a string value is a valid datetime based on the supplied datetime format. Supplied format must match the official Go time format layout as documented in https://golang.org/pkg/time/

@go-playground/admins

psampaz: I will rebase this one
deankarn: @psampaz cool was just about to resolve conflicts, will wait for your rebase if you can be done soon.

Patch:
diff --git a/baked_in.go b/baked_in.go
index deb00a2..4124c62 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -171,6 +171,7 @@ var (
 		"hostname_port":        isHostnamePort,
 		"lowercase":            isLowercase,
 		"uppercase":            isUppercase,
+		"datetime":             isDatetime,
 	}
 )
 
@@ -2020,8 +2021,9 @@ func isJSON(fl FieldLevel) bool {
 	if field.Kind() == reflect.String {
 		val := field.String()
 		return json.Valid([]byte(val))
-  }
-  panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	}
+
+	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
 
 // isHostnamePort validates a <dns>:<port> combination for fields typically used for socket address.
@@ -2070,3 +2072,20 @@ func isUppercase(fl FieldLevel) bool {
 
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
+
+// isDatetime is the validation function for validating if the current field's value is a valid datetime string.
+func isDatetime(fl FieldLevel) bool {
+	field := fl.Field()
+	param := fl.Param()
+
+	if field.Kind() == reflect.String {
+		_, err := time.Parse(param, 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 9bcba9a..291c629 100644
--- a/doc.go
+++ b/doc.go
@@ -1062,6 +1062,13 @@ can be used to valiate fields typically passed to sockets and connections.
 
 	Usage: hostname_port
 
+Datetime
+
+This validates that a string value is a valid datetime based on the supplied datetime format.
+Supplied format must match the official Go time format layout as documented in https://golang.org/pkg/time/
+
+	Usage: datetime=2006-01-02
+
 Alias Validators and Tags
 
 NOTE: When returning an error, the tag returned in "FieldError" will be
diff --git a/translations/en/en.go b/translations/en/en.go
index be61d85..1fdea95 100644
--- a/translations/en/en.go
+++ b/translations/en/en.go
@@ -1324,7 +1324,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 		{
 			tag:         "json",
 			translation: "{0} must be a valid json string",
-      override:    false,
+			override:    false,
     },
     {
 			tag:         "lowercase",
@@ -1336,6 +1336,21 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} must be an uppercase string",
 			override:    false,
 		},
+		{
+			tag:         "datetime",
+			translation: "{0} does not match the {1} format",
+			override:    false,
+			customTransFunc: func(ut ut.Translator, fe validator.FieldError) string {
+
+				t, err := ut.T(fe.Tag(), fe.Field(), fe.Param())
+				if err != nil {
+					log.Printf("warning: error translating FieldError: %#v", fe)
+					return fe.(error).Error()
+				}
+
+				return t
+			},
+		},
 	}
 
 	for _, t := range translations {


Test patch:
diff --git a/translations/en/en_test.go b/translations/en/en_test.go
index b95ca98..2b113c6 100644
--- a/translations/en/en_test.go
+++ b/translations/en/en_test.go
@@ -144,6 +144,7 @@ func TestTranslations(t *testing.T) {
 		JSONString        string            `validate:"json"`
 		LowercaseString   string            `validate:"lowercase"`
 		UppercaseString   string            `validate:"uppercase"`
+		Datetime          string            `validate:"datetime=2006-01-02"`
 	}
 
 	var test Test
@@ -195,6 +196,7 @@ func TestTranslations(t *testing.T) {
 
 	test.UniqueSlice = []string{"1234", "1234"}
 	test.UniqueMap = map[string]string{"key1": "1234", "key2": "1234"}
+	test.Datetime = "2008-Feb-01"
 
 	err = validate.Struct(test)
 	NotEqual(t, err, nil)
@@ -650,6 +652,10 @@ func TestTranslations(t *testing.T) {
 			ns:       "Test.UppercaseString",
 			expected: "UppercaseString must be an uppercase string",
 		},
+		{
+			ns:       "Test.Datetime",
+			expected: "Datetime does not match the 2006-01-02 format",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/validator_test.go b/validator_test.go
index 1323867..2e4215f 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -9155,4 +9155,42 @@ func TestUppercaseValidation(t *testing.T) {
 	PanicMatches(t, func() {
 		_ = validate.Var(2, "uppercase")
 	}, "Bad field type int")
-}
\ No newline at end of file
+
+}
+
+func TestDatetimeValidation(t *testing.T) {
+	tests := []struct {
+		value    string `validate:"datetime=2006-01-02"`
+		tag      string
+		expected bool
+	}{
+		{"2008-02-01", `datetime=2006-01-02`, true},
+		{"2008-Feb-01", `datetime=2006-01-02`, 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 datetime failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d datetime failed Error: %s", i, errs)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "datetime" {
+					t.Fatalf("Index: %d datetime failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+
+	PanicMatches(t, func() {
+		_ = validate.Var(2, "datetime")
+	}, "Bad field type int")
+}