diff --git a/README.md b/README.md index dc8b245..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,15 +119,17 @@ func main() { panic(err) } + // 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 +140,8 @@ func main() { > **Note**: You could Marshal your schema as a json object for backup usages with `json.Marshal` function. + + # Fields ## Integer @@ -375,6 +380,34 @@ vjson.Array("foo", vjson.Integer("item").Range(0,20)).Required().MinLength(2).Ma } ``` +### Fixed Length 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 @@ -388,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 @@ -403,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 @@ -413,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 5ad016a..351b5de 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,28 @@ 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 && 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) + if err != nil { + result = multierror.Append(result, errors.Wrapf(err, "%v item is invalid in %s array", value, a.name)) + } + } + } + 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 +104,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 +151,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..e24c5ec 100644 --- a/array_test.go +++ b/array_test.go @@ -2,8 +2,9 @@ package vjson import ( "encoding/json" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestArrayField_GetName(t *testing.T) { @@ -116,10 +117,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"]) + + 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..6085519 100644 --- a/examples/schema_parsing/main.go +++ b/examples/schema_parsing/main.go @@ -1,15 +1,58 @@ package main -import "github.com/miladibra10/vjson" +import ( + "github.com/miladibra10/vjson" +) func main() { schemaStr := ` { + "strict": true, "fields": [ { "name": "name", - "type": "string" - "required": true + "type": "array", + "required": true, + "fix_items": [{ + "name": "11", + "type": "string" + }, { + "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 + } + ] + } } ] } @@ -18,10 +61,16 @@ func main() { if err != nil { panic(err) } - jsonString := ` { - "name": "James" + "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..037b2b4 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,56 @@ 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_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{ + 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 8ef1edd..2853e93 100644 --- a/schema.go +++ b/schema.go @@ -2,22 +2,25 @@ 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 `json:"strict"` } // SchemaSpec is used for parsing a Schema type SchemaSpec struct { - Fields []map[string]interface{} `json:"fields"` + Fields []map[string]interface{} `json:"fields"` + StrictMode bool `json:"strict"` } // UnmarshalJSON is implemented for parsing a Schema. it overrides json.Unmarshal behaviour. @@ -28,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 @@ -186,23 +190,40 @@ 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 { - return nil, errors.Errorf("invalid format for items key 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) } - 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) + + 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) } _, 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 +314,26 @@ func (s *Schema) ValidateString(input string) error { func (s *Schema) validateJSON(json gjson.Result) error { var result error + if json.IsObject() { + if s.StrictMode { + 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()