Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: schema custom coercer support #48

Merged
merged 3 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,49 @@ 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
postTransforms []p.PostTransform
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 {
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions boolean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -439,3 +452,8 @@ func TestBoolPostTransform(t *testing.T) {
})
}
}

func TestBoolGetType(t *testing.T) {
s := Bool()
assert.Equal(t, zconst.TypeBool, s.getType())
}
42 changes: 24 additions & 18 deletions conf/Coercers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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() {
Expand Down
60 changes: 41 additions & 19 deletions numbers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,64 @@ 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
postTransforms []p.PostTransform
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
Expand All @@ -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] {
Expand Down
33 changes: 33 additions & 0 deletions numbers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
56 changes: 38 additions & 18 deletions slices.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading