Add value validators & sanitizers for all field kinds
This commit is contained in:
@@ -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
|
||||
//
|
||||
|
||||
@@ -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" <email> 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
|
||||
//}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user