diff --git a/boolean.go b/boolean.go index 626ed47..d5130e2 100644 --- a/boolean.go +++ b/boolean.go @@ -3,8 +3,11 @@ package zog import ( "github.com/Oudwins/zog/conf" p "github.com/Oudwins/zog/internals" + "github.com/Oudwins/zog/zconst" ) +var _ ZogSchema = &boolProcessor{} + type boolProcessor struct { preTransforms []p.PreTransform tests []p.Test @@ -12,12 +15,37 @@ type boolProcessor struct { defaultVal *bool required *p.Test catch *bool + coercer conf.CoercerFunc +} + +// ! INTERNALS + +// Returns the type of the schema +func (v *boolProcessor) getType() zconst.ZogType { + return zconst.TypeBool +} + +// Sets the coercer for the schema +func (v *boolProcessor) setCoercer(c conf.CoercerFunc) { + v.coercer = c +} + +// Internal function to process the data +func (v *boolProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { + primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, v.coercer, p.IsParseZeroValue) } -func Bool() *boolProcessor { - return &boolProcessor{ - tests: []p.Test{}, +// ! USER FACING FUNCTIONS + +// Returns a new Bool Schema +func Bool(opts ...SchemaOption) *boolProcessor { + b := &boolProcessor{ + coercer: conf.Coercers.Bool, // default coercer + } + for _, opt := range opts { + opt(b) } + return b } func (v *boolProcessor) Parse(data any, dest *bool, options ...ParsingOption) p.ZogErrList { @@ -33,10 +61,6 @@ func (v *boolProcessor) Parse(data any, dest *bool, options ...ParsingOption) p. return errs.List } -func (v *boolProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { - primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, conf.Coercers.Bool, p.IsParseZeroValue) -} - // GLOBAL METHODS // Adds pretransform function to schema diff --git a/boolean_test.go b/boolean_test.go index bfedc84..de373a8 100644 --- a/boolean_test.go +++ b/boolean_test.go @@ -5,6 +5,8 @@ import ( "testing" p "github.com/Oudwins/zog/internals" + "github.com/Oudwins/zog/zconst" + "github.com/stretchr/testify/assert" ) func TestBoolParse(t *testing.T) { @@ -50,6 +52,17 @@ func TestBoolParse(t *testing.T) { } } +func TestBoolSchemaOption(t *testing.T) { + s := Bool(WithCoercer(func(original any) (value any, err error) { + return true, nil + })) + + var result bool + err := s.Parse("asdasdas", &result) + assert.Nil(t, err) + assert.Equal(t, true, result) +} + func TestParsingOption(t *testing.T) { t.Run("Parse context is passed to parsing option", func(t *testing.T) { boolProc := Bool() @@ -439,3 +452,8 @@ func TestBoolPostTransform(t *testing.T) { }) } } + +func TestBoolGetType(t *testing.T) { + s := Bool() + assert.Equal(t, zconst.TypeBool, s.getType()) +} diff --git a/conf/Coercers.go b/conf/Coercers.go index c23ce6c..8cde650 100644 --- a/conf/Coercers.go +++ b/conf/Coercers.go @@ -7,6 +7,27 @@ import ( "time" ) +func TimeCoercerFactory(format func(data string) (time.Time, error)) CoercerFunc { + return func(data any) (any, error) { + switch v := data.(type) { + case time.Time: + return v, nil + case string: + tim, err := format(v) + if err != nil { + return nil, fmt.Errorf("failed to parse time: %v", err) + } + return tim, nil + case int: + return time.Unix(int64(v), 0), nil + case int64: + return time.Unix(v, 0), nil + default: + return nil, fmt.Errorf("input data is an unsupported type to coerce to time.Time: %v", data) + } + } +} + // takes in an original value and attempts to coerce it into another type. Returns an error if the coercion fails. type CoercerFunc = func(original any) (value any, err error) @@ -97,24 +118,9 @@ var DefaultCoercers = struct { return nil, fmt.Errorf("input data is an unsupported type to coerce to float64: %v", data) } }, - Time: func(data any) (any, error) { - switch v := data.(type) { - case time.Time: - return v, nil - case string: - tim, err := time.Parse(time.RFC3339, v) - if err != nil { - return nil, fmt.Errorf("failed to parse time: %v", err) - } - return tim, nil - case int: - return time.Unix(int64(v), 0), nil - case int64: - return time.Unix(v, 0), nil - default: - return nil, fmt.Errorf("input data is an unsupported type to coerce to time.Time: %v", data) - } - }, + Time: TimeCoercerFactory(func(data string) (time.Time, error) { + return time.Parse(time.RFC3339, data) + }), Slice: func(data any) (any, error) { refVal := reflect.TypeOf(data) switch refVal.Kind() { diff --git a/numbers.go b/numbers.go index c40079a..dcb55df 100644 --- a/numbers.go +++ b/numbers.go @@ -3,12 +3,15 @@ package zog import ( "github.com/Oudwins/zog/conf" p "github.com/Oudwins/zog/internals" + "github.com/Oudwins/zog/zconst" ) type Numeric interface { ~int | ~float64 } +var _ ZogSchema = &numberProcessor[int]{} + type numberProcessor[T Numeric] struct { preTransforms []p.PreTransform tests []p.Test @@ -16,16 +19,48 @@ type numberProcessor[T Numeric] struct { defaultVal *T required *p.Test catch *T + coercer conf.CoercerFunc +} + +// ! INTERNALS + +// Returns the type of the schema +func (v *numberProcessor[T]) getType() zconst.ZogType { + return zconst.TypeNumber +} + +// Sets the coercer for the schema +func (v *numberProcessor[T]) setCoercer(c conf.CoercerFunc) { + v.coercer = c } -// creates a new float64 processor -func Float() *numberProcessor[float64] { - return &numberProcessor[float64]{} +// Internal function to process the data +func (v *numberProcessor[T]) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { + primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, v.coercer, p.IsParseZeroValue) +} + +// ! USER FACING FUNCTIONS + +// creates a new float64 schema +func Float(opts ...SchemaOption) *numberProcessor[float64] { + s := &numberProcessor[float64]{ + coercer: conf.Coercers.Float64, + } + for _, opt := range opts { + opt(s) + } + return s } -// creates a new int processor -func Int() *numberProcessor[int] { - return &numberProcessor[int]{} +// creates a new int schema +func Int(opts ...SchemaOption) *numberProcessor[int] { + s := &numberProcessor[int]{ + coercer: conf.Coercers.Int, + } + for _, opt := range opts { + opt(s) + } + return s } // parses the value and stores it in the destination @@ -43,19 +78,6 @@ func (v *numberProcessor[T]) Parse(data any, dest *T, options ...ParsingOption) return errs.List } -func (v *numberProcessor[T]) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { - - var coercer conf.CoercerFunc - switch any(dest).(type) { - case *float64: - coercer = conf.Coercers.Float64 - case *int: - coercer = conf.Coercers.Int - } - - primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, coercer, p.IsParseZeroValue) -} - // GLOBAL METHODS func (v *numberProcessor[T]) PreTransform(transform p.PreTransform) *numberProcessor[T] { diff --git a/numbers_test.go b/numbers_test.go index d39be4e..b88f600 100644 --- a/numbers_test.go +++ b/numbers_test.go @@ -3,9 +3,32 @@ package zog import ( "testing" + "github.com/Oudwins/zog/zconst" "github.com/stretchr/testify/assert" ) +func TestIntSchemaOption(t *testing.T) { + s := Int(WithCoercer(func(original any) (value any, err error) { + return 42, nil + })) + + var result int + err := s.Parse("123", &result) + assert.Nil(t, err) + assert.Equal(t, 42, result) +} + +func TestFloatSchemaOption(t *testing.T) { + s := Float(WithCoercer(func(original any) (value any, err error) { + return 3.14, nil + })) + + var result float64 + err := s.Parse("2.718", &result) + assert.Nil(t, err) + assert.Equal(t, 3.14, result) +} + func TestNumberRequired(t *testing.T) { validator := Int().Required(Message("custom")) var dest int @@ -258,3 +281,13 @@ func TestNumberCustomTest(t *testing.T) { } assert.Equal(t, 5, dest) } + +func TestIntGetType(t *testing.T) { + i := Int() + assert.Equal(t, zconst.TypeNumber, i.getType()) +} + +func TestFloatGetType(t *testing.T) { + f := Float() + assert.Equal(t, zconst.TypeNumber, f.getType()) +} diff --git a/slices.go b/slices.go index 3a010c6..cb0b9bc 100644 --- a/slices.go +++ b/slices.go @@ -9,36 +9,32 @@ import ( "github.com/Oudwins/zog/zconst" ) +var _ ZogSchema = &sliceProcessor{} + type sliceProcessor struct { preTransforms []p.PreTransform tests []p.Test - schema Processor + schema ZogSchema postTransforms []p.PostTransform required *p.Test defaultVal any // catch any + coercer conf.CoercerFunc } -func Slice(schema Processor) *sliceProcessor { - return &sliceProcessor{ - schema: schema, - tests: []p.Test{}, - } -} +// ! INTERNALS -// only supports val = slice[any] & dest = &slice[] -func (v *sliceProcessor) Parse(data any, dest any, options ...ParsingOption) p.ZogErrMap { - errs := p.NewErrsMap() - ctx := p.NewParseCtx(errs, conf.ErrorFormatter) - for _, opt := range options { - opt(ctx) - } - path := p.PathBuilder("") - v.process(data, dest, path, ctx) +// Returns the type of the schema +func (v *sliceProcessor) getType() zconst.ZogType { + return zconst.TypeSlice +} - return errs.M +// Sets the coercer for the schema +func (v *sliceProcessor) setCoercer(c conf.CoercerFunc) { + v.coercer = c } +// Internal function to process the data func (v *sliceProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { destType := zconst.TypeSlice // 1. preTransforms @@ -85,7 +81,7 @@ func (v *sliceProcessor) process(val any, dest any, path p.PathBuilder, ctx Pars } } else { // make sure val is a slice if not try to make it one - v, err := conf.Coercers.Slice(val) + v, err := v.coercer(val) if err != nil { ctx.NewError(path, Errors.New(zconst.ErrCodeCoerce, val, destType, nil, "", err)) return @@ -120,6 +116,30 @@ func (v *sliceProcessor) process(val any, dest any, path p.PathBuilder, ctx Pars // 4. postTransforms -> defered see above } +func Slice(schema ZogSchema, opts ...SchemaOption) *sliceProcessor { + s := &sliceProcessor{ + schema: schema, + coercer: conf.Coercers.Slice, // default coercer + } + for _, opt := range opts { + opt(s) + } + return s +} + +// only supports val = slice[any] & dest = &slice[] +func (v *sliceProcessor) Parse(data any, dest any, options ...ParsingOption) p.ZogErrMap { + errs := p.NewErrsMap() + ctx := p.NewParseCtx(errs, conf.ErrorFormatter) + for _, opt := range options { + opt(ctx) + } + path := p.PathBuilder("") + v.process(data, dest, path, ctx) + + return errs.M +} + // Adds pretransform function to schema func (v *sliceProcessor) PreTransform(transform p.PreTransform) *sliceProcessor { if v.preTransforms == nil { diff --git a/slices_test.go b/slices_test.go index 36b5fc7..b51dc1b 100644 --- a/slices_test.go +++ b/slices_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/Oudwins/zog/zconst" "github.com/stretchr/testify/assert" ) @@ -228,3 +229,19 @@ func TestSliceCustomTest(t *testing.T) { assert.Equal(t, "custom", errs["$root"][0].Message()) assert.Equal(t, "custom_test", errs["$root"][0].Code()) } + +func TestSliceSchemaOption(t *testing.T) { + s := Slice(String(), WithCoercer(func(original any) (value any, err error) { + return []string{"coerced"}, nil + })) + + var result []string + err := s.Parse(123, &result) + assert.Nil(t, err) + assert.Equal(t, []string{"coerced"}, result) +} + +func TestSliceGetType(t *testing.T) { + s := Slice(String()) + assert.Equal(t, zconst.TypeSlice, s.getType()) +} diff --git a/string.go b/string.go index 85d609b..ffcbf12 100644 --- a/string.go +++ b/string.go @@ -22,14 +22,40 @@ type stringProcessor struct { defaultVal *string required *p.Test catch *string + coercer conf.CoercerFunc } -func String() *stringProcessor { - return &stringProcessor{ - tests: []p.Test{}, +// ! INTERNALS + +// Internal function to process the data +func (v *stringProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { + primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, v.coercer, p.IsParseZeroValue) +} + +// Returns the type of the schema +func (v *stringProcessor) getType() zconst.ZogType { + return zconst.TypeString +} + +// Sets the coercer for the schema +func (v *stringProcessor) setCoercer(c conf.CoercerFunc) { + v.coercer = c +} + +// ! USER FACING FUNCTIONS + +// Returns a new String Schema +func String(opts ...SchemaOption) *stringProcessor { + s := &stringProcessor{ + coercer: conf.Coercers.String, // default coercer + } + for _, opt := range opts { + opt(s) } + return s } +// Parses the data into the destination string. Returns a list of errors func (v *stringProcessor) Parse(data any, dest *string, options ...ParsingOption) p.ZogErrList { errs := p.NewErrsList() ctx := p.NewParseCtx(errs, conf.ErrorFormatter) @@ -43,10 +69,6 @@ func (v *stringProcessor) Parse(data any, dest *string, options ...ParsingOption return errs.List } -func (v *stringProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { - primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, conf.Coercers.String, p.IsParseZeroValue) -} - // Adds pretransform function to schema func (v *stringProcessor) PreTransform(transform p.PreTransform) *stringProcessor { if v.preTransforms == nil { @@ -95,7 +117,9 @@ func (v *stringProcessor) Catch(val string) *stringProcessor { return v } -// ! VALIDATORS +// ! PRETRANSFORMS + +// ! Tests // custom test function call it -> schema.Test(t z.Test, opts ...TestOption) func (v *stringProcessor) Test(t p.Test, opts ...TestOption) *stringProcessor { for _, opt := range opts { @@ -105,7 +129,7 @@ func (v *stringProcessor) Test(t p.Test, opts ...TestOption) *stringProcessor { return v } -// checks that the value is one of the enum values +// Test: checks that the value is one of the enum values func (v *stringProcessor) OneOf(enum []string, options ...TestOption) *stringProcessor { t := p.In(enum) for _, opt := range options { @@ -115,7 +139,7 @@ func (v *stringProcessor) OneOf(enum []string, options ...TestOption) *stringPro return v } -// checks that the value is at least n characters long +// Test: checks that the value is at least n characters long func (v *stringProcessor) Min(n int, options ...TestOption) *stringProcessor { t := p.LenMin[string](n) for _, opt := range options { @@ -125,7 +149,7 @@ func (v *stringProcessor) Min(n int, options ...TestOption) *stringProcessor { return v } -// checks that the value is at most n characters long +// Test: checks that the value is at most n characters long func (v *stringProcessor) Max(n int, options ...TestOption) *stringProcessor { t := p.LenMax[string](n) for _, opt := range options { @@ -135,7 +159,7 @@ func (v *stringProcessor) Max(n int, options ...TestOption) *stringProcessor { return v } -// checks that the value is exactly n characters long +// Test: checks that the value is exactly n characters long func (v *stringProcessor) Len(n int, options ...TestOption) *stringProcessor { t := p.Len[string](n) for _, opt := range options { @@ -145,7 +169,7 @@ func (v *stringProcessor) Len(n int, options ...TestOption) *stringProcessor { return v } -// checks that the value is a valid email address +// Test: checks that the value is a valid email address func (v *stringProcessor) Email(options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeEmail, @@ -164,6 +188,7 @@ func (v *stringProcessor) Email(options ...TestOption) *stringProcessor { return v } +// Test: checks that the value is a valid URL func (v *stringProcessor) URL(options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeURL, @@ -183,6 +208,7 @@ func (v *stringProcessor) URL(options ...TestOption) *stringProcessor { return v } +// Test: checks that the value has the prefix func (v *stringProcessor) HasPrefix(s string, options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeHasPrefix, @@ -203,6 +229,7 @@ func (v *stringProcessor) HasPrefix(s string, options ...TestOption) *stringProc return v } +// Test: checks that the value has the suffix func (v *stringProcessor) HasSuffix(s string, options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeHasSuffix, @@ -223,6 +250,7 @@ func (v *stringProcessor) HasSuffix(s string, options ...TestOption) *stringProc return v } +// Test: checks that the value contains the substring func (v *stringProcessor) Contains(sub string, options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeContains, @@ -243,6 +271,7 @@ func (v *stringProcessor) Contains(sub string, options ...TestOption) *stringPro return v } +// Test: checks that the value contains an uppercase letter func (v *stringProcessor) ContainsUpper(options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeContainsUpper, @@ -266,6 +295,7 @@ func (v *stringProcessor) ContainsUpper(options ...TestOption) *stringProcessor return v } +// Test: checks that the value contains a digit func (v *stringProcessor) ContainsDigit(options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeContainsDigit, @@ -291,6 +321,7 @@ func (v *stringProcessor) ContainsDigit(options ...TestOption) *stringProcessor return v } +// Test: checks that the value contains a special character func (v *stringProcessor) ContainsSpecial(options ...TestOption) *stringProcessor { t := p.Test{ @@ -318,7 +349,7 @@ func (v *stringProcessor) ContainsSpecial(options ...TestOption) *stringProcesso return v } -// checks that the value is a valid uuid +// Test: checks that the value is a valid uuid func (v *stringProcessor) UUID(options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeUUID, @@ -337,7 +368,7 @@ func (v *stringProcessor) UUID(options ...TestOption) *stringProcessor { return v } -// checks that value matches to regex +// Test: checks that value matches to regex func (v *stringProcessor) Match(regex *regexp.Regexp, options ...TestOption) *stringProcessor { t := p.Test{ ErrCode: zconst.ErrCodeMatch, diff --git a/string_test.go b/string_test.go index 64a4f10..17847ec 100644 --- a/string_test.go +++ b/string_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/Oudwins/zog/zconst" "github.com/stretchr/testify/assert" ) @@ -347,3 +348,19 @@ func TestStringRegex(t *testing.T) { assert.Empty(t, errs) assert.Equal(t, "00", dest) } + +func TestStringSchemaOption(t *testing.T) { + s := String(WithCoercer(func(original any) (value any, err error) { + return "coerced", nil + })) + + var result string + err := s.Parse(123, &result) + assert.Nil(t, err) + assert.Equal(t, "coerced", result) +} + +func TestStringGetType(t *testing.T) { + s := String() + assert.Equal(t, zconst.TypeString, s.getType()) +} diff --git a/struct.go b/struct.go index 4915745..9fa4f87 100644 --- a/struct.go +++ b/struct.go @@ -11,19 +11,7 @@ import ( "github.com/Oudwins/zog/zconst" ) -type StructParser interface { - Parse(val p.DataProvider, destPtr any) p.ZogErrMap -} - -// A map of field names to zog schemas -type Schema map[string]Processor - -// Returns a new structProcessor which can be used to parse input data into a struct -func Struct(schema Schema) *structProcessor { - return &structProcessor{ - schema: schema, - } -} +var _ ZogSchema = &structProcessor{} type structProcessor struct { preTransforms []p.PreTransform @@ -35,55 +23,14 @@ type structProcessor struct { // catch any } -// WARNING. THIS WILL PROBABLY BE DEPRECATED SOON IN FAVOR OF z.Merge(schema1, schema2) -func (v *structProcessor) Merge(other *structProcessor) *structProcessor { - new := &structProcessor{ - // preTransforms: make([]p.PreTransform, len(v.preTransforms)+len(other.preTransforms)), - // postTransforms: make([]p.PostTransform, len(v.postTransforms)+len(other.postTransforms)), - // tests: make([]p.Test, len(v.tests)+len(other.tests)), - preTransforms: make([]p.PreTransform, 0), - postTransforms: make([]p.PostTransform, 0), - tests: make([]p.Test, 0), - } - if v.preTransforms != nil { - new.preTransforms = append(new.preTransforms, v.preTransforms...) - } - if other.preTransforms != nil { - new.preTransforms = append(new.preTransforms, other.preTransforms...) - } - - if v.postTransforms != nil { - new.postTransforms = append(new.postTransforms, v.postTransforms...) - } - if other.postTransforms != nil { - new.postTransforms = append(new.postTransforms, other.postTransforms...) - } - - if v.tests != nil { - new.tests = append(new.tests, v.tests...) - } - if other.tests != nil { - new.tests = append(new.tests, other.tests...) - } - new.required = v.required - new.schema = Schema{} - maps.Copy(new.schema, v.schema) - maps.Copy(new.schema, other.schema) - return new +// Returns the type of the schema +func (v *structProcessor) getType() zconst.ZogType { + return zconst.TypeStruct } -// Parses val into destPtr and validates each field based on the schema. Only supports val = map[string]any & dest = &struct -func (v *structProcessor) Parse(data any, destPtr any, options ...ParsingOption) p.ZogErrMap { - errs := p.NewErrsMap() - ctx := p.NewParseCtx(errs, conf.ErrorFormatter) - for _, opt := range options { - opt(ctx) - } - path := p.PathBuilder("") - - v.process(data, destPtr, path, ctx) - - return errs.M +// Sets the coercer for the schema +func (v *structProcessor) setCoercer(c conf.CoercerFunc) { + // noop } func (v *structProcessor) process(data any, dest any, path p.PathBuilder, ctx ParseCtx) { @@ -181,6 +128,69 @@ func (v *structProcessor) process(data any, dest any, path p.PathBuilder, ctx Pa } +// ! USER FACING FUNCTIONS + +// A map of field names to zog schemas +type Schema map[string]ZogSchema + +// Returns a new structProcessor which can be used to parse input data into a struct +func Struct(schema Schema) *structProcessor { + return &structProcessor{ + schema: schema, + } +} + +// WARNING. THIS WILL PROBABLY BE DEPRECATED SOON IN FAVOR OF z.Merge(schema1, schema2) +func (v *structProcessor) Merge(other *structProcessor) *structProcessor { + new := &structProcessor{ + // preTransforms: make([]p.PreTransform, len(v.preTransforms)+len(other.preTransforms)), + // postTransforms: make([]p.PostTransform, len(v.postTransforms)+len(other.postTransforms)), + // tests: make([]p.Test, len(v.tests)+len(other.tests)), + preTransforms: make([]p.PreTransform, 0), + postTransforms: make([]p.PostTransform, 0), + tests: make([]p.Test, 0), + } + if v.preTransforms != nil { + new.preTransforms = append(new.preTransforms, v.preTransforms...) + } + if other.preTransforms != nil { + new.preTransforms = append(new.preTransforms, other.preTransforms...) + } + + if v.postTransforms != nil { + new.postTransforms = append(new.postTransforms, v.postTransforms...) + } + if other.postTransforms != nil { + new.postTransforms = append(new.postTransforms, other.postTransforms...) + } + + if v.tests != nil { + new.tests = append(new.tests, v.tests...) + } + if other.tests != nil { + new.tests = append(new.tests, other.tests...) + } + new.required = v.required + new.schema = Schema{} + maps.Copy(new.schema, v.schema) + maps.Copy(new.schema, other.schema) + return new +} + +// Parses val into destPtr and validates each field based on the schema. Only supports val = map[string]any & dest = &struct +func (v *structProcessor) Parse(data any, destPtr any, options ...ParsingOption) p.ZogErrMap { + errs := p.NewErrsMap() + ctx := p.NewParseCtx(errs, conf.ErrorFormatter) + for _, opt := range options { + opt(ctx) + } + path := p.PathBuilder("") + + v.process(data, destPtr, path, ctx) + + return errs.M +} + // Add a pretransform step to the schema func (v *structProcessor) PreTransform(transform p.PreTransform) *structProcessor { if v.preTransforms == nil { diff --git a/struct_test.go b/struct_test.go index b3d8954..f10ae56 100644 --- a/struct_test.go +++ b/struct_test.go @@ -6,6 +6,7 @@ import ( "time" p "github.com/Oudwins/zog/internals" + "github.com/Oudwins/zog/zconst" "github.com/stretchr/testify/assert" ) @@ -346,3 +347,10 @@ func TestStructRequired(t *testing.T) { // assert.Equal(t, "", output.OptionalField) // }) // } + +func TestStructGetType(t *testing.T) { + s := Struct(Schema{ + "field": String(), + }) + assert.Equal(t, zconst.TypeStruct, s.getType()) +} diff --git a/time.go b/time.go index 858317e..16314aa 100644 --- a/time.go +++ b/time.go @@ -8,6 +8,9 @@ import ( "github.com/Oudwins/zog/zconst" ) +// ! INTERNALS +var _ ZogSchema = &timeProcessor{} + type timeProcessor struct { preTransforms []p.PreTransform tests []p.Test @@ -15,12 +18,61 @@ type timeProcessor struct { defaultVal *time.Time required *p.Test catch *time.Time + coercer conf.CoercerFunc +} + +// internal processes the data +func (v *timeProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { + primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, v.coercer, p.IsParseZeroValue) +} + +// Returns the type of the schema +func (v *timeProcessor) getType() zconst.ZogType { + return zconst.TypeTime } -func Time() *timeProcessor { - return &timeProcessor{} +// Sets the coercer for the schema +func (v *timeProcessor) setCoercer(c conf.CoercerFunc) { + v.coercer = c } +type TimeFunc func(opts ...SchemaOption) *timeProcessor + +// ! USER FACING FUNCTIONS + +// Returns a new Time Schema +var Time TimeFunc = func(opts ...SchemaOption) *timeProcessor { + t := &timeProcessor{ + coercer: conf.Coercers.Time, + } + for _, opt := range opts { + opt(t) + } + return t +} + +// Sets the format function for the time schema +// Usage is: +// +// z.Time(z.Time.FormatFunc(func(data string) (time.Time, error) { +// return time.Parse(time.RFC3339, data) +// })) +func (t TimeFunc) FormatFunc(format func(data string) (time.Time, error)) SchemaOption { + return func(s ZogSchema) { + s.setCoercer(conf.TimeCoercerFactory(format)) + } +} + +// Sets the string format for the time schema +// Usage is: +// z.Time(z.Time.Format(time.RFC3339)) +func (t TimeFunc) Format(format string) SchemaOption { + return t.FormatFunc(func(data string) (time.Time, error) { + return time.Parse(format, data) + }) +} + +// Parses the data into the destination time.Time. Returns a list of errors func (v *timeProcessor) Parse(data any, dest *time.Time, options ...ParsingOption) p.ZogErrList { errs := p.NewErrsList() ctx := p.NewParseCtx(errs, conf.ErrorFormatter) @@ -36,10 +88,6 @@ func (v *timeProcessor) Parse(data any, dest *time.Time, options ...ParsingOptio return errs.List } -func (v *timeProcessor) process(val any, dest any, path p.PathBuilder, ctx ParseCtx) { - primitiveProcessor(val, dest, path, ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, conf.Coercers.Time, p.IsParseZeroValue) -} - // Adds pretransform function to schema func (v *timeProcessor) PreTransform(transform p.PreTransform) *timeProcessor { if v.preTransforms == nil { diff --git a/time_test.go b/time_test.go index 240228c..38b2178 100644 --- a/time_test.go +++ b/time_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/Oudwins/zog/zconst" "github.com/stretchr/testify/assert" ) @@ -126,3 +127,37 @@ func TestTimeCustomTest(t *testing.T) { assert.NotNil(t, errs) assert.Equal(t, "custom", errs[0].Message()) } + +func TestTimeSchemaOption(t *testing.T) { + s := Time(WithCoercer(func(original any) (value any, err error) { + return time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), nil + })) + + var result time.Time + err := s.Parse("invalid-date", &result) + assert.Nil(t, err) + assert.Equal(t, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), result) +} + +func TestTimeFormat(t *testing.T) { + s := Time(Time.Format(time.RFC1123)) + var result time.Time + err := s.Parse("Mon, 01 Jan 2024 00:00:00 UTC", &result) + assert.Nil(t, err) + assert.Equal(t, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), result) +} + +func TestTimeFormatFunc(t *testing.T) { + s := Time(Time.FormatFunc(func(data string) (time.Time, error) { + return time.Parse(time.RFC1123, data) + })) + var result time.Time + err := s.Parse("Mon, 01 Jan 2024 00:00:00 UTC", &result) + assert.Nil(t, err) + assert.Equal(t, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), result) +} + +func TestTimeGetType(t *testing.T) { + s := Time() + assert.Equal(t, zconst.TypeTime, s.getType()) +} diff --git a/utils.go b/utils.go index 6558cd1..f3dc582 100644 --- a/utils.go +++ b/utils.go @@ -10,20 +10,37 @@ import ( "github.com/Oudwins/zog/zconst" ) -type Processor interface { +// The ZogSchema is the interface all schemas must implement +type ZogSchema interface { process(val any, dest any, path p.PathBuilder, ctx ParseCtx) + setCoercer(c conf.CoercerFunc) + getType() zconst.ZogType } // ! Passing Types through +// ParseCtx is the context passed through the parser type ParseCtx = p.ParseCtx -type Test = p.Test +// ZogError is the ZogError interface type ZogError = p.ZogError -type ZogErrMap = p.ZogErrMap + +// ZogErrList is a []ZogError returned from parsing primitive schemas type ZogErrList = p.ZogErrList +// ZogErrMap is a map[string][]ZogError returned from parsing complex schemas +type ZogErrMap = p.ZogErrMap + // ! TESTS + +// Test is the test object +type Test = p.Test + +// TestFunc is a helper function to define a custom test. It takes the error code which will be used for the error message and a validate function. Usage: +// +// schema.Test(z.TestFunc(zconst.ErrCodeCustom, func(val any, ctx ParseCtx) bool { +// return val == "hello" +// })) func TestFunc(errCode zconst.ZogErrCode, validateFunc p.TestFunc) p.Test { t := p.Test{ ErrCode: errCode, @@ -36,7 +53,7 @@ func TestFunc(errCode zconst.ZogErrCode, validateFunc p.TestFunc) p.Test { type errHelpers struct { } -// Beware this API may change +// Helper struct for dealing with zog errors. Beware this API may change var Errors = errHelpers{} // Create error from (originValue any, destinationValue any, test *p.Test) diff --git a/utilsOptions.go b/utilsOptions.go index 9198d4b..37cbac8 100644 --- a/utilsOptions.go +++ b/utilsOptions.go @@ -1,9 +1,11 @@ package zog import ( + "github.com/Oudwins/zog/conf" p "github.com/Oudwins/zog/internals" ) +// Options that can be passed to a test type TestOption = func(test *p.Test) // Message is a function that allows you to set a custom message for the test. @@ -22,6 +24,16 @@ func MessageFunc(fn p.ErrFmtFunc) TestOption { } } +// Options that can be passed to a `schema.New()` call +type SchemaOption = func(s ZogSchema) + +func WithCoercer(c conf.CoercerFunc) SchemaOption { + return func(s ZogSchema) { + s.setCoercer(c) + } +} + +// Options that can be passed to a `schema.Parse()` call type ParsingOption = func(p *p.ZogParseCtx) func WithErrFormatter(fmter p.ErrFmtFunc) ParsingOption {