Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added array support #13

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.20.x", "1.21.x"]
go-version: ["1.20.x", "1.21.x", "1.22.x"]

steps:
- uses: actions/checkout@v4
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [Ints](#ints)
- [Booleans](#booleans)
- [Objects](#objects)
- [Arrays](#arrays)
- [Schema methods](#schema-methods)
- [`.parse`](#parse)
- [`.refine`](#refine)
Expand Down Expand Up @@ -152,6 +153,30 @@ mySchema := schema.Object(map[string]schema.ISchema{
})
```

## Arrays

The array schema accepts any type that implements the `ISchema` interface, this allows you to parse in other schema.

```go
mySchema := schema.Array(schema.String().Min(2)).Max(4)
```

The above schema defines an array of strings, each of which has a minimum length of 2, with the overall array max length of 4.

You may utilize array and object to construct a more advanced schema

```go
mySchema := schema.Object(map[string]schema.ISchema{
"Username": schema.String().Min(5),
"Firstname": schema.String().Min(2).Max(128),
"Age": schema.Int().Gte(18),
"Addresses": schema.Array(schema.Object(map[string]schema.ISchema{
"Postcode": schema.String().Min(4).Max(10),
"Country": schema.String().Length(2),
})),
})
```

## Schema methods

All schemas contain certain methods.
Expand Down
92 changes: 92 additions & 0 deletions array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package schema

import (
"fmt"
"reflect"
"strconv"
)

type ArraySchema struct {
Schema[[]interface{}]
schema ISchema
}

var _ ISchema = (*ArraySchema)(nil)

func Array(s ISchema) *ArraySchema {
return &ArraySchema{schema: s}
}

func (s *ArraySchema) Max(maxLength int) *ArraySchema {
validator := Validator[[]interface{}]{
MessageFunc: func(value []interface{}) string {
return fmt.Sprintf("Array must contain at most %d element(s)", maxLength)
},
ValidateFunc: func(value []interface{}) bool {
return len(value) <= maxLength
},
}

s.validators = append(s.validators, validator)

return s
}

func (s *ArraySchema) Min(minLength int) *ArraySchema {
validator := Validator[[]interface{}]{
MessageFunc: func(value []interface{}) string {
return fmt.Sprintf("Array must contain at least %d element(s)", minLength)
},
ValidateFunc: func(value []interface{}) bool {
return len(value) >= minLength
},
}

s.validators = append(s.validators, validator)

return s
}

func (s *ArraySchema) Parse(value any) *ValidationResult {
t := reflect.TypeOf(value)

if t == nil || (t.Kind() != reflect.Array && t.Kind() != reflect.Slice) {
return &ValidationResult{Errors: []ValidationError{{Path: "", Message: fmt.Sprintf("Expected array, got %T", value)}}}
}

v := reflect.ValueOf(value)

val := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
val[i] = v.Index(i).Interface()
}

// Parse array validations
result := &ValidationResult{Errors: []ValidationError{}}

for _, validator := range s.validators {
if !validator.ValidateFunc(val) {
err := ValidationError{
Path: "",
Message: validator.MessageFunc(val),
}

result.Errors = append(result.Errors, err)
}
}

// Parse schema validations within array for each item
for i := 0; i < len(val); i++ {
res := s.schema.Parse(val[i])

if !res.IsValid() {
for index, err := range res.Errors {
res.Errors[index].Path = formatPath(strconv.Itoa(i), err.Path)
}

result.Errors = append(result.Errors, res.Errors...)
}
}

return result
}
102 changes: 102 additions & 0 deletions array_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package schema_test

import (
"testing"

schema "github.com/Jamess-Lucass/validator-go"
"github.com/stretchr/testify/assert"
)

func TestArray_String_Type(t *testing.T) {
s := schema.Array(schema.String())

assert.True(t, s.Parse([]string{"one", "two"}).IsValid())
assert.True(t, s.Parse([]string{""}).IsValid())
assert.True(t, s.Parse([]string{}).IsValid())

assert.False(t, s.Parse(123).IsValid())
assert.False(t, s.Parse(nil).IsValid())
assert.False(t, s.Parse(map[string]int{
"one": 1,
"two": 2,
}).IsValid())
assert.False(t, s.Parse([]int{1, 2, 3}).IsValid())
assert.False(t, s.Parse(0).IsValid())
assert.False(t, s.Parse("57c6b6aa-211a-4b49-a012-3fd9b4a4ea2d").IsValid())
assert.False(t, s.Parse("db9fb12c-daea-11ee-a506-0242ac120002").IsValid())
assert.False(t, s.Parse("018e0e8f-b1d9-7503-ac4a-49b18a95be69").IsValid())
assert.False(t, s.Parse("00000000-0000-0000-0000-000000000000").IsValid())
}

func TestArray_Path(t *testing.T) {
s := schema.Array(schema.String().Min(4)).Parse([]string{"one"})

assert.Len(t, s.Errors, 1)
assert.Equal(t, "0", s.Errors[0].Path)
}

func TestArray_String(t *testing.T) {
s := schema.Array(schema.String().Min(4).StartsWith("a"))

assert.True(t, s.Parse([]string{"aour", "aive"}).IsValid())
assert.True(t, s.Parse([]string{"aour"}).IsValid())
assert.True(t, s.Parse([]string{}).IsValid())

assert.False(t, s.Parse([]string{"one"}).IsValid())
assert.False(t, s.Parse([]string{"one", "four"}).IsValid())

assert.Len(t, s.Parse([]string{"ane", "aour"}).Errors, 1)
assert.Len(t, s.Parse([]string{"ane", "four"}).Errors, 2)
assert.Len(t, s.Parse([]string{"one", "four"}).Errors, 3)
}

func TestArray_Min(t *testing.T) {
s := schema.Array(schema.String()).Min(1)

assert.True(t, s.Parse([]string{"aour", "aive"}).IsValid())
assert.True(t, s.Parse([]string{"aour"}).IsValid())

assert.False(t, s.Parse([]string{}).IsValid())
}

func TestArray_Max(t *testing.T) {
s := schema.Array(schema.String()).Max(1)

assert.True(t, s.Parse([]string{"aour"}).IsValid())
assert.True(t, s.Parse([]string{}).IsValid())

assert.False(t, s.Parse([]string{"aour", "aive"}).IsValid())
}

func TestArray_Object(t *testing.T) {
type User struct {
Firstname string
}

s := schema.Array(schema.Object(map[string]schema.ISchema{
"Firstname": schema.String().Min(4),
}))

val := []User{
{Firstname: "1234"},
{Firstname: "1234"},
}

assert.True(t, s.Parse(val).IsValid())
}

func TestArray_Object_Path(t *testing.T) {
type User struct {
Firstname string
}

s := schema.Array(schema.Object(map[string]schema.ISchema{
"Firstname": schema.String().Min(4),
})).Parse([]User{
{Firstname: "123"},
{Firstname: "123"},
})

assert.Len(t, s.Errors, 2)
assert.Equal(t, "0.Firstname", s.Errors[0].Path)
}
11 changes: 6 additions & 5 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ func (s *ObjectSchema) Refine(predicate func(map[string]interface{}) bool) *Obje

func (s *ObjectSchema) Parse(value any) *ValidationResult {
t := reflect.TypeOf(value)
val := reflect.ValueOf(value)

if t.Kind() != reflect.Struct {
if t == nil || t.Kind() != reflect.Struct {
return &ValidationResult{Errors: []ValidationError{{Path: "", Message: fmt.Sprintf("Expected struct, got %T", value)}}}
}

val := reflect.ValueOf(value)

res := &ValidationResult{}

for key, schema := range s.value {
Expand All @@ -56,7 +57,7 @@ func (s *ObjectSchema) Parse(value any) *ValidationResult {
if !result.IsValid() {
for _, err := range result.Errors {
newError := ValidationError{
Path: key,
Path: formatPath(key, err.Path),
Message: err.Message,
}

Expand All @@ -65,7 +66,7 @@ func (s *ObjectSchema) Parse(value any) *ValidationResult {
}
}

valueMap := StructToMap(value)
valueMap := structToMap(value)

for _, validator := range s.validators {
if !validator.ValidateFunc(valueMap) {
Expand All @@ -81,7 +82,7 @@ func (s *ObjectSchema) Parse(value any) *ValidationResult {
return res
}

func StructToMap(item interface{}) map[string]interface{} {
func structToMap(item interface{}) map[string]interface{} {
result := map[string]interface{}{}

val := reflect.ValueOf(item)
Expand Down
75 changes: 75 additions & 0 deletions object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package schema_test

import (
"testing"

schema "github.com/Jamess-Lucass/validator-go"
"github.com/stretchr/testify/assert"
)

func TestObject_Type(t *testing.T) {
type User struct {
Firstname string
Lastname string
}

s := schema.Object(map[string]schema.ISchema{
"Firstname": schema.String().Min(2),
"Lastname": schema.String().StartsWith("d"),
})

assert.True(t, s.Parse(User{Firstname: "john", Lastname: "doe"}).IsValid())

assert.False(t, s.Parse(User{Firstname: "john", Lastname: ""}).IsValid())
assert.False(t, s.Parse(User{Firstname: "", Lastname: "doe"}).IsValid())
assert.False(t, s.Parse(User{Firstname: "", Lastname: ""}).IsValid())
assert.False(t, s.Parse(123).IsValid())
assert.False(t, s.Parse(nil).IsValid())
assert.False(t, s.Parse(map[string]int{
"one": 1,
"two": 2,
}).IsValid())
assert.False(t, s.Parse([]int{1, 2, 3}).IsValid())
assert.False(t, s.Parse(0).IsValid())
assert.False(t, s.Parse("57c6b6aa-211a-4b49-a012-3fd9b4a4ea2d").IsValid())
assert.False(t, s.Parse("db9fb12c-daea-11ee-a506-0242ac120002").IsValid())
assert.False(t, s.Parse("018e0e8f-b1d9-7503-ac4a-49b18a95be69").IsValid())
assert.False(t, s.Parse("00000000-0000-0000-0000-000000000000").IsValid())
}

func TestObject_Path(t *testing.T) {
type User struct {
Firstname string
Lastname string
}

s := schema.Object(map[string]schema.ISchema{
"Firstname": schema.String().Min(2),
"Lastname": schema.String().StartsWith("d"),
}).Parse(User{Firstname: "", Lastname: ""})

assert.Len(t, s.Errors, 2)
assert.Equal(t, "Firstname", s.Errors[0].Path)
assert.Equal(t, "Lastname", s.Errors[1].Path)
}

func TestObject_ArrayObject_Path(t *testing.T) {
type Address struct {
Postcode string
}

type User struct {
Addresses []Address
}

s := schema.Object(map[string]schema.ISchema{
"Addresses": schema.Array(schema.Object(map[string]schema.ISchema{
"Postcode": schema.String().Min(4),
})),
}).Parse(User{
Addresses: []Address{{Postcode: "123"}},
})

assert.Len(t, s.Errors, 1)
assert.Equal(t, "Addresses.0.Postcode", s.Errors[0].Path)
}
8 changes: 8 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,11 @@ func (s *Schema[T]) Parse(value any) *ValidationResult {

return res
}

func formatPath(key string, path string) string {
if path != "" {
return fmt.Sprintf("%s.%s", key, path)
}

return key
}
7 changes: 7 additions & 0 deletions string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ func TestString_Type(t *testing.T) {
assert.False(t, s.Parse(0).IsValid())
}

func TestString_Path(t *testing.T) {
s := schema.String().Min(4).Parse("123")

assert.Len(t, s.Errors, 1)
assert.Equal(t, "", s.Errors[0].Path)
}

func TestString_Max(t *testing.T) {
s := schema.String().Max(5)

Expand Down