diff --git a/compose/service/record.go b/compose/service/record.go index 046c58009..1736e2cfa 100644 --- a/compose/service/record.go +++ b/compose/service/record.go @@ -131,7 +131,7 @@ func (svc record) With(ctx context.Context) RecordService { // Initialize validator and setup all checkers it needs validator := values.Validator() - validator.UniqueChecker(func(m *types.Module, f *types.ModuleField, v *types.RecordValue) (uint64, error) { + validator.UniqueChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (uint64, error) { if v.Ref == 0 { return 0, nil } @@ -139,7 +139,7 @@ func (svc record) With(ctx context.Context) RecordService { return repository.Record(ctx, db).RefValueLookup(m.ID, f.Name, v.Ref) }) - validator.RecordRefChecker(func(m *types.Module, f *types.ModuleField, v *types.RecordValue) (bool, error) { + validator.RecordRefChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) { if v.Ref == 0 { return false, nil } @@ -148,11 +148,20 @@ func (svc record) With(ctx context.Context) RecordService { return r != nil, err }) - validator.UserRefChecker(func(m *types.Module, f *types.ModuleField, v *types.RecordValue) (bool, error) { + validator.UserRefChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) { // @todo cross service check return true, nil }) + validator.FileRefChecker(func(v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) { + if v.Ref == 0 { + return false, nil + } + + r, err := repository.Attachment(ctx, db).FindByID(m.NamespaceID, v.Ref) + return r != nil, err + }) + return &record{ db: db, ctx: ctx, @@ -430,7 +439,7 @@ func (svc record) Update(upd *types.Record) (rec *types.Record, err error) { // This logic is kept in a utility function - it's used in the beginning // of the update procedure and after results are back from the automation scripts // - // Both these points introduce external data that need to be checked fully in the same manner + // Both these points introduce external data that need to be checked fully in the same maner procChanges := func(m *types.Module, upd *types.Record, old *types.Record) *types.RecordValueErrorSet { // First sanitization // diff --git a/compose/service/values/sanitizer.go b/compose/service/values/sanitizer.go index 26eebb542..e814910de 100644 --- a/compose/service/values/sanitizer.go +++ b/compose/service/values/sanitizer.go @@ -2,28 +2,13 @@ package values import ( "github.com/cortezaproject/corteza-server/compose/types" - "regexp" "strconv" "strings" "time" ) -const ( - boolTrue = "1" - boolFalse = "0" -) - type ( - sanitizer struct { - } -) - -var ( - // value resembles something that can be true - truthy = regexp.MustCompile(`^(t(rue)?|y(es)?|1)$`) - - // valeu resembles something that can be a reference - refy = regexp.MustCompile(`^[1-9](\d*)$`) + sanitizer struct{} ) // Sanitizer initializes sanitizer @@ -86,42 +71,42 @@ func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.Reco // Per field type validators switch strings.ToLower(f.Kind) { case "bool": - v = s.sanitizeBool(v, f, m) + v = s.sBool(v, f, m) case "datetime": - v = s.sanitizeDatetime(v, f, m) - case "email": - v = s.sanitizeEmail(v, f, m) - case "file": - v = s.sanitizeFile(v, f, m) - case "number": - v = s.sanitizeNumber(v, f, m) - case "record": - v = s.sanitizeRecord(v, f, m) - case "select": - v = s.sanitizeSelect(v, f, m) - case "string": - v = s.sanitizeString(v, f, m) - case "url": - v = s.sanitizeUrl(v, f, m) - case "user": - v = s.sanitizeUser(v, f, m) + v = s.sDatetime(v, f, m) + //case "email": + // v = s.sEmail(v, f, m) + //case "file": + // v = s.sFile(v, f, m) + //case "number": + // v = s.sNumber(v, f, m) + //case "record": + // v = s.sRecord(v, f, m) + //case "select": + // v = s.sSelect(v, f, m) + //case "string": + // v = s.sString(v, f, m) + //case "url": + // v = s.sUrl(v, f, m) + //case "user": + // v = s.sUser(v, f, m) } } return } -func (sanitizer) sanitizeBool(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +func (sanitizer) sBool(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { if truthy.MatchString(strings.ToLower(v.Value)) { - v.Value = boolTrue + v.Value = strBoolTrue } else { - v.Value = boolFalse + v.Value = strBoolFalse } return v } -func (sanitizer) sanitizeDatetime(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +func (sanitizer) sDatetime(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { var ( // input format set inputFormats []string @@ -131,26 +116,32 @@ func (sanitizer) sanitizeDatetime(v *types.RecordValue, f *types.ModuleField, m ) if f.Options.Bool("onlyDate") { - of = "2006-01-02" + of = datetimeInputFormatDate inputFormats = []string{ - "2006-01-02", + datetimeInputFormatDate, "02 Jan 06", "Monday, 02-Jan-06", "Mon, 02 Jan 2006", "2019/_1/_2", } } else if f.Options.Bool("onlyTime") { - of = "15:04:05" + of = datetimeInputFormatTime inputFormats = []string{ - "15:04:05", + datetimeInputFormatTime, "15:04", + "15:04:05Z07:00", + "15:04:05 MST", + "15:04:05 -0700", + "15:04 MST", + "15:04Z07:00", + "15:04 -0700", time.Kitchen, } } else { - of = time.RFC3339 + of = datetimeInputFormatFull // date & time inputFormats = []string{ - time.RFC3339, + datetimeInputFormatFull, time.RFC1123Z, time.RFC1123, time.RFC850, @@ -176,34 +167,38 @@ func (sanitizer) sanitizeDatetime(v *types.RecordValue, f *types.ModuleField, m return v } -func (sanitizer) sanitizeEmail(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeFile(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeNumber(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeRecord(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeSelect(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeString(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeUrl(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} - -func (sanitizer) sanitizeUser(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { - return v -} +// +//func (sanitizer) sEmail(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// // @todo extract from "name" format +// return v +//} +// +//func (sanitizer) sFile(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// return v +//} +// +//func (sanitizer) sNumber(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// // @todo cut off the decimals / round up +// // @todo sanitize decimal/thousands dot/comma +// return v +//} +// +//func (sanitizer) sRecord(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// return v +//} +// +//func (sanitizer) sSelect(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// return v +//} +// +//func (sanitizer) sString(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// return v +//} +// +//func (sanitizer) sUrl(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// return v +//} +// +//func (sanitizer) sUser(v *types.RecordValue, f *types.ModuleField, m *types.Module) *types.RecordValue { +// return v +//} diff --git a/compose/service/values/shared.go b/compose/service/values/shared.go new file mode 100644 index 000000000..69eb44e46 --- /dev/null +++ b/compose/service/values/shared.go @@ -0,0 +1,37 @@ +package values + +import ( + "regexp" + "time" +) + +const ( + strBoolTrue = "1" + strBoolFalse = "0" + + datetimeInputFormatDate = "2006-01-02" + datetimeInputFormatTime = "15:04:05" + datetimeInputFormatFull = time.RFC3339 + + fieldOpt_Datetime_onlyDate = "onlyDate" + fieldOpt_Datetime_onlyTime = "onlyTime" + fieldOpt_Datetime_onlyFutureValues = "onlyFutureValues" + fieldOpt_Datetime_onlyPastValues = "onlyPastValues" + + fieldOpt_Number_precision = "precision" + + fieldOpt_Url_onlySecure = "onlySecure" +) + +var ( + // value resembles something that can be true + truthy = regexp.MustCompile(`^(t(rue)?|y(es)?|1)$`) + + // value resembles something that can be a reference + refy = regexp.MustCompile(`^[1-9](\d*)$`) +) + +func nowPtr() *time.Time { + now := time.Now() + return &now +} diff --git a/compose/service/values/validator.go b/compose/service/values/validator.go index 7d715673e..3fe49162c 100644 --- a/compose/service/values/validator.go +++ b/compose/service/values/validator.go @@ -1,8 +1,14 @@ package values import ( + "errors" "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/slice" + "math/big" + "net/mail" + "net/url" "strings" + "time" ) // Validator package provides tooling to validate @@ -13,45 +19,70 @@ import ( // is no need for such level of interaction and dynamic we require on the frontend type ( - UniqueChecker func(*types.Module, *types.ModuleField, *types.RecordValue) (uint64, error) - ReferenceChecker func(*types.Module, *types.ModuleField, *types.RecordValue) (bool, error) + UniqueChecker func(*types.RecordValue, *types.ModuleField, *types.Module) (uint64, error) + ReferenceChecker func(*types.RecordValue, *types.ModuleField, *types.Module) (bool, error) validator struct { uniqueCheckerFn UniqueChecker recordRefCheckerFn ReferenceChecker userRefCheckerFn ReferenceChecker + fileRefCheckerFn ReferenceChecker + + now func() time.Time } ) -func makeInternalErr(field string, err error) types.RecordValueError { - return types.RecordValueError{Kind: "internal", Message: err.Error(), Meta: map[string]interface{}{"field": field}} +func makeInternalErr(field *types.ModuleField, err error) types.RecordValueError { + return types.RecordValueError{Kind: "internal", Message: err.Error(), Meta: map[string]interface{}{"field": field.Name}} } -func makeEmptyErr(field string) types.RecordValueError { - return types.RecordValueError{Kind: "empty", Meta: map[string]interface{}{"field": field}} + +func makeEmptyErr(field *types.ModuleField) types.RecordValueError { + return types.RecordValueError{Kind: "empty", Meta: map[string]interface{}{"field": field.Name}} } -func makeDuplicateValueInSetErr(field string) types.RecordValueError { - return types.RecordValueError{Kind: "duplicateValueInSet", Meta: map[string]interface{}{"field": field}} + +func makeInvalidValueErr(field *types.ModuleField, value string) types.RecordValueError { + return types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": field.Name, "value": value}} } -func makeDuplicateValueErr(field string, recordID uint64) types.RecordValueError { - return types.RecordValueError{Kind: "duplicateValue", Meta: map[string]interface{}{"recordID": recordID, "field": field}} + +func makeInvalidRefErr(field *types.ModuleField, ref uint64) types.RecordValueError { + return types.RecordValueError{Kind: "invalidRef", Meta: map[string]interface{}{"field": field.Name, "ref": ref}} +} + +func makeDuplicateValueInSetErr(field *types.ModuleField, value string) types.RecordValueError { + return types.RecordValueError{Kind: "duplicateValueInSet", Meta: map[string]interface{}{"field": field.Name, "value": value}} +} + +func makeDuplicateValueErr(field *types.ModuleField, recordID uint64) types.RecordValueError { + return types.RecordValueError{Kind: "duplicateValue", Meta: map[string]interface{}{"field": field.Name, "recordID": recordID}} +} + +// Simple wrapper for easier error returning from validation functions +func e2s(ee ...types.RecordValueError) []types.RecordValueError { + return ee } func Validator() *validator { - return &validator{} + return &validator{ + now: func() time.Time { return time.Now() }, + } } -func (vldtr validator) UniqueChecker(fn UniqueChecker) { +func (vldtr *validator) UniqueChecker(fn UniqueChecker) { vldtr.uniqueCheckerFn = fn } -func (vldtr validator) RecordRefChecker(fn ReferenceChecker) { +func (vldtr *validator) RecordRefChecker(fn ReferenceChecker) { vldtr.recordRefCheckerFn = fn } -func (vldtr validator) UserRefChecker(fn ReferenceChecker) { +func (vldtr *validator) UserRefChecker(fn ReferenceChecker) { vldtr.userRefCheckerFn = fn } +func (vldtr *validator) FileRefChecker(fn ReferenceChecker) { + vldtr.fileRefCheckerFn = fn +} + // Run validates record and it's values against module & module fields options // // @@ -73,13 +104,13 @@ fields: if f.Required { if len(vv) == 0 { - out.Push(makeEmptyErr(f.Name)) + out.Push(makeEmptyErr(f)) continue fields } for _, v := range vv { if len(v.Value) == 0 || (f.IsRef() && v.Ref == 0) { - out.Push(makeEmptyErr(f.Name)) + out.Push(makeEmptyErr(f)) continue fields } } @@ -89,7 +120,7 @@ fields: flipped := make(map[string]bool) for _, v := range vv { if flipped[v.Value] { - out.Push(makeDuplicateValueInSetErr(f.Name)) + out.Push(makeDuplicateValueInSetErr(f, v.Value)) continue fields } @@ -99,32 +130,42 @@ fields: } for _, v := range r.Values { + if !v.IsUpdated() || v.IsUpdated() { + // We'll validate only updated (and non-deleted) values + continue + } + if f = m.Fields.FindByName(v.Name); f == nil { continue } + if v.Value == "" { + // Nothing to do with empty value + return nil + } + // Per field type validators switch strings.ToLower(f.Kind) { case "bool": - out.Push(vldtr.validateBoolFieldKind(v, f, r, m)...) + out.Push(vldtr.vBool(v, f, r, m)...) case "datetime": - out.Push(vldtr.validateDatetimeFieldKind(v, f, r, m)...) + out.Push(vldtr.vDatetime(v, f, r, m)...) case "email": - out.Push(vldtr.validateEmailFieldKind(v, f, r, m)...) + out.Push(vldtr.vEmail(v, f, r, m)...) case "file": - out.Push(vldtr.validateFileFieldKind(v, f, r, m)...) + out.Push(vldtr.vFile(v, f, r, m)...) case "number": - out.Push(vldtr.validateNumberFieldKind(v, f, r, m)...) + out.Push(vldtr.vNumber(v, f, r, m)...) case "record": - out.Push(vldtr.validateRecordFieldKind(v, f, r, m)...) + out.Push(vldtr.vRecord(v, f, r, m)...) case "select": - out.Push(vldtr.validateSelectFieldKind(v, f, r, m)...) - case "string": - out.Push(vldtr.validateStringFieldKind(v, f, r, m)...) + out.Push(vldtr.vSelect(v, f, r, m)...) + //case "string": + // out.Push(vldtr.vString(v, f, r, m)...) case "url": - out.Push(vldtr.validateUrlFieldKind(v, f, r, m)...) + out.Push(vldtr.vUrl(v, f, r, m)...) case "user": - out.Push(vldtr.validateUserFieldKind(v, f, r, m)...) + out.Push(vldtr.vUser(v, f, r, m)...) } } @@ -140,11 +181,11 @@ fields: continue } - duplicateRecordID, err := vldtr.uniqueCheckerFn(m, f, v) + duplicateRecordID, err := vldtr.uniqueCheckerFn(v, f, m) if err != nil { - out.Push(makeInternalErr(f.Name, err)) + out.Push(makeInternalErr(f, err)) } else if duplicateRecordID > 0 && duplicateRecordID != r.ID { - out.Push(makeDuplicateValueErr(f.Name, duplicateRecordID)) + out.Push(makeDuplicateValueErr(f, duplicateRecordID)) } } @@ -155,53 +196,151 @@ fields: return out } -func (vldtr validator) validateBoolFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo must be 1 or 0 +func (vldtr validator) vBool(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + if v.Value == "" { + return nil + } + + if v.Value != strBoolTrue && v.Value != strBoolFalse { + return e2s(makeInvalidValueErr(f, v.Value)) + } + return nil } -func (vldtr validator) validateDatetimeFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo must be datetime, UTC! - // @todo how do we check past/future (no info about previous value) +func (vldtr validator) vDatetime(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + var ( + inputFormat string + t time.Time + err error + + // We'll validate against this value + refTime = vldtr.now() + ) + + if f.Options.Bool(fieldOpt_Datetime_onlyDate) { + inputFormat = datetimeInputFormatDate + + // Round down ref time to midnight + refTime = time.Date(refTime.Year(), refTime.Month(), refTime.Day(), 0, 0, 0, 0, refTime.Location()) + } else if f.Options.Bool(fieldOpt_Datetime_onlyTime) { + inputFormat = datetimeInputFormatTime + + // Round down ref time to day one + refTime = time.Date(0, 1, 1, refTime.Hour(), refTime.Minute(), refTime.Second(), refTime.Nanosecond(), refTime.Location()) + } else { + inputFormat = datetimeInputFormatFull + } + + t, err = time.Parse(inputFormat, v.Value) + if err != nil { + return e2s(makeInvalidValueErr(f, v.Value)) + } + + if f.Options.Bool(fieldOpt_Datetime_onlyFutureValues) { + if !t.After(refTime) { + return e2s(makeInvalidValueErr(f, v.Value)) + } + } else if f.Options.Bool(fieldOpt_Datetime_onlyPastValues) { + if !t.Before(refTime) { + return e2s(makeInvalidValueErr(f, v.Value)) + } + } + + // @todo check past/future + return nil } -func (vldtr validator) validateEmailFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo must be an email +func (vldtr validator) vEmail(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + if _, err := mail.ParseAddress(v.Value); err != nil { + return e2s(makeInvalidValueErr(f, v.Value)) + } + return nil } -func (vldtr validator) validateFileFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo file is a Ref! - // @todo we can check for uniquenes (as we do for other refs) +func (vldtr validator) vFile(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + if ok, err := vldtr.fileRefCheckerFn(v, f, m); err != nil { + return e2s(makeInternalErr(f, err)) + } else if !ok { + return e2s(makeInvalidRefErr(f, v.Ref)) + } + return nil } -func (vldtr validator) validateNumberFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo make sure it is a number and cut decimals (precision) +func (vldtr validator) vNumber(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + var ( + precision = uint(f.Options.Int64Def(fieldOpt_Number_precision, 2)) + ) + + if _, _, err := big.ParseFloat(v.Value, 0, precision, big.ToNearestEven); err != nil { + return e2s(makeInvalidValueErr(f, v.Value)) + } + return nil } -func (vldtr validator) validateRecordFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo record is a ref +func (vldtr validator) vRecord(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + if ok, err := vldtr.recordRefCheckerFn(v, f, m); err != nil { + return e2s(makeInternalErr(f, err)) + } else if !ok { + return e2s(makeInvalidRefErr(f, v.Ref)) + } + return nil } -func (vldtr validator) validateSelectFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo validate v.Value against f.Options.Strings("options") +func (vldtr validator) vSelect(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + // @todo validate v.Value against + var ( + options = f.Options["options"] + sbm = make(map[string]bool) + ) + + if oo, ok := options.([]string); ok { + sbm = slice.ToStringBoolMap(oo) + } else if oo, ok := options.([]map[string]string); ok { + for _, o := range oo { + sbm[o["value"]] = true + } + } else if options == nil { + return nil + } else { + return e2s(makeInternalErr(f, errors.New("invalid select options definition"))) + } + + if !sbm[v.Value] { + return e2s(makeInvalidValueErr(f, v.Value)) + + } + return nil } -func (vldtr validator) validateStringFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { +//func (vldtr validator) vString(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { +// return nil +//} + +func (vldtr validator) vUrl(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + if p, err := url.Parse(v.Value); err != nil { + return e2s(makeInvalidValueErr(f, v.Value)) + } else if p.Scheme == "" || p.Host == "" { + return e2s(makeInvalidValueErr(f, v.Value)) + } else if f.Options.Bool(fieldOpt_Url_onlySecure) && p.Scheme != "https" { + return e2s(makeInvalidValueErr(f, v.Value)) + } + return nil } -func (vldtr validator) validateUrlFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo must be URL - return nil -} +func (vldtr validator) vUser(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { + if ok, err := vldtr.userRefCheckerFn(v, f, m); err != nil { + return e2s(makeInternalErr(f, err)) + } else if !ok { + return e2s(makeInvalidRefErr(f, v.Ref)) + } -func (vldtr validator) validateUserFieldKind(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError { - // @todo user is a ref return nil } diff --git a/compose/service/values/validator_test.go b/compose/service/values/validator_test.go new file mode 100644 index 000000000..b3c1bdae0 --- /dev/null +++ b/compose/service/values/validator_test.go @@ -0,0 +1,220 @@ +package values + +import ( + "github.com/cortezaproject/corteza-server/compose/types" + "reflect" + "testing" + "time" +) + +func Test_validator_vDatetime(t *testing.T) { + var ( + vldtr = validator{ + now: func() time.Time { + t, err := time.Parse(time.RFC3339, "2020-02-20T10:10:10Z") + if err != nil { + panic(err) + } + return t + }, + } + tests = []struct { + name string + val string + opt types.ModuleFieldOptions + want []types.RecordValueError + }{ + { + name: "unparsable", + val: "unparsable", + want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "unparsable"}}), + }, + { + name: "valid datetime value", + val: "2020-02-20T10:10:10Z", + }, + { + name: "valid date value", + val: "2020-02-20", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyDate: true}, + }, + { + name: "valid time value", + val: "10:10:10", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyTime: true}, + }, + { + name: "valid future datetime value", + val: "2021-02-20T10:10:10Z", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyFutureValues: true}, + }, + { + name: "valid future date value", + val: "2021-02-20", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyFutureValues: true, fieldOpt_Datetime_onlyDate: true}, + }, + { + name: "valid future time value", + val: "11:10:10", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyFutureValues: true, fieldOpt_Datetime_onlyTime: true}, + }, + { + name: "valid past datetime value", + val: "2019-02-20T10:10:10Z", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyPastValues: true}, + }, + { + name: "valid past date value", + val: "2019-02-20", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyPastValues: true, fieldOpt_Datetime_onlyDate: true}, + }, + { + name: "valid past time value", + val: "09:10:10", + opt: types.ModuleFieldOptions{fieldOpt_Datetime_onlyPastValues: true, fieldOpt_Datetime_onlyTime: true}, + }, + } + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := vldtr.vDatetime(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) { + t.Errorf("vDatetime() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func Test_validator_vNumber(t *testing.T) { + var ( + vldtr = validator{} + tests = []struct { + name string + val string + opt types.ModuleFieldOptions + want []types.RecordValueError + }{ + { + name: "unparsable", + val: "unparsable", + want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "unparsable"}}), + }, + { + name: "valid number value", + val: "42", + }, + { + name: "valid number value", + val: "42.123", + }, + { + name: "valid number value", + val: "412412412.322894325892365", + }, + } + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := vldtr.vNumber(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) { + t.Errorf("vNumber() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func Test_validator_vUrl(t *testing.T) { + var ( + vldtr = validator{} + tests = []struct { + name string + val string + opt types.ModuleFieldOptions + want []types.RecordValueError + }{ + { + name: "invalid-url", + val: "invalid-url", + want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "invalid-url"}}), + }, + { + name: "valid value", + val: "https://crust.tech/", + }, + } + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := vldtr.vUrl(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) { + t.Errorf("vUrl() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func Test_validator_vEmail(t *testing.T) { + var ( + vldtr = validator{} + tests = []struct { + name string + val string + opt types.ModuleFieldOptions + want []types.RecordValueError + }{ + { + name: "unparsable", + val: "un pars able", + want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "un pars able"}}), + }, + { + name: "valid value", + val: "qa@crust.tech", + }, + } + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := vldtr.vEmail(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) { + t.Errorf("vEmail() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func Test_validator_vSelect(t *testing.T) { + var ( + vldtr = validator{} + tests = []struct { + name string + val string + opt types.ModuleFieldOptions + want []types.RecordValueError + }{ + { + name: "unparsable", + val: "dummy", + }, + { + name: "valid value", + val: "crust", + opt: types.ModuleFieldOptions{"options": []string{"crust", "corteza"}}, + }, + { + name: "valid value", + val: "the rest", + opt: types.ModuleFieldOptions{"options": []string{"crust", "corteza"}}, + want: e2s(types.RecordValueError{Kind: "invalidValue", Meta: map[string]interface{}{"field": "", "value": "the rest"}}), + }, + } + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := vldtr.vSelect(&types.RecordValue{Value: tt.val}, &types.ModuleField{Options: tt.opt}, nil, nil); !reflect.DeepEqual(got, tt.want) { + t.Errorf("vSelect() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/compose/types/module_field_options.go b/compose/types/module_field_options.go index feacf0853..2d356b2fd 100644 --- a/compose/types/module_field_options.go +++ b/compose/types/module_field_options.go @@ -47,6 +47,20 @@ func (opt ModuleFieldOptions) Bool(key string) bool { return false } +func (opt ModuleFieldOptions) Int64(key string) int64 { + return opt.Int64Def(key, 0) +} + +func (opt ModuleFieldOptions) Int64Def(key string, def int64) int64 { + if _, has := opt[key]; has { + if n, ok := opt[key].(int64); ok { + return n + } + } + + return def +} + // Strings returns option value for key as slice of strings // // Invalid, non-existing are returned as nil