diff --git a/go.mod b/go.mod index d5f4bc3..a5e066a 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.21 require ( github.com/gofrs/uuid v4.4.0+incompatible + github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 golang.org/x/net v0.21.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9ba7fb4..518fbdb 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/json.go b/json.go new file mode 100644 index 0000000..444a6e5 --- /dev/null +++ b/json.go @@ -0,0 +1,97 @@ +package validator + +import ( + "context" + "encoding/json" + "fmt" +) + +type JSON struct { + message string + + whenFunc WhenFunc + skipEmpty bool + skipError bool +} + +func NewJSON() *JSON { + return &JSON{ + message: "Must be a valid JSON", + } +} + +func (j *JSON) ValidateValue(_ context.Context, value any) error { + var bytes []byte + + switch v := value.(type) { + case string: + bytes = []byte(v) + case *string: + bytes = []byte(*v) + case []byte: + bytes = v + case *[]byte: + bytes = *v + case json.RawMessage: + bytes = v + case *json.RawMessage: + bytes = *v + case fmt.Stringer: + if i, ok := v.(fmt.Stringer); ok { + bytes = []byte(i.String()) + } + default: + return NewResult().WithError(NewValidationError(j.message)) + } + + if isValid := json.Valid(bytes); !isValid { + return NewResult().WithError(NewValidationError(j.message)) + } + + return nil +} + +func (j *JSON) When(v WhenFunc) *JSON { + rc := *j + rc.whenFunc = v + + return &rc +} + +func (j *JSON) when() WhenFunc { + return j.whenFunc +} + +func (j *JSON) setWhen(v WhenFunc) { + j.whenFunc = v +} + +func (j *JSON) SkipOnEmpty() *JSON { + rc := *j + rc.skipEmpty = true + + return &rc +} + +func (j *JSON) skipOnEmpty() bool { + return j.skipEmpty +} + +func (j *JSON) setSkipOnEmpty(v bool) { + j.skipEmpty = v +} + +func (j *JSON) SkipOnError() *JSON { + rs := *j + rs.skipError = true + + return &rs +} + +func (j *JSON) shouldSkipOnError() bool { + return j.skipError +} + +func (j *JSON) setSkipOnError(v bool) { + j.skipError = v +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..62296e8 --- /dev/null +++ b/json_test.go @@ -0,0 +1,241 @@ +package validator + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +type stringer string + +func (s stringer) String() string { + return string(s) +} + +func TestJSON_ValidateValue_Successfully(t *testing.T) { + // region data provider + ctx := context.Background() + + str := `"hello world"` + obj := `{"hello":"world"}` + arr := `["hello","world"]` + + bytesWithString := []byte(str) + bytesWithObject := []byte(obj) + bytesWithArray := []byte(arr) + + jsonRawWithString := json.RawMessage(str) + jsonRawWithObject := json.RawMessage(obj) + jsonRawWithArray := json.RawMessage(arr) + + type args struct { + value any + } + tests := []struct { + name string + args args + }{ + { + name: "stringer with string", + args: args{value: stringer(str)}, + }, + { + name: "string", + args: args{value: str}, + }, + { + name: "string object", + args: args{value: obj}, + }, + { + name: "string array", + args: args{value: arr}, + }, + { + name: "string pointer", + args: args{value: &str}, + }, + { + name: "string pointer object", + args: args{value: &obj}, + }, + { + name: "string pointer array", + args: args{value: &arr}, + }, + { + name: "bytes with string", + args: args{value: bytesWithString}, + }, + { + name: "bytes with object", + args: args{value: bytesWithObject}, + }, + { + name: "bytes with array", + args: args{value: bytesWithArray}, + }, + { + name: "bytes pointer with string", + args: args{value: &bytesWithString}, + }, + { + name: "bytes with object", + args: args{value: &bytesWithObject}, + }, + { + name: "bytes with array", + args: args{value: &bytesWithArray}, + }, + { + name: "json.RawMessage with string", + args: args{value: jsonRawWithString}, + }, + { + name: "json.RawMessage with object", + args: args{value: jsonRawWithObject}, + }, + { + name: "json.RawMessage with array", + args: args{value: jsonRawWithArray}, + }, + { + name: "json.RawMessage pointer with string", + args: args{value: &jsonRawWithString}, + }, + { + name: "json.RawMessage pointer with object", + args: args{value: &jsonRawWithObject}, + }, + { + name: "json.RawMessage pointer with array", + args: args{value: &jsonRawWithArray}, + }, + } + // endregion + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + j := NewJSON() + err := j.ValidateValue(ctx, tt.args.value) + require.NoError(t, err) + }) + } +} + +func TestJSON_ValidateValue_Failure(t *testing.T) { + // region data provider + ctx := context.Background() + + str := `hello world` + obj := `{hello":"world"` + arr := `["hello","world` + + bytesWithString := []byte(str) + bytesWithObject := []byte(obj) + bytesWithArray := []byte(arr) + + jsonRawWithString := json.RawMessage(str) + jsonRawWithObject := json.RawMessage(obj) + jsonRawWithArray := json.RawMessage(arr) + + type args struct { + value any + } + tests := []struct { + name string + args args + }{ + { + name: "int", + args: args{value: 6}, + }, + { + name: "stringer with string", + args: args{value: stringer(str)}, + }, + { + name: "string", + args: args{value: str}, + }, + { + name: "string object", + args: args{value: obj}, + }, + { + name: "string array", + args: args{value: arr}, + }, + { + name: "string pointer", + args: args{value: &str}, + }, + { + name: "string pointer object", + args: args{value: &obj}, + }, + { + name: "string pointer array", + args: args{value: &arr}, + }, + { + name: "bytes with string", + args: args{value: bytesWithString}, + }, + { + name: "bytes with object", + args: args{value: bytesWithObject}, + }, + { + name: "bytes with array", + args: args{value: bytesWithArray}, + }, + { + name: "bytes pointer with string", + args: args{value: &bytesWithString}, + }, + { + name: "bytes with object", + args: args{value: &bytesWithObject}, + }, + { + name: "bytes with array", + args: args{value: &bytesWithArray}, + }, + { + name: "json.RawMessage with string", + args: args{value: jsonRawWithString}, + }, + { + name: "json.RawMessage with object", + args: args{value: jsonRawWithObject}, + }, + { + name: "json.RawMessage with array", + args: args{value: jsonRawWithArray}, + }, + { + name: "json.RawMessage pointer with string", + args: args{value: &jsonRawWithString}, + }, + { + name: "json.RawMessage pointer with object", + args: args{value: &jsonRawWithObject}, + }, + { + name: "json.RawMessage pointer with array", + args: args{value: &jsonRawWithArray}, + }, + } + // endregion + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + j := NewJSON() + err := j.ValidateValue(ctx, tt.args.value) + require.Error(t, err) + }) + } +}