diff --git a/duration/duration.go b/duration/duration.go new file mode 100644 index 0000000..80b400e --- /dev/null +++ b/duration/duration.go @@ -0,0 +1,26 @@ +package duration + +import ( + "fmt" + "reflect" + "time" +) + +// ValidateString validates that input is a valid duration +func ValidateString(input string) (bool, error) { + _, err := time.ParseDuration(input) + if err != nil { + return false, err + } + + return true, nil +} + +// ValidateStructField validates a struct field holds a valid duration +func ValidateStructField(value reflect.Value, tag string) (bool, error) { + if value.Kind() != reflect.String { + return false, fmt.Errorf("only strings can be Duration validated") + } + + return ValidateString(value.String()) +} diff --git a/duration/duration_test.go b/duration/duration_test.go new file mode 100644 index 0000000..3bffedd --- /dev/null +++ b/duration/duration_test.go @@ -0,0 +1,54 @@ +package duration + +import ( + "reflect" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestFileContent(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validator/Duration") +} + +var _ = Describe("ValidateString", func() { + It("Should match durations correctly", func() { + ok, err := ValidateString("1s") + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + ok, err = ValidateString("1h") + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + ok, err = ValidateString("1w") + Expect(err).To(MatchError("time: unknown unit w in duration 1w")) + Expect(ok).To(BeFalse()) + }) +}) + +var _ = Describe("ValidateStructField", func() { + type t struct { + Interval string `validate:"duration"` + } + + It("Should validate the struct correctly", func() { + st := t{"1h"} + + val := reflect.ValueOf(st) + valueField := val.FieldByName("Interval") + typeField, _ := val.Type().FieldByName("Interval") + + ok, err := ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + st.Interval = "foo" + valueField = reflect.ValueOf(st).FieldByName("Interval") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).To(MatchError("time: invalid duration foo")) + Expect(ok).To(BeFalse()) + }) +}) diff --git a/validator.go b/validator.go index b8f7989..b18a69f 100644 --- a/validator.go +++ b/validator.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" + "github.com/choria-io/go-validator/duration" "github.com/choria-io/go-validator/enum" "github.com/choria-io/go-validator/ipaddress" "github.com/choria-io/go-validator/ipv4" @@ -116,6 +117,11 @@ func validateStructField(valueField reflect.Value, typeField reflect.StructField if ok, err := enum.ValidateStructField(valueField, validation); !ok { return fmt.Errorf("%s enum validation failed: %s", typeField.Name, err) } + + } else if strings.HasPrefix(validation, "duration") { + if ok, err := duration.ValidateStructField(valueField, validation); !ok { + return fmt.Errorf("%s duration validation failed: %s", typeField.Name, err) + } } return nil diff --git a/validator_test.go b/validator_test.go index 8ecc765..ba0e488 100644 --- a/validator_test.go +++ b/validator_test.go @@ -19,13 +19,14 @@ type nest struct { } type vdata struct { - SS string `validate:"shellsafe"` - ML string `validate:"maxlength=3"` - Enum []string `validate:"enum=one,two"` - IPv4 string `validate:"ipv4"` - IPv6 string `validate:"ipv6"` - IP string `validate:"ipaddress"` - RE string `validate:"regex=world$"` + SS string `validate:"shellsafe"` + ML string `validate:"maxlength=3"` + Enum []string `validate:"enum=one,two"` + IPv4 string `validate:"ipv4"` + IPv6 string `validate:"ipv6"` + IP string `validate:"ipaddress"` + RE string `validate:"regex=world$"` + Duration string `validate:"duration"` nest } @@ -34,10 +35,11 @@ var s vdata var _ = Describe("ValidateStructField", func() { BeforeEach(func() { s = vdata{ - IPv4: "1.2.3.4", - IPv6: "2a00:1450:4003:807::200e", - IP: "1.2.3.4", - RE: "hello world", + IPv4: "1.2.3.4", + IPv6: "2a00:1450:4003:807::200e", + IP: "1.2.3.4", + RE: "hello world", + Duration: "1h", } }) @@ -58,10 +60,11 @@ var _ = Describe("ValidateStructField", func() { var _ = Describe("ValidateStruct", func() { BeforeEach(func() { s = vdata{ - IPv4: "1.2.3.4", - IPv6: "2a00:1450:4003:807::200e", - IP: "1.2.3.4", - RE: "hello world", + IPv4: "1.2.3.4", + IPv6: "2a00:1450:4003:807::200e", + IP: "1.2.3.4", + RE: "hello world", + Duration: "1h", } }) @@ -130,4 +133,12 @@ var _ = Describe("ValidateStruct", func() { Expect(err).To(MatchError("RE regular expression validation failed: input does not match 'world$'")) Expect(ok).To(BeFalse()) }) + + It("Should support regex", func() { + s.Duration = "1w" + ok, err := validator.ValidateStruct(s) + + Expect(err).To(MatchError("Duration duration validation failed: time: unknown unit w in duration 1w")) + Expect(ok).To(BeFalse()) + }) })