Skip to content

Commit

Permalink
add array
Browse files Browse the repository at this point in the history
aacebo committed Oct 8, 2024
1 parent fe6c884 commit 4d0d1bb
Showing 15 changed files with 361 additions and 89 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ if err := schema.Validate("..."); err != nil { // nil
| Int ||
| String ||
| Object ||
| Array | |
| Array | |
| Time ||
| Union ||
| Custom Error Messages ||
30 changes: 18 additions & 12 deletions any.go
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ type AnySchema struct {

func Any() *AnySchema {
self := &AnySchema{[]Rule{}}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -44,7 +44,7 @@ func (self *AnySchema) Message(message string) *AnySchema {
}

func (self *AnySchema) Required() *AnySchema {
return self.Rule("required", true, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("required", true, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, errors.New("required")
}
@@ -54,7 +54,7 @@ func (self *AnySchema) Required() *AnySchema {
}

func (self *AnySchema) Enum(values ...any) *AnySchema {
return self.Rule("enum", values, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("enum", values, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -84,24 +84,30 @@ func (self AnySchema) Validate(value any) error {
}

func (self AnySchema) validate(key string, value reflect.Value) error {
err := NewErrorGroup(key)
err := NewEmptyError("", key)

for _, rule := range self.rules {
if rule.Resolve == nil {
continue
}

v, e := rule.Resolve(rule, value)
v, e := rule.Resolve(value)

if e != nil {
message := e.Error()

if rule.Message != "" {
message = rule.Message
if group, ok := e.(ErrorGroup); ok {
for _, subErr := range group {
err = err.Add(subErr)
}
} else {
message := e.Error()

if rule.Message != "" {
message = rule.Message
}

err = err.Add(NewError(rule.Key, key, message))
continue
}

err = err.Add(NewError(rule.Key, key, message))
continue
}

value = reflect.ValueOf(v)
121 changes: 121 additions & 0 deletions array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package owl

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
)

type ArraySchema struct {
schema *AnySchema
of Schema
}

func Array(schema Schema) *ArraySchema {
self := &ArraySchema{Any(), schema}
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}

if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
return value.Interface(), errors.New("must be an array/slice")
}

return value.Interface(), nil
})

self.Rule("items", schema, nil)
return self
}

func (self ArraySchema) Type() string {
return fmt.Sprintf("array[%s]", self.of.Type())
}

func (self *ArraySchema) Rule(key string, value any, rule RuleFn) *ArraySchema {
self.schema.Rule(key, value, rule)
return self
}

func (self *ArraySchema) Message(message string) *ArraySchema {
self.schema.Message(message)
return self
}

func (self *ArraySchema) Required() *ArraySchema {
self.schema.Required()
return self
}

func (self *ArraySchema) Min(min int) *ArraySchema {
return self.Rule("min", min, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}

if value.Len() < min {
return value.Interface(), fmt.Errorf("must have length of at least %d", min)
}

return value.Interface(), nil
})
}

func (self *ArraySchema) Max(max int) *ArraySchema {
return self.Rule("max", max, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}

if value.Len() > max {
return value.Interface(), fmt.Errorf("must have length of at most %d", max)
}

return value.Interface(), nil
})
}

func (self ArraySchema) MarshalJSON() ([]byte, error) {
return json.Marshal(self.schema)
}

func (self ArraySchema) Validate(value any) error {
return self.validate("", reflect.Indirect(reflect.ValueOf(value)))
}

func (self ArraySchema) validate(key string, value reflect.Value) error {
if err := self.schema.validate(key, value); err != nil {
return err
}

if !value.IsValid() {
return nil
}

if value.Kind() == reflect.Interface {
value = value.Elem()
}

err := NewEmptyError("items", key)

for i := 0; i < value.Len(); i++ {
item := reflect.Indirect(value.Index(i))

if item.Kind() == reflect.Interface {
item = item.Elem()
}

if e := self.of.validate(strconv.Itoa(i), item); e != nil {
err = err.Add(e)
}
}

if len(err.Errors) > 0 {
return err
}

return nil
}
179 changes: 179 additions & 0 deletions array_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package owl_test

import (
"encoding/json"
"testing"

"github.com/aacebo/owl"
)

func Test_Array(t *testing.T) {
t.Run("required", func(t *testing.T) {
t.Run("should succeed", func(t *testing.T) {
err := owl.Array(owl.String()).Required().Validate([]any{})

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should fail", func(t *testing.T) {
err := owl.Array(owl.String()).Required().Validate(nil)

if err == nil {
t.Fatal()
}
})
})

t.Run("items", func(t *testing.T) {
t.Run("should succeed", func(t *testing.T) {
err := owl.Array(owl.String().Required()).Required().Validate([]string{"test"})

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should succeed when item not required", func(t *testing.T) {
err := owl.Array(owl.String()).Required().Validate([]any{nil})

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should fail when not array", func(t *testing.T) {
err := owl.Array(owl.Bool()).Validate("test")

if err == nil {
t.FailNow()
}
})

t.Run("should fail when item type invalid", func(t *testing.T) {
err := owl.Array(owl.Bool()).Required().Validate([]string{"test"})

if err == nil {
t.FailNow()
}
})

t.Run("should fail when item required", func(t *testing.T) {
err := owl.Array(owl.String().Required()).Required().Validate([]any{nil})

if err == nil {
t.FailNow()
}
})
})

t.Run("message", func(t *testing.T) {
t.Run("should have custom error message", func(t *testing.T) {
err := owl.Array(owl.String()).Required().Message("a test message").Validate(nil)

if err == nil {
t.FailNow()
}

if err.Error() != `{"errors":[{"rule":"required","message":"a test message"}]}` {
t.Errorf(
"expected `%s`, received `%s`",
`{"errors":[{"rule":"required","message":"required"}]}`,
err.Error(),
)
}
})
})

t.Run("min", func(t *testing.T) {
t.Run("should succeed when nil", func(t *testing.T) {
err := owl.Array(owl.String()).Min(5).Validate(nil)

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should succeed when gt min", func(t *testing.T) {
err := owl.Array(owl.String()).Min(5).Validate([]string{
"a", "b", "c", "d", "e",
})

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should fail when lt min", func(t *testing.T) {
err := owl.Array(owl.String()).Min(5).Validate([]string{
"a", "b", "c", "d",
})

if err == nil {
t.Fatal()
}
})
})

t.Run("max", func(t *testing.T) {
t.Run("should succeed when nil", func(t *testing.T) {
err := owl.Array(owl.String()).Max(5).Validate(nil)

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should succeed when lt max", func(t *testing.T) {
err := owl.Array(owl.String()).Max(5).Validate([]string{
"a", "b", "c", "d", "e",
})

if err != nil {
t.Fatal(err.Error())
}
})

t.Run("should fail when gt max", func(t *testing.T) {
err := owl.Array(owl.String()).Max(5).Validate([]string{
"a", "b", "c", "d", "e", "f",
})

if err == nil {
t.Fatal()
}
})
})

t.Run("json", func(t *testing.T) {
t.Run("serialize", func(t *testing.T) {
schema := owl.Array(owl.String()).Required()
b, err := json.Marshal(schema)

if err != nil {
t.Error(err)
}

if string(b) != `{"items":{"type":"string"},"required":true,"type":"array[string]"}` {
t.Errorf(
"expected `%s`, received `%s`",
`{"items":{"type":"string"},"required":true,"type":"array[string]"}`,
string(b),
)
}
})
})
}

func ExampleArray() {
schema := owl.Array(owl.String().Required())

if err := schema.Validate([]string{"test"}); err != nil { // nil
panic(err)
}

if err := schema.Validate([]int{1}); err != nil { // error
panic(err)
}
}
2 changes: 1 addition & 1 deletion bool.go
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ type BoolSchema struct {

func Bool() *BoolSchema {
self := &BoolSchema{Any()}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
21 changes: 18 additions & 3 deletions error.go
Original file line number Diff line number Diff line change
@@ -5,19 +5,34 @@ import (
)

type Error struct {
Rule string `json:"rule,omitempty"`
Key string `json:"key,omitempty"`
Message string `json:"message,omitempty"`
Rule string `json:"rule,omitempty"`
Key string `json:"key,omitempty"`
Message string `json:"message,omitempty"`
Errors []error `json:"errors,omitempty"`
}

func NewError(rule string, key string, message string) Error {
return Error{
Rule: rule,
Key: key,
Message: message,
Errors: []error{},
}
}

func NewEmptyError(rule string, key string) Error {
return Error{
Rule: rule,
Key: key,
Errors: []error{},
}
}

func (self Error) Add(err error) Error {
self.Errors = append(self.Errors, err)
return self
}

func (self Error) Error() string {
b, _ := json.Marshal(self)
return string(b)
17 changes: 1 addition & 16 deletions error_group.go
Original file line number Diff line number Diff line change
@@ -2,22 +2,7 @@ package owl

import "encoding/json"

type ErrorGroup struct {
Key string `json:"key,omitempty"`
Errors []error `json:"errors,omitempty"`
}

func NewErrorGroup(key string) ErrorGroup {
return ErrorGroup{
Key: key,
Errors: []error{},
}
}

func (self ErrorGroup) Add(err error) ErrorGroup {
self.Errors = append(self.Errors, err)
return self
}
type ErrorGroup []error

func (self ErrorGroup) Error() string {
b, _ := json.Marshal(self)
34 changes: 0 additions & 34 deletions error_group_test.go

This file was deleted.

6 changes: 3 additions & 3 deletions float.go
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ type FloatSchema struct {

func Float() *FloatSchema {
self := &FloatSchema{Any()}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -63,7 +63,7 @@ func (self *FloatSchema) Enum(values ...float64) *FloatSchema {
}

func (self *FloatSchema) Min(min float64) *FloatSchema {
return self.Rule("min", min, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("min", min, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -77,7 +77,7 @@ func (self *FloatSchema) Min(min float64) *FloatSchema {
}

func (self *FloatSchema) Max(max float64) *FloatSchema {
return self.Rule("max", max, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("max", max, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
6 changes: 3 additions & 3 deletions int.go
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ type IntSchema struct {

func Int() *IntSchema {
self := &IntSchema{Any()}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -63,7 +63,7 @@ func (self *IntSchema) Enum(values ...int) *IntSchema {
}

func (self *IntSchema) Min(min int) *IntSchema {
return self.Rule("min", min, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("min", min, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -77,7 +77,7 @@ func (self *IntSchema) Min(min int) *IntSchema {
}

func (self *IntSchema) Max(max int) *IntSchema {
return self.Rule("max", max, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("max", max, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
8 changes: 4 additions & 4 deletions object.go
Original file line number Diff line number Diff line change
@@ -13,8 +13,7 @@ type ObjectSchema struct {

func Object() *ObjectSchema {
self := &ObjectSchema{Any(), map[string]Schema{}}
self.Rule("fields", self.fields, nil)
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -26,6 +25,7 @@ func Object() *ObjectSchema {
return value.Interface(), nil
})

self.Rule("fields", self.fields, nil)
return self
}

@@ -90,7 +90,7 @@ func (self ObjectSchema) validate(key string, value reflect.Value) error {
}

func (self ObjectSchema) validateMap(key string, value reflect.Value) error {
err := NewErrorGroup(key)
err := NewEmptyError("fields", key)

for name, schema := range self.fields {
k := reflect.ValueOf(name)
@@ -113,7 +113,7 @@ func (self ObjectSchema) validateMap(key string, value reflect.Value) error {
}

func (self ObjectSchema) validateStruct(key string, value reflect.Value) error {
err := NewErrorGroup(key)
err := NewEmptyError("fields", key)

for name, schema := range self.fields {
fieldName, exists := self.getStructFieldByName(name, value)
2 changes: 1 addition & 1 deletion rule.go
Original file line number Diff line number Diff line change
@@ -9,4 +9,4 @@ type Rule struct {
Resolve RuleFn `json:"-"`
}

type RuleFn func(rule Rule, value reflect.Value) (any, error)
type RuleFn func(value reflect.Value) (any, error)
14 changes: 7 additions & 7 deletions string.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ type StringSchema struct {

func String() *StringSchema {
self := &StringSchema{Any()}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -62,7 +62,7 @@ func (self *StringSchema) Enum(values ...string) *StringSchema {
}

func (self *StringSchema) Min(min int) *StringSchema {
return self.Rule("min", min, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("min", min, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -76,7 +76,7 @@ func (self *StringSchema) Min(min int) *StringSchema {
}

func (self *StringSchema) Max(max int) *StringSchema {
return self.Rule("max", max, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("max", max, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -90,7 +90,7 @@ func (self *StringSchema) Max(max int) *StringSchema {
}

func (self *StringSchema) Regex(re *regexp.Regexp) *StringSchema {
return self.Rule("regex", re.String(), func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("regex", re.String(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -104,7 +104,7 @@ func (self *StringSchema) Regex(re *regexp.Regexp) *StringSchema {
}

func (self *StringSchema) Email() *StringSchema {
return self.Rule("email", true, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("email", true, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -121,7 +121,7 @@ func (self *StringSchema) Email() *StringSchema {
}

func (self *StringSchema) UUID() *StringSchema {
return self.Rule("uuid", true, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("uuid", true, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -138,7 +138,7 @@ func (self *StringSchema) UUID() *StringSchema {
}

func (self *StringSchema) URL() *StringSchema {
return self.Rule("url", true, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("url", true, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
6 changes: 3 additions & 3 deletions time.go
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ type TimeSchema struct {

func Time() *TimeSchema {
self := &TimeSchema{Any(), time.RFC3339}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -65,7 +65,7 @@ func (self *TimeSchema) Required() *TimeSchema {
}

func (self *TimeSchema) Min(min time.Time) *TimeSchema {
return self.Rule("min", min, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("min", min, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
@@ -81,7 +81,7 @@ func (self *TimeSchema) Min(min time.Time) *TimeSchema {
}

func (self *TimeSchema) Max(max time.Time) *TimeSchema {
return self.Rule("max", max, func(rule Rule, value reflect.Value) (any, error) {
return self.Rule("max", max, func(value reflect.Value) (any, error) {
if !value.IsValid() {
return nil, nil
}
2 changes: 1 addition & 1 deletion union.go
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ type UnionSchema struct {

func Union(anyOf ...Schema) *UnionSchema {
self := &UnionSchema{Any(), anyOf}
self.Rule("type", self.Type(), func(rule Rule, value reflect.Value) (any, error) {
self.Rule("type", self.Type(), func(value reflect.Value) (any, error) {
for _, schema := range self.anyOf {
e := schema.Validate(value.Interface())

0 comments on commit 4d0d1bb

Please sign in to comment.