diff --git a/README.md b/README.md index bb569f5..dc8b245 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ func main() { `schema` object contains a string field, named `name`. This code validates `jsonString`. +> **Note**: You could Marshal your schema as a json object for backup usages with `json.Marshal` function. + # Fields ## Integer diff --git a/array.go b/array.go index dda42c2..5ad016a 100644 --- a/array.go +++ b/array.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) @@ -82,6 +83,27 @@ func (a *ArrayField) MaxLength(length int) *ArrayField { return a } +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) + } + + 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) + } + return json.Marshal(ArrayFieldSpec{ + Name: a.name, + Type: arrayType, + Required: a.required, + Items: items, + MinLength: a.minLength, + MaxLength: a.maxLength, + }) +} + // Array is the constructor of an array field. func Array(name string, itemField Field) *ArrayField { return &ArrayField{ diff --git a/array_spec.go b/array_spec.go index dd75670..748f9cf 100644 --- a/array_spec.go +++ b/array_spec.go @@ -2,11 +2,12 @@ package vjson // ArrayFieldSpec is a type used for parsing an ArrayField type ArrayFieldSpec struct { - Name string `mapstructure:"name"` - Required bool `mapstructure:"required"` - Items map[string]interface{} `mapstructure:"items"` - MinLength int `mapstructure:"min_length"` - MaxLength int `mapstructure:"max_length"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` + Required bool `mapstructure:"required" json:"required,omitempty"` + 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"` } // NewArray receives an ArrayFieldSpec and returns and ArrayField diff --git a/array_test.go b/array_test.go index 228f7ff..0c1ce1c 100644 --- a/array_test.go +++ b/array_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -96,6 +97,21 @@ func TestArrayField_Validate(t *testing.T) { }) } +func TestArrayField_MarshalJSON(t *testing.T) { + field := Array("foo", String("bar")) + + 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"]) + assert.Equal(t, "bar", data["items"].(map[string]interface{})["name"]) +} + func TestNewArray(t *testing.T) { field := NewArray(ArrayFieldSpec{ Name: "bar", diff --git a/boolean.go b/boolean.go index fff5a48..188a739 100644 --- a/boolean.go +++ b/boolean.go @@ -1,6 +1,9 @@ package vjson -import "github.com/pkg/errors" +import ( + "encoding/json" + "github.com/pkg/errors" +) // BooleanField is the type for validating booleans in a JSON type BooleanField struct { @@ -55,6 +58,15 @@ func (b *BooleanField) ShouldBe(value bool) *BooleanField { return b } +func (b *BooleanField) MarshalJSON() ([]byte, error) { + return json.Marshal(BooleanFieldSpec{ + Name: b.name, + Type: booleanType, + Required: b.required, + Value: b.value, + }) +} + // Boolean is the constructor of a boolean field func Boolean(name string) *BooleanField { return &BooleanField{ diff --git a/boolean_spec.go b/boolean_spec.go index 5e22845..ccf6349 100644 --- a/boolean_spec.go +++ b/boolean_spec.go @@ -2,9 +2,10 @@ package vjson // BooleanFieldSpec is a type used for parsing an BooleanField type BooleanFieldSpec struct { - Name string `mapstructure:"name"` - Required bool `mapstructure:"required"` - Value bool `mapstructure:"value"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` + Required bool `mapstructure:"required" json:"required,omitempty"` + Value bool `mapstructure:"value" json:"value,omitempty"` } // NewBoolean receives an BooleanFieldSpec and returns and BooleanField diff --git a/boolean_test.go b/boolean_test.go index bf2e572..0cec727 100644 --- a/boolean_test.go +++ b/boolean_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -55,6 +56,20 @@ func TestBooleanField_Validate(t *testing.T) { }) } +func TestBooleanField_MarshalJSON(t *testing.T) { + field := Boolean("foo") + + b, err := json.Marshal(field) + assert.Nil(t, err) + + data := map[string]string{} + err = json.Unmarshal(b, &data) + assert.Nil(t, err) + + assert.Equal(t, "foo", data["name"]) + assert.Equal(t, string(booleanType), data["type"]) +} + func TestNewBoolean(t *testing.T) { field := NewBoolean(BooleanFieldSpec{ Name: "bar", diff --git a/field.go b/field.go index 09f9a48..da1ec89 100644 --- a/field.go +++ b/field.go @@ -1,8 +1,11 @@ package vjson +import "encoding/json" + // Field is the abstraction on a field in a json. // different field types can be implemented with implementing this interface. type Field interface { + json.Marshaler GetName() string Validate(interface{}) error } diff --git a/float.go b/float.go index 0aad5b1..4d64439 100644 --- a/float.go +++ b/float.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "fmt" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -138,6 +139,25 @@ func (f *FloatField) Range(start, end float64) *FloatField { return f } +func (f *FloatField) MarshalJSON() ([]byte, error) { + ranges := make([]FloatRangeSpec, 0, len(f.ranges)) + for _, r := range f.ranges { + ranges = append(ranges, FloatRangeSpec{ + Start: r.start, + End: r.end, + }) + } + return json.Marshal(FloatFieldSpec{ + Name: f.name, + Type: floatType, + Required: f.required, + Min: f.min, + Max: f.max, + Positive: f.positive, + Ranges: ranges, + }) +} + // Float is the constructor of a float field func Float(name string) *FloatField { return &FloatField{ diff --git a/float_spec.go b/float_spec.go index 1ee8e10..a19eef2 100644 --- a/float_spec.go +++ b/float_spec.go @@ -2,18 +2,19 @@ package vjson // FloatRangeSpec is a type for parsing a float field range type FloatRangeSpec struct { - Start float64 `mapstructure:"start"` - End float64 `mapstructure:"end"` + Start float64 `mapstructure:"start" json:"start"` + End float64 `mapstructure:"end" json:"end"` } // FloatFieldSpec is a type used for parsing an FloatField type FloatFieldSpec struct { - Name string `mapstructure:"name"` - Required bool `mapstructure:"required"` - Min float64 `mapstructure:"min"` - Max float64 `mapstructure:"max"` - Positive bool `mapstructure:"positive"` - Ranges []FloatRangeSpec `mapstructure:"ranges"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` + Required bool `mapstructure:"required" json:"required,omitempty"` + Min float64 `mapstructure:"min" json:"min,omitempty"` + Max float64 `mapstructure:"max" json:"max,omitempty"` + Positive bool `mapstructure:"positive" json:"positive,omitempty"` + Ranges []FloatRangeSpec `mapstructure:"ranges" json:"ranges,omitempty"` } // NewFloat receives an FloatFieldSpec and returns and FloatField diff --git a/float_test.go b/float_test.go index 04bbbd3..b9ff459 100644 --- a/float_test.go +++ b/float_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -94,6 +95,21 @@ func TestFloatField_Validate(t *testing.T) { }) } +func TestFloatField_MarshalJSON(t *testing.T) { + field := Float("foo").Range(10, 20) + 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(floatType), data["type"]) + assert.Equal(t, float64(10), data["ranges"].([]interface{})[0].(map[string]interface{})["start"]) + assert.Equal(t, float64(20), data["ranges"].([]interface{})[0].(map[string]interface{})["end"]) +} + func TestNewFloat(t *testing.T) { field := NewFloat(FloatFieldSpec{ Name: "bar", diff --git a/go.mod b/go.mod index 76c03c2..203ba90 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/miladibra10/vjson -go 1.15 +go 1.17 require ( github.com/hashicorp/go-multierror v1.1.1 @@ -9,3 +9,12 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.7.5 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/match v1.0.3 // indirect + github.com/tidwall/pretty v1.1.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/integer.go b/integer.go index 261a634..6844ee2 100644 --- a/integer.go +++ b/integer.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "fmt" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -147,6 +148,25 @@ func (i *IntegerField) Range(start, end int) *IntegerField { return i } +func (i *IntegerField) MarshalJSON() ([]byte, error) { + ranges := make([]IntRangeSpec, 0, len(i.ranges)) + for _, r := range i.ranges { + ranges = append(ranges, IntRangeSpec{ + Start: r.start, + End: r.end, + }) + } + return json.Marshal(IntegerFieldSpec{ + Name: i.name, + Required: i.required, + Min: i.min, + Max: i.max, + Positive: i.positive, + Ranges: ranges, + Type: integerType, + }) +} + // Integer is the constructor of an integer field func Integer(name string) *IntegerField { return &IntegerField{ diff --git a/integer_spec.go b/integer_spec.go index 4d86743..5a1339e 100644 --- a/integer_spec.go +++ b/integer_spec.go @@ -2,18 +2,19 @@ package vjson // IntRangeSpec is a type for parsing an integer field range type IntRangeSpec struct { - Start int `mapstructure:"start"` - End int `mapstructure:"end"` + Start int `mapstructure:"start" json:"start"` + End int `mapstructure:"end" json:"end"` } // IntegerFieldSpec is a type used for parsing an IntegerField type IntegerFieldSpec struct { - Name string `mapstructure:"name"` - Required bool `mapstructure:"required"` - Min int `mapstructure:"min"` - Max int `mapstructure:"max"` - Positive bool `mapstructure:"positive"` - Ranges []IntRangeSpec `mapstructure:"ranges"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` + Required bool `mapstructure:"required" json:"required,omitempty"` + Min int `mapstructure:"min" json:"min,omitempty"` + Max int `mapstructure:"max" json:"max,omitempty"` + Positive bool `mapstructure:"positive" json:"positive,omitempty"` + Ranges []IntRangeSpec `mapstructure:"ranges" json:"ranges,omitempty"` } // NewInteger receives an IntegerFieldSpec and returns and IntegerField diff --git a/integer_test.go b/integer_test.go index 58eb5b9..90b2f1b 100644 --- a/integer_test.go +++ b/integer_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -120,6 +121,21 @@ func TestIntegerField_Validate(t *testing.T) { }) } +func TestIntegerField_MarshalJSON(t *testing.T) { + field := Integer("foo").Range(10, 20) + 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(integerType), data["type"]) + assert.Equal(t, float64(10), data["ranges"].([]interface{})[0].(map[string]interface{})["start"]) + assert.Equal(t, float64(20), data["ranges"].([]interface{})[0].(map[string]interface{})["end"]) +} + func TestNewInteger(t *testing.T) { field := NewInteger(IntegerFieldSpec{ Name: "bar", diff --git a/null.go b/null.go index cca84a3..3d8df63 100644 --- a/null.go +++ b/null.go @@ -1,6 +1,9 @@ package vjson -import "github.com/pkg/errors" +import ( + "encoding/json" + "github.com/pkg/errors" +) // NullField is the type for validating floats in a JSON type NullField struct { @@ -23,6 +26,13 @@ func (n *NullField) Validate(input interface{}) error { return errors.Errorf("Value for %s should be null", n.name) } +func (n *NullField) MarshalJSON() ([]byte, error) { + return json.Marshal(NullFieldSpec{ + Name: n.name, + Type: nullType, + }) +} + // Null is the constructor of a null field in a JSON. func Null(name string) *NullField { return &NullField{ diff --git a/null_spec.go b/null_spec.go index ac6dbe7..88d64a3 100644 --- a/null_spec.go +++ b/null_spec.go @@ -2,7 +2,8 @@ package vjson // NullFieldSpec is a type used for parsing an NullField type NullFieldSpec struct { - Name string `mapstructure:"name"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` } // NewNull receives an NullFieldSpec and returns and NullField diff --git a/null_test.go b/null_test.go index 3549792..39d3b16 100644 --- a/null_test.go +++ b/null_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -10,6 +11,20 @@ func TestNullField_GetName(t *testing.T) { assert.Equal(t, "foo", field.GetName()) } +func TestNullField_MarshalJSON(t *testing.T) { + field := Null("foo") + + b, err := json.Marshal(field) + assert.Nil(t, err) + + data := map[string]string{} + err = json.Unmarshal(b, &data) + assert.Nil(t, err) + + assert.Equal(t, "foo", data["name"]) + assert.Equal(t, string(nullType), data["type"]) +} + func TestNullField_Validate(t *testing.T) { t.Run("invalid_input", func(t *testing.T) { field := Null("foo") diff --git a/object.go b/object.go index 86683a1..24a26c0 100644 --- a/object.go +++ b/object.go @@ -52,6 +52,26 @@ func (o *ObjectField) Required() *ObjectField { return o } +func (o *ObjectField) MarshalJSON() ([]byte, error) { + schemaRaw, err := json.Marshal(o.schema) + if err != nil { + return nil, errors.Wrapf(err, "could not marshal schema field of object field: %s", o.name) + } + + schema := make(map[string]interface{}) + err = json.Unmarshal(schemaRaw, &schema) + if err != nil { + return nil, errors.Wrapf(err, "could not unmarshal schema field of array field: %s", o.name) + } + + return json.Marshal(ObjectFieldSpec{ + Name: o.name, + Type: objectType, + Required: o.required, + Schema: schema, + }) +} + // Object is the constructor of an object field func Object(name string, schema Schema) *ObjectField { return &ObjectField{ diff --git a/object_spec.go b/object_spec.go index afdb352..8707fc0 100644 --- a/object_spec.go +++ b/object_spec.go @@ -2,9 +2,10 @@ package vjson // ObjectFieldSpec is a type used for parsing an ObjectField type ObjectFieldSpec struct { - Name string `mapstructure:"name"` - Required bool `mapstructure:"required"` - Schema map[string]interface{} `mapstructure:"schema"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` + Required bool `mapstructure:"required" json:"required,omitempty"` + Schema map[string]interface{} `mapstructure:"schema" json:"schema,omitempty"` } // NewObject receives an ObjectFieldSpec and returns and ObjectField diff --git a/object_test.go b/object_test.go index c23be9d..76770f7 100644 --- a/object_test.go +++ b/object_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -62,6 +63,21 @@ func TestObjectField_Validate(t *testing.T) { }) } +func TestObjectField_MarshalJSON(t *testing.T) { + field := Object("foo", NewSchema(String("bar"))) + + 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(objectType), data["type"]) + assert.Equal(t, "bar", data["schema"].(map[string]interface{})["fields"].([]interface{})[0].(map[string]interface{})["name"]) +} + func TestNewObject(t *testing.T) { s := Schema{} field := NewObject(ObjectFieldSpec{ diff --git a/schema.go b/schema.go index 5bcfbd3..8ef1edd 100644 --- a/schema.go +++ b/schema.go @@ -12,7 +12,7 @@ import ( // Schema is the type for declaring a JSON schema and validating a json object. type Schema struct { - Fields []Field + Fields []Field `json:"fields"` } // SchemaSpec is used for parsing a Schema diff --git a/schema_test.go b/schema_test.go index 1a20572..46f1a50 100644 --- a/schema_test.go +++ b/schema_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -269,9 +270,9 @@ func TestReadFromFile(t *testing.T) { } func TestReadFromString(t *testing.T) { - schema, err := ReadFromString("{}") + schema, err := ReadFromString(`{"fields":[{"name":"bar","type": "string","required":true}]}`) assert.Nil(t, err) - assert.Len(t, schema.Fields, 0) + assert.Len(t, schema.Fields, 1) } func TestNewSchema(t *testing.T) { @@ -282,6 +283,23 @@ func TestNewSchema(t *testing.T) { assert.Len(t, s.Fields, 2) } +func TestSchema_MarshalJSON(t *testing.T) { + schema := NewSchema( + Integer("foo"), + String("bar").Required(), + ) + schemaBytes, _ := json.Marshal(schema) + + var newSchema Schema + + err := json.Unmarshal(schemaBytes, &newSchema) + assert.Nil(t, err) + + assert.Equal(t, schema.Fields[0], newSchema.Fields[0]) + assert.Equal(t, len(schema.Fields), len(newSchema.Fields)) + +} + func TestSchema_UnmarshalJSON(t *testing.T) { var s Schema err := s.UnmarshalJSON([]byte("{{")) diff --git a/string.go b/string.go index 40ff123..2b151f5 100644 --- a/string.go +++ b/string.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "regexp" @@ -66,7 +67,7 @@ func (s *StringField) Format(format string) *StringField { return s } -// Choices is called to set valid choices of a string field in validation +// Choices function is called to set valid choices of a string field in validation func (s *StringField) Choices(choices ...string) *StringField { s.choices = choices s.validateChoices = true @@ -128,6 +129,18 @@ func (s *StringField) Validate(value interface{}) error { return result } +func (s *StringField) MarshalJSON() ([]byte, error) { + return json.Marshal(StringFieldSpec{ + Name: s.name, + Required: s.required, + MinLength: s.minLength, + MaxLength: s.maxLength, + Format: s.format, + Choices: s.choices, + Type: stringType, + }) +} + // String is the constructor of a string field func String(name string) *StringField { return &StringField{ diff --git a/string_spec.go b/string_spec.go index 79631ad..0ec439a 100644 --- a/string_spec.go +++ b/string_spec.go @@ -2,12 +2,13 @@ package vjson // StringFieldSpec is a type used for parsing an StringField type StringFieldSpec struct { - Name string `mapstructure:"name"` - Required bool `mapstructure:"required"` - MinLength int `mapstructure:"min_length"` - MaxLength int `mapstructure:"max_length"` - Format string `mapstructure:"format"` - Choices []string `mapstructure:"choices"` + Name string `mapstructure:"name" json:"name"` + Type fieldType `json:"type"` + Required bool `mapstructure:"required" json:"required,omitempty"` + MinLength int `mapstructure:"min_length" json:"minLength,omitempty"` + MaxLength int `mapstructure:"max_length" json:"maxLength,omitempty"` + Format string `mapstructure:"format" json:"format,omitempty"` + Choices []string `mapstructure:"choices" json:"choices,omitempty"` } // NewString receives an StringFieldSpec and returns and StringField diff --git a/string_test.go b/string_test.go index 41d3e98..c8e99e5 100644 --- a/string_test.go +++ b/string_test.go @@ -1,6 +1,7 @@ package vjson import ( + "encoding/json" "github.com/stretchr/testify/assert" "testing" ) @@ -128,6 +129,20 @@ func TestStringField_Validate(t *testing.T) { }) } +func TestStringField_MarshalJSON(t *testing.T) { + field := String("foo") + + b, err := json.Marshal(field) + assert.Nil(t, err) + + data := map[string]string{} + err = json.Unmarshal(b, &data) + assert.Nil(t, err) + + assert.Equal(t, "foo", data["name"]) + assert.Equal(t, string(stringType), data["type"]) +} + func TestNewString(t *testing.T) { field := NewString(StringFieldSpec{ Name: "bar",