From 3ec4cfe059c114407e25c054e2efedcf6aed99dd Mon Sep 17 00:00:00 2001 From: thanharrow Date: Mon, 3 Jun 2024 15:57:44 +0700 Subject: [PATCH 1/3] add strict mode and array fixed position validate --- README.md | 42 +++++++++++++++++++-- array.go | 65 +++++++++++++++++++++++++++------ array_spec.go | 5 ++- array_test.go | 28 +++++++++++++- examples/schema_parsing/main.go | 17 +++++++-- schema.go | 62 +++++++++++++++++++++++++------ 6 files changed, 187 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index dc8b245..13fb16c 100644 --- a/README.md +++ b/README.md @@ -118,15 +118,19 @@ func main() { panic(err) } + schema.SetStrict(true) // default false + + // field age is not validate, so validate is false jsonString := ` { - "name": "James" + "name": "James", + "age": 10 } ` - + err = schema.ValidateString(jsonString) if err != nil { - panic(err) + fmt.Printf("schema is not valid: " + err.Error()) } } ``` @@ -137,6 +141,10 @@ func main() { > **Note**: You could Marshal your schema as a json object for backup usages with `json.Marshal` function. + +### Strict Mode +When set strict mode is true, all fields in json object must validate. Default strict mode is `false`. + # Fields ## Integer @@ -375,6 +383,34 @@ vjson.Array("foo", vjson.Integer("item").Range(0,20)).Required().MinLength(2).Ma } ``` +### Fix Position Array + +Each item has a different type in the array. + +#### Code +```go +vjson.FixArray("foo", []Field{ + vjson.String("item"), + vjson.Integer("item2") +}) +``` + +#### File +```json +{ + "name": "foo", + "type": "array", + "required": true, + "fix_items": [{ + "name": "item", + "type": "string", + }, { + "name": "item2", + "type": "integer", + }] +} +``` + ## Object An object field could be created in code like this: ```go diff --git a/array.go b/array.go index 5ad016a..6e3af2e 100644 --- a/array.go +++ b/array.go @@ -2,6 +2,7 @@ package vjson import ( "encoding/json" + "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) @@ -11,6 +12,7 @@ type ArrayField struct { name string required bool items Field + fixItems []Field minLength int minLengthValidation bool @@ -54,10 +56,23 @@ func (a *ArrayField) Validate(v interface{}) error { } } - for _, value := range values { - err := a.items.Validate(value) - if err != nil { - result = multierror.Append(result, errors.Wrapf(err, "%v item is invalid in %s array", value, a.name)) + if a.items != nil { + for _, value := range values { + err := a.items.Validate(value) + if err != nil { + result = multierror.Append(result, errors.Wrapf(err, "%v item is invalid in %s array", value, a.name)) + } + } + } else if a.fixItems != nil { + if len(a.fixItems) != len(values) { + result = multierror.Append(result, errors.Errorf("length of %s array should is equal %d", a.name, len(a.fixItems))) + return result + } + for i, value := range values { + err := a.fixItems[i].Validate(value) + if err != nil { + result = multierror.Append(result, errors.Wrapf(err, "%v item is invalid in %s array", value, a.name)) + } } } return result @@ -84,21 +99,40 @@ func (a *ArrayField) MaxLength(length int) *ArrayField { } func (a *ArrayField) MarshalJSON() ([]byte, error) { - itemsRaw, err := json.Marshal(a.items) - if err != nil { - return nil, errors.Wrapf(err, "could not marshal items field of array field: %s", a.name) + var items map[string]interface{} + if a.items != nil { + itemsRaw, err := json.Marshal(a.items) + if err != nil { + return nil, errors.Wrapf(err, "could not marshal items field of array field: %s", a.name) + } + + items = make(map[string]interface{}) + err = json.Unmarshal(itemsRaw, &items) + if err != nil { + return nil, errors.Wrapf(err, "could not unmarshal items field of array field: %s", a.name) + } } - items := make(map[string]interface{}) - err = json.Unmarshal(itemsRaw, &items) - if err != nil { - return nil, errors.Wrapf(err, "could not unmarshal items field of array field: %s", a.name) + var fixItems []map[string]interface{} + if a.fixItems != nil { + itemsRaw, err := json.Marshal(a.fixItems) + if err != nil { + return nil, errors.Wrapf(err, "could not marshal fix items field of array field: %s", a.name) + } + + fixItems = []map[string]interface{}{} + err = json.Unmarshal(itemsRaw, &fixItems) + if err != nil { + return nil, errors.Wrapf(err, "could not unmarshal fix items field of array field: %s", a.name) + } } + return json.Marshal(ArrayFieldSpec{ Name: a.name, Type: arrayType, Required: a.required, Items: items, + FixItems: fixItems, MinLength: a.minLength, MaxLength: a.maxLength, }) @@ -112,3 +146,12 @@ func Array(name string, itemField Field) *ArrayField { items: itemField, } } + +// Array is the constructor of an array field. +func FixArray(name string, itemFields []Field) *ArrayField { + return &ArrayField{ + name: name, + required: false, + fixItems: itemFields, + } +} diff --git a/array_spec.go b/array_spec.go index 748f9cf..271907f 100644 --- a/array_spec.go +++ b/array_spec.go @@ -8,14 +8,17 @@ type ArrayFieldSpec struct { Items map[string]interface{} `mapstructure:"items" json:"items,omitempty"` MinLength int `mapstructure:"min_length" json:"minLength,omitempty"` MaxLength int `mapstructure:"max_length" json:"maxLength,omitempty"` + + FixItems []map[string]interface{} `mapstructure:"fix_items" json:"fix_items,omitempty"` } // NewArray receives an ArrayFieldSpec and returns and ArrayField -func NewArray(spec ArrayFieldSpec, itemField Field, minLengthValidation, maxLengthValidation bool) *ArrayField { +func NewArray(spec ArrayFieldSpec, itemField Field, fixItemsField []Field, minLengthValidation, maxLengthValidation bool) *ArrayField { return &ArrayField{ name: spec.Name, required: spec.Required, items: itemField, + fixItems: fixItemsField, minLength: spec.MinLength, minLengthValidation: minLengthValidation, maxLength: spec.MaxLength, diff --git a/array_test.go b/array_test.go index 0c1ce1c..f6d2b78 100644 --- a/array_test.go +++ b/array_test.go @@ -2,8 +2,10 @@ package vjson import ( "encoding/json" - "github.com/stretchr/testify/assert" + "fmt" "testing" + + "github.com/stretchr/testify/assert" ) func TestArrayField_GetName(t *testing.T) { @@ -116,10 +118,32 @@ func TestNewArray(t *testing.T) { field := NewArray(ArrayFieldSpec{ Name: "bar", Required: true, - }, String("foo"), false, false) + }, String("foo"), nil, false, false) assert.NotNil(t, field) assert.Equal(t, "bar", field.name) assert.Equal(t, false, field.minLengthValidation) assert.Equal(t, false, field.maxLengthValidation) } + +// ---- for fixItems Array --- +func TestFixPositionArrayField_MarshalJSON(t *testing.T) { + field := FixArray("foo", []Field{ + Integer("bar"), + String("baz"), + }) + + b, err := json.Marshal(field) + assert.Nil(t, err) + + data := map[string]interface{}{} + err = json.Unmarshal(b, &data) + assert.Nil(t, err) + + assert.Equal(t, "foo", data["name"]) + assert.Equal(t, string(arrayType), data["type"]) + fmt.Printf("%v", data) + fixItems := data["fix_items"].([]interface{}) + assert.Equal(t, "bar", fixItems[0].(map[string]interface{})["name"]) + assert.Equal(t, "baz", fixItems[1].(map[string]interface{})["name"]) +} diff --git a/examples/schema_parsing/main.go b/examples/schema_parsing/main.go index 70f6004..5f9d0cd 100644 --- a/examples/schema_parsing/main.go +++ b/examples/schema_parsing/main.go @@ -1,6 +1,8 @@ package main -import "github.com/miladibra10/vjson" +import ( + "github.com/miladibra10/vjson" +) func main() { schemaStr := ` @@ -8,8 +10,15 @@ func main() { "fields": [ { "name": "name", - "type": "string" - "required": true + "type": "array", + "required": true, + "fix_items": [{ + "name": "11", + "type": "string" + }, { + "name": "12", + "type": "integer" + }] } ] } @@ -21,7 +30,7 @@ func main() { jsonString := ` { - "name": "James" + "name": ["hello", 123] } ` diff --git a/schema.go b/schema.go index 8ef1edd..d3d89ce 100644 --- a/schema.go +++ b/schema.go @@ -2,17 +2,19 @@ package vjson import ( "encoding/json" + "io/ioutil" + "os" + "github.com/hashicorp/go-multierror" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/tidwall/gjson" - "io/ioutil" - "os" ) // Schema is the type for declaring a JSON schema and validating a json object. type Schema struct { - Fields []Field `json:"fields"` + Fields []Field `json:"fields"` + StrictMode bool } // SchemaSpec is used for parsing a Schema @@ -20,6 +22,10 @@ type SchemaSpec struct { Fields []map[string]interface{} `json:"fields"` } +func (s *Schema) SetStrict(strict bool) { + s.StrictMode = strict +} + // UnmarshalJSON is implemented for parsing a Schema. it overrides json.Unmarshal behaviour. func (s *Schema) UnmarshalJSON(bytes []byte) error { var schemaSpec SchemaSpec @@ -186,23 +192,37 @@ func (s *Schema) getArrayField(fieldSpec map[string]interface{}) (*ArrayField, e return nil, errors.Errorf("name field is required for an array field") } + var itemField Field + var fixItemFields []Field + itemsFieldSpecRaw, found := fieldSpec["items"] - if !found { + fixItemsFieldSpecRaw, foundFixItem := fieldSpec["fix_items"] + if !found && !foundFixItem { return nil, errors.Errorf("items key is missing for array field name: %s", arraySpec.Name) } - itemsFieldSpec, ok := itemsFieldSpecRaw.(map[string]interface{}) - if !ok { + + if itemsFieldSpec, ok := itemsFieldSpecRaw.(map[string]interface{}); ok { + itemField, err = s.getField(itemsFieldSpec) + if err != nil { + return nil, errors.Wrapf(err, "could not get item field of array field name: %s", arraySpec.Name) + } + } else if fixItemsFieldSpec, ok := fixItemsFieldSpecRaw.([]interface{}); ok { + fixItemFields = []Field{} + for _, fixFieldSpec := range fixItemsFieldSpec { + fixItemField, err := s.getField(fixFieldSpec.(map[string]interface{})) + if err != nil { + return nil, errors.Wrapf(err, "could not get item field of array field name: %s", arraySpec.Name) + } + fixItemFields = append(fixItemFields, fixItemField) + } + } else { return nil, errors.Errorf("invalid format for items key for array field name: %s", arraySpec.Name) } - itemField, err := s.getField(itemsFieldSpec) - if err != nil { - return nil, errors.Wrapf(err, "could not get item field of array field name: %s", arraySpec.Name) - } _, minLenValidation := fieldSpec["min_length"] _, maxLenValidation := fieldSpec["max_length"] - arrayField := NewArray(arraySpec, itemField, minLenValidation, maxLenValidation) + arrayField := NewArray(arraySpec, itemField, fixItemFields, minLenValidation, maxLenValidation) return arrayField, nil } @@ -293,6 +313,26 @@ func (s *Schema) ValidateString(input string) error { func (s *Schema) validateJSON(json gjson.Result) error { var result error + if s.StrictMode { + if json.IsObject() { + jsonMap := json.Map() + for jsonField := range jsonMap { + fieldFound := false + for _, field := range s.Fields { + if jsonField == field.GetName() { + fieldFound = true + } + } + if !fieldFound { + result = multierror.Append(result, errors.Errorf("Field %s is not validata", jsonField)) + } + } + if result != nil { + return result + } + } + } + for _, field := range s.Fields { fieldName := field.GetName() fieldValue := json.Get(fieldName).Value() From 40d5f4f2ef2f3a6ebe13e271677cdb46850b1c0c Mon Sep 17 00:00:00 2001 From: thanharrow Date: Mon, 3 Jun 2024 23:42:44 +0700 Subject: [PATCH 2/3] add fix for StrictMode --- README.md | 13 +++++----- array.go | 7 +++++- array_test.go | 3 +-- examples/schema_parsing/main.go | 44 +++++++++++++++++++++++++++++++-- object.go | 6 +++++ object_test.go | 32 +++++++++++++++++++++++- schema.go | 17 +++++++------ 7 files changed, 102 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 13fb16c..b823fe6 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ import "github.com/miladibra10/vjson" func main() { schemaStr := ` { + "strict": true, "fields": [ { "name": "name", @@ -118,8 +119,6 @@ func main() { panic(err) } - schema.SetStrict(true) // default false - // field age is not validate, so validate is false jsonString := ` { @@ -142,8 +141,6 @@ func main() { > **Note**: You could Marshal your schema as a json object for backup usages with `json.Marshal` function. -### Strict Mode -When set strict mode is true, all fields in json object must validate. Default strict mode is `false`. # Fields @@ -383,7 +380,7 @@ vjson.Array("foo", vjson.Integer("item").Range(0,20)).Required().MinLength(2).Ma } ``` -### Fix Position Array +### Fixed Length Array Each item has a different type in the array. @@ -424,6 +421,9 @@ the first argument is the name of object field, and the second one is the schema some validation characteristics could be added to an array field with chaining some functions: + [Required()](#object) sets the field as a required field. validation will return an error if a required field is not present in json object. ++ [Strict()](#object) +When set strict mode is true, all fields in json object must validate. Default strict mode is `false`. + object field could be described by a json for schema parsing. + **`name`**: the name of the field @@ -439,7 +439,7 @@ a required object field, named `foo` which its valid value is an object with `na vjson.Object("foo", vjson.NewSchema( vjson.String("name").Required(), vjson.String("last_name").Required(), - )).Required() + )).Required().Strict() ``` #### File @@ -449,6 +449,7 @@ vjson.Object("foo", vjson.NewSchema( "type": "object", "required": true, "schema": { + "strict": true, "fields": [ { "name": "name", diff --git a/array.go b/array.go index 6e3af2e..351b5de 100644 --- a/array.go +++ b/array.go @@ -56,6 +56,10 @@ func (a *ArrayField) Validate(v interface{}) error { } } + if a.items != nil && a.fixItems != nil { + result = multierror.Append(result, errors.Errorf("could not using both items key and fix items for array %s", a.name)) + } + if a.items != nil { for _, value := range values { err := a.items.Validate(value) @@ -63,7 +67,8 @@ func (a *ArrayField) Validate(v interface{}) error { result = multierror.Append(result, errors.Wrapf(err, "%v item is invalid in %s array", value, a.name)) } } - } else if a.fixItems != nil { + } + if a.fixItems != nil { if len(a.fixItems) != len(values) { result = multierror.Append(result, errors.Errorf("length of %s array should is equal %d", a.name, len(a.fixItems))) return result diff --git a/array_test.go b/array_test.go index f6d2b78..e24c5ec 100644 --- a/array_test.go +++ b/array_test.go @@ -2,7 +2,6 @@ package vjson import ( "encoding/json" - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -142,7 +141,7 @@ func TestFixPositionArrayField_MarshalJSON(t *testing.T) { assert.Equal(t, "foo", data["name"]) assert.Equal(t, string(arrayType), data["type"]) - fmt.Printf("%v", data) + fixItems := data["fix_items"].([]interface{}) assert.Equal(t, "bar", fixItems[0].(map[string]interface{})["name"]) assert.Equal(t, "baz", fixItems[1].(map[string]interface{})["name"]) diff --git a/examples/schema_parsing/main.go b/examples/schema_parsing/main.go index 5f9d0cd..6085519 100644 --- a/examples/schema_parsing/main.go +++ b/examples/schema_parsing/main.go @@ -7,6 +7,7 @@ import ( func main() { schemaStr := ` { + "strict": true, "fields": [ { "name": "name", @@ -19,6 +20,39 @@ func main() { "name": "12", "type": "integer" }] + }, + { + "name": "person", + "type": "object", + "required": true, + "schema": { + "strict": true, + "fields": [ + { + "name": "name", + "type": "object", + "required": true, + "schema": { + "strict": true, + "fields": [{ + "name": "first", + "type": "string", + "required": true + }, + { + "name": "last", + "type": "string", + "required": true + }] + } + }, + { + "name": "gender", + "type": "string", + "required": true + } + ] + } } ] } @@ -27,10 +61,16 @@ func main() { if err != nil { panic(err) } - jsonString := ` { - "name": ["hello", 123] + "name": ["hello", 123], + "person": { + "name": { + "first": "asg", + "last": "4234" + }, + "gender": "male" + } } ` diff --git a/object.go b/object.go index 24a26c0..794ae70 100644 --- a/object.go +++ b/object.go @@ -2,6 +2,7 @@ package vjson import ( "encoding/json" + "github.com/pkg/errors" ) @@ -52,6 +53,11 @@ func (o *ObjectField) Required() *ObjectField { return o } +func (o *ObjectField) Strict() *ObjectField { + o.schema.StrictMode = true + return o +} + func (o *ObjectField) MarshalJSON() ([]byte, error) { schemaRaw, err := json.Marshal(o.schema) if err != nil { diff --git a/object_test.go b/object_test.go index 76770f7..8566198 100644 --- a/object_test.go +++ b/object_test.go @@ -2,8 +2,9 @@ package vjson import ( "encoding/json" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestObjectField_GetName(t *testing.T) { @@ -61,6 +62,35 @@ func TestObjectField_Validate(t *testing.T) { assert.Nil(t, err) }) }) + + t.Run("strict_field", func(t *testing.T) { + t.Run("strict_off", func(t *testing.T) { + field := Object("foo", objSchema).Required() + + err := field.Validate(`{"age":10, "name": "john"}`) + assert.Nil(t, err) + }) + + t.Run("strict_on", func(t *testing.T) { + field := Object("foo", objSchema).Required().Strict() + + err := field.Validate(`{"age":10, "name": "john"}`) + assert.NotNil(t, err) + }) + + t.Run("strict_on_struct", func(t *testing.T) { + objSchemaStrict := Schema{ + Fields: []Field{ + Integer("age").Min(0).Max(90).Required(), + }, + } + objSchemaStrict.StrictMode = true + field := Object("foo", objSchemaStrict) + + err := field.Validate(`{"age":10, "name": "john"}`) + assert.NotNil(t, err) + }) + }) } func TestObjectField_MarshalJSON(t *testing.T) { diff --git a/schema.go b/schema.go index d3d89ce..2853e93 100644 --- a/schema.go +++ b/schema.go @@ -14,16 +14,13 @@ import ( // Schema is the type for declaring a JSON schema and validating a json object. type Schema struct { Fields []Field `json:"fields"` - StrictMode bool + StrictMode bool `json:"strict"` } // SchemaSpec is used for parsing a Schema type SchemaSpec struct { - Fields []map[string]interface{} `json:"fields"` -} - -func (s *Schema) SetStrict(strict bool) { - s.StrictMode = strict + Fields []map[string]interface{} `json:"fields"` + StrictMode bool `json:"strict"` } // UnmarshalJSON is implemented for parsing a Schema. it overrides json.Unmarshal behaviour. @@ -34,6 +31,7 @@ func (s *Schema) UnmarshalJSON(bytes []byte) error { return errors.Wrap(err, "could not unmarshal to SchemaSpec") } s.Fields = make([]Field, 0, len(schemaSpec.Fields)) + s.StrictMode = schemaSpec.StrictMode var result error @@ -200,6 +198,9 @@ func (s *Schema) getArrayField(fieldSpec map[string]interface{}) (*ArrayField, e if !found && !foundFixItem { return nil, errors.Errorf("items key is missing for array field name: %s", arraySpec.Name) } + if found && foundFixItem { + return nil, errors.Errorf("could not both using items key and fix items for array field name: %s", arraySpec.Name) + } if itemsFieldSpec, ok := itemsFieldSpecRaw.(map[string]interface{}); ok { itemField, err = s.getField(itemsFieldSpec) @@ -313,8 +314,8 @@ func (s *Schema) ValidateString(input string) error { func (s *Schema) validateJSON(json gjson.Result) error { var result error - if s.StrictMode { - if json.IsObject() { + if json.IsObject() { + if s.StrictMode { jsonMap := json.Map() for jsonField := range jsonMap { fieldFound := false From 96ff4efaa9901d4c3fb39483f2595a53b17da9a7 Mon Sep 17 00:00:00 2001 From: thanharrow Date: Tue, 4 Jun 2024 00:26:59 +0700 Subject: [PATCH 3/3] add test --- object_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/object_test.go b/object_test.go index 8566198..037b2b4 100644 --- a/object_test.go +++ b/object_test.go @@ -78,6 +78,27 @@ func TestObjectField_Validate(t *testing.T) { assert.NotNil(t, err) }) + t.Run("strict_off_and_not_required", func(t *testing.T) { + field := Object("foo", objSchema) + + err := field.Validate(`{"age":10, "name": "john"}`) + assert.Nil(t, err) + }) + + t.Run("strict_on_and_not_required", func(t *testing.T) { + field := Object("foo", objSchema).Strict() + + err := field.Validate(`{"age":10, "name": "john"}`) + assert.NotNil(t, err) + }) + + t.Run("strict_on_and_not_required_for_nil", func(t *testing.T) { + field := Object("foo", objSchema).Strict() + + err := field.Validate(nil) + assert.Nil(t, err) + }) + t.Run("strict_on_struct", func(t *testing.T) { objSchemaStrict := Schema{ Fields: []Field{