diff --git a/DOC.md b/DOC.md index c56b2b6..129dd97 100644 --- a/DOC.md +++ b/DOC.md @@ -29,6 +29,7 @@ Package v2 Checker is a Go library for validating user input through checker rul - [func IsDiscoverCreditCard\(number string\) \(string, error\)](<#IsDiscoverCreditCard>) - [func IsEmail\(value string\) \(string, error\)](<#IsEmail>) - [func IsFQDN\(value string\) \(string, error\)](<#IsFQDN>) +- [func IsGte\[T cmp.Ordered\]\(value, n T\) \(T, error\)](<#IsGte>) - [func IsHex\(value string\) \(string, error\)](<#IsHex>) - [func IsIP\(value string\) \(string, error\)](<#IsIP>) - [func IsIPv4\(value string\) \(string, error\)](<#IsIPv4>) @@ -36,6 +37,7 @@ Package v2 Checker is a Go library for validating user input through checker rul - [func IsISBN\(value string\) \(string, error\)](<#IsISBN>) - [func IsJcbCreditCard\(number string\) \(string, error\)](<#IsJcbCreditCard>) - [func IsLUHN\(value string\) \(string, error\)](<#IsLUHN>) +- [func IsLte\[T cmp.Ordered\]\(value, n T\) \(T, error\)](<#IsLte>) - [func IsMAC\(value string\) \(string, error\)](<#IsMAC>) - [func IsMasterCardCreditCard\(number string\) \(string, error\)](<#IsMasterCardCreditCard>) - [func IsRegexp\(expression, value string\) \(string, error\)](<#IsRegexp>) @@ -80,6 +82,24 @@ const ( ## Variables + + +```go +var ( + // ErrGte indicates that the value is not greater than or equal to the given value. + ErrGte = NewCheckError("NOT_GTE") +) +``` + + + +```go +var ( + // ErrLte indicates that the value is not less than or equal to the given value. + ErrLte = NewCheckError("NOT_LTE") +) +``` + ```go @@ -707,6 +727,15 @@ func main() {

+ +## func [IsGte]() + +```go +func IsGte[T cmp.Ordered](value, n T) (T, error) +``` + +IsGte checks if the value is greater than or equal to the given value. + ## func [IsHex]() @@ -944,6 +973,15 @@ func main() {

+ +## func [IsLte]() + +```go +func IsLte[T cmp.Ordered](value, n T) (T, error) +``` + +IsLte checks if the value is less than or equal to the given value. + ## func [IsMAC]() @@ -1173,7 +1211,7 @@ func RegisterLocale(locale string, messages map[string]string) RegisterLocale registers the localized error messages for the given locale. -## func [RegisterMaker]() +## func [RegisterMaker]() ```go func RegisterMaker(name string, maker MakeCheckFunc) diff --git a/README.md b/README.md index 395b858..1389044 100644 --- a/README.md +++ b/README.md @@ -104,11 +104,13 @@ type Person struct { - [`digits`](DOC.md#IsDigits): Ensures the string contains only digits. - [`email`](DOC.md#IsEmail): Ensures the string is a valid email address. - [`fqdn`](DOC.md#IsFQDN): Ensures the string is a valid fully qualified domain name. -- [`hex`](DOC.md#IsHex): Ensures the string contains only hex digits. +- [`gte`](DOC.md#IsGte): Ensures the value is greater than or equal to the specified number. +- [`hex`](DOC.md#IsHex): Ensures the string contains only hexadecimal digits. - [`ip`](DOC.md#IsIP): Ensures the string is a valid IP address. - [`ipv4`](DOC.md#IsIPv4): Ensures the string is a valid IPv4 address. - [`ipv6`](DOC.md#IsIPv6): Ensures the string is a valid IPv6 address. - [`isbn`](DOC.md#IsISBN): Ensures the string is a valid ISBN. +- [`lte`](DOC.md#ISLte): Ensures the value is less than or equal to the specified number. - [`luhn`](DOC.md#IsLUHN): Ensures the string is a valid LUHN number. - [`mac`](DOC.md#IsMAC): Ensures the string is a valid MAC address. - [`max-len`](DOC.md#func-maxlen): Ensures the length of the given value (string, slice, or map) is at most n. diff --git a/gte.go b/gte.go new file mode 100644 index 0000000..1ae5fa5 --- /dev/null +++ b/gte.go @@ -0,0 +1,67 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2 + +import ( + "cmp" + "reflect" + "strconv" +) + +const ( + // nameGte is the name of the greater than or equal to check. + nameGte = "gte" +) + +var ( + // ErrGte indicates that the value is not greater than or equal to the given value. + ErrGte = NewCheckError("NOT_GTE") +) + +// IsGte checks if the value is greater than or equal to the given value. +func IsGte[T cmp.Ordered](value, n T) (T, error) { + if cmp.Compare(value, n) < 0 { + return value, newGteError(n) + } + + return value, nil +} + +// makeGte creates a greater than or equal to check function from a string parameter. +// Panics if the parameter cannot be parsed as a number. +func makeGte(params string) CheckFunc[reflect.Value] { + n, err := strconv.ParseFloat(params, 64) + if err != nil { + panic("unable to parse params as float") + } + + return func(value reflect.Value) (reflect.Value, error) { + v := reflect.Indirect(value) + + switch { + case v.CanInt(): + _, err := IsGte(float64(v.Int()), n) + return v, err + + case v.CanFloat(): + _, err := IsGte(v.Float(), n) + return v, err + + default: + panic("value is not numeric") + } + } +} + +// newGteError creates a new greater than or equal to error with the given value. +func newGteError[T cmp.Ordered](n T) error { + return NewCheckErrorWithData( + ErrGte.Code, + map[string]interface{}{ + "n": n, + }, + ) +} diff --git a/gte_test.go b/gte_test.go new file mode 100644 index 0000000..48df21f --- /dev/null +++ b/gte_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2_test + +import ( + "errors" + "testing" + + v2 "github.com/cinar/checker/v2" +) + +func TestGteIntSuccess(t *testing.T) { + value := 4 + + result, err := v2.IsGte(value, 4) + if result != value { + t.Fatalf("result (%d) is not the original value (%d)", result, value) + } + + if err != nil { + t.Fatal(err) + } +} + +func TestGteIntError(t *testing.T) { + value := 4 + + result, err := v2.IsGte(value, 5) + if result != value { + t.Fatalf("result (%d) is not the original value (%d)", result, value) + } + + if err == nil { + t.Fatal("expected error") + } + + message := "Value cannot be less than 5." + + if err.Error() != message { + t.Fatalf("expected %s actual %s", message, err.Error()) + } +} + +func TestReflectGteIntError(t *testing.T) { + type Person struct { + Age int `checkers:"gte:18"` + } + + person := &Person{ + Age: 16, + } + + errs, ok := v2.CheckStruct(person) + if ok { + t.Fatalf("expected errors") + } + + if !errors.Is(errs["Age"], v2.ErrGte) { + t.Fatalf("expected ErrGte") + } +} + +func TestReflectGteIntInvalidGte(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Age int `checkers:"gte:abcd"` + } + + person := &Person{ + Age: 16, + } + + v2.CheckStruct(person) +} + +func TestReflectGteIntInvalidType(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Age string `checkers:"gte:18"` + } + + person := &Person{ + Age: "18", + } + + v2.CheckStruct(person) +} + +func TestReflectGteFloatError(t *testing.T) { + type Person struct { + Weight float64 `checkers:"gte:165.0"` + } + + person := &Person{ + Weight: 150, + } + + errs, ok := v2.CheckStruct(person) + if ok { + t.Fatalf("expected errors") + } + + if !errors.Is(errs["Weight"], v2.ErrGte) { + t.Fatalf("expected ErrGte") + } +} + +func TestReflectGteFloatInvalidGte(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Weight float64 `checkers:"gte:abcd"` + } + + person := &Person{ + Weight: 170, + } + + v2.CheckStruct(person) +} + +func TestReflectGteFloatInvalidType(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Weight string `checkers:"gte:165.0"` + } + + person := &Person{ + Weight: "170", + } + + v2.CheckStruct(person) +} diff --git a/locales/DOC.md b/locales/DOC.md index 88718f0..f34002c 100644 --- a/locales/DOC.md +++ b/locales/DOC.md @@ -40,11 +40,13 @@ var EnUSMessages = map[string]string{ "NOT_DIGITS": "Can only contain digits.", "NOT_EMAIL": "Not a valid email address.", "NOT_FQDN": "Not a fully qualified domain name (FQDN).", + "NOT_GTE": "Value cannot be less than {{ .n }}.", "NOT_HEX": "Can only contain hexadecimal characters.", "NOT_IP": "Not a valid IP address.", "NOT_IPV4": "Not a valid IPv4 address.", "NOT_IPV6": "Not a valid IPv6 address.", "NOT_ISBN": "Not a valid ISBN number.", + "NOT_LTE": "Value cannot be less than {{ .n }}.", "NOT_LUHN": "Not a valid LUHN number.", "NOT_MAC": "Not a valid MAC address.", "NOT_MAX_LEN": "Value cannot be greater than {{ .max }}.", diff --git a/locales/en_us.go b/locales/en_us.go index c35c55b..01cd1eb 100644 --- a/locales/en_us.go +++ b/locales/en_us.go @@ -14,11 +14,13 @@ var EnUSMessages = map[string]string{ "NOT_DIGITS": "Can only contain digits.", "NOT_EMAIL": "Not a valid email address.", "NOT_FQDN": "Not a fully qualified domain name (FQDN).", + "NOT_GTE": "Value cannot be less than {{ .n }}.", "NOT_HEX": "Can only contain hexadecimal characters.", "NOT_IP": "Not a valid IP address.", "NOT_IPV4": "Not a valid IPv4 address.", "NOT_IPV6": "Not a valid IPv6 address.", "NOT_ISBN": "Not a valid ISBN number.", + "NOT_LTE": "Value cannot be less than {{ .n }}.", "NOT_LUHN": "Not a valid LUHN number.", "NOT_MAC": "Not a valid MAC address.", "NOT_MAX_LEN": "Value cannot be greater than {{ .max }}.", diff --git a/lte.go b/lte.go new file mode 100644 index 0000000..884bcd8 --- /dev/null +++ b/lte.go @@ -0,0 +1,67 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2 + +import ( + "cmp" + "reflect" + "strconv" +) + +const ( + // nameLte is the name of the less than or equal to check. + nameLte = "lte" +) + +var ( + // ErrLte indicates that the value is not less than or equal to the given value. + ErrLte = NewCheckError("NOT_LTE") +) + +// IsLte checks if the value is less than or equal to the given value. +func IsLte[T cmp.Ordered](value, n T) (T, error) { + if cmp.Compare(value, n) > 0 { + return value, newLteError(n) + } + + return value, nil +} + +// makeLte creates a less than or equal to check function from a string parameter. +// Panics if the parameter cannot be parsed as a number. +func makeLte(params string) CheckFunc[reflect.Value] { + n, err := strconv.ParseFloat(params, 64) + if err != nil { + panic("unable to parse params as float") + } + + return func(value reflect.Value) (reflect.Value, error) { + v := reflect.Indirect(value) + + switch { + case v.CanInt(): + _, err := IsLte(float64(v.Int()), n) + return v, err + + case v.CanFloat(): + _, err := IsLte(v.Float(), n) + return v, err + + default: + panic("value is not numeric") + } + } +} + +// newLteError creates a new less than or equal to error with the given value. +func newLteError[T cmp.Ordered](n T) error { + return NewCheckErrorWithData( + ErrLte.Code, + map[string]interface{}{ + "n": n, + }, + ) +} diff --git a/lte_test.go b/lte_test.go new file mode 100644 index 0000000..d039f53 --- /dev/null +++ b/lte_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2_test + +import ( + "errors" + "testing" + + v2 "github.com/cinar/checker/v2" +) + +func TestLteIntSuccess(t *testing.T) { + value := 4 + + result, err := v2.IsLte(value, 4) + if result != value { + t.Fatalf("result (%d) is not the original value (%d)", result, value) + } + + if err != nil { + t.Fatal(err) + } +} + +func TestLteIntError(t *testing.T) { + value := 6 + + result, err := v2.IsLte(value, 5) + if result != value { + t.Fatalf("result (%d) is not the original value (%d)", result, value) + } + + if err == nil { + t.Fatal("expected error") + } + + message := "Value cannot be less than 5." + + if err.Error() != message { + t.Fatalf("expected %s actual %s", message, err.Error()) + } +} + +func TestReflectLteIntError(t *testing.T) { + type Person struct { + Age int `checkers:"lte:18"` + } + + person := &Person{ + Age: 21, + } + + errs, ok := v2.CheckStruct(person) + if ok { + t.Fatalf("expected errors") + } + + if !errors.Is(errs["Age"], v2.ErrLte) { + t.Fatalf("expected ErrLte") + } +} + +func TestReflectLteIntInvalidLte(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Age int `checkers:"lte:abcd"` + } + + person := &Person{ + Age: 16, + } + + v2.CheckStruct(person) +} + +func TestReflectLteIntInvalidType(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Age string `checkers:"lte:18"` + } + + person := &Person{ + Age: "18", + } + + v2.CheckStruct(person) +} + +func TestReflectLteFloatError(t *testing.T) { + type Person struct { + Weight float64 `checkers:"lte:165.0"` + } + + person := &Person{ + Weight: 170, + } + + errs, ok := v2.CheckStruct(person) + if ok { + t.Fatalf("expected errors") + } + + if !errors.Is(errs["Weight"], v2.ErrLte) { + t.Fatalf("expected ErrLte") + } +} + +func TestReflectLteFloatInvalidLte(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Weight float64 `checkers:"lte:abcd"` + } + + person := &Person{ + Weight: 170, + } + + v2.CheckStruct(person) +} + +func TestReflectLteFloatInvalidType(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Person struct { + Weight string `checkers:"lte:165.0"` + } + + person := &Person{ + Weight: "170", + } + + v2.CheckStruct(person) +} diff --git a/maker.go b/maker.go index 935d04b..ed1b529 100644 --- a/maker.go +++ b/maker.go @@ -23,6 +23,7 @@ var makers = map[string]MakeCheckFunc{ nameDigits: makeDigits, nameEmail: makeEmail, nameFQDN: makeFQDN, + nameGte: makeGte, nameHex: makeHex, nameHTMLEscape: makeHTMLEscape, nameHTMLUnescape: makeHTMLUnescape, @@ -31,6 +32,7 @@ var makers = map[string]MakeCheckFunc{ nameIPv6: makeIPv6, nameISBN: makeISBN, nameLower: makeLower, + nameLte: makeLte, nameLUHN: makeLUHN, nameMAC: makeMAC, nameMaxLen: makeMaxLen,