isdef/dsl.go (93 lines of code) (raw):
package isdef
import (
"fmt"
"reflect"
"github.com/elastic/go-lookslike/internal/llreflect"
"github.com/elastic/go-lookslike/llpath"
"github.com/elastic/go-lookslike/llresult"
"github.com/elastic/go-lookslike/validator"
)
// Is creates a named IsDef with the given Checker.
func Is(name string, checker ValueValidator) IsDef {
return IsDef{Name: name, Checker: checker}
}
// A ValueValidator is used to validate a value in an interface{}.
type ValueValidator func(path llpath.Path, v interface{}) *llresult.Results
// An IsDef defines the type of Check to do.
// Generally only Name and Checker are set. Optional and CheckKeyMissing are
// needed for weird checks like key presence.
type IsDef struct {
Name string
Checker ValueValidator
Optional bool
CheckKeyMissing bool
}
// Check runs the IsDef at the given value at the given path
func (id IsDef) Check(path llpath.Path, v interface{}, keyExists bool) *llresult.Results {
if id.CheckKeyMissing {
if !keyExists {
return llresult.ValidResult(path)
}
return llresult.SimpleResult(path, false, "this key should not exist")
}
if !id.Optional && !keyExists {
return llresult.KeyMissingResult(path)
}
if id.Checker != nil {
return id.Checker(path, v)
}
return llresult.ValidResult(path)
}
// Optional wraps an IsDef to mark the field's presence as Optional.
func Optional(id IsDef) IsDef {
id.Name = "Optional " + id.Name
id.Optional = true
return id
}
// IsSliceOf validates that the array at the given key is an array of objects all validatable
// via the given validator.Validator.
func IsSliceOf(validator validator.Validator) IsDef {
return Is("slice", func(path llpath.Path, v interface{}) *llresult.Results {
if reflect.TypeOf(v).Kind() != reflect.Slice {
return llresult.SimpleResult(path, false, "Expected slice at given path")
}
vSlice := llreflect.InterfaceToSliceOfInterfaces(v)
res := llresult.NewResults()
for idx, curV := range vSlice {
var validatorRes *llresult.Results
validatorRes = validator(curV)
res.MergeUnderPrefix(path.ExtendSlice(idx), validatorRes)
}
return res
})
}
// IsAny takes a variable number of IsDef's and combines them with a logical OR. If any single definition
// matches the key will be marked as valid.
func IsAny(of ...IsDef) IsDef {
names := make([]string, len(of))
for i, def := range of {
names[i] = def.Name
}
isName := fmt.Sprintf("either %#v", names)
return Is(isName, func(path llpath.Path, v interface{}) *llresult.Results {
for _, def := range of {
vr := def.Check(path, v, true)
if vr.Valid {
return vr
}
}
return llresult.SimpleResult(
path,
false,
fmt.Sprintf("Value was none of %#v, actual value was %#v", names, v),
)
})
}
// IsUnique instances are used in multiple spots, flagging a value as being in error if it's seen across invocations.
// To use it, assign IsUnique to a variable, then use that variable multiple times in a map[string]interface{}.
func IsUnique() IsDef {
return ScopedIsUnique().IsUniqueTo("")
}
// UniqScopeTracker is represents the tracking data for invoking IsUniqueTo.
type UniqScopeTracker map[interface{}]string
// IsUniqueTo validates that the given value is only ever seen within a single namespace.
func (ust UniqScopeTracker) IsUniqueTo(namespace string) IsDef {
return Is("unique", func(path llpath.Path, v interface{}) *llresult.Results {
for trackerK, trackerNs := range ust {
hasNamespace := len(namespace) > 0
if reflect.DeepEqual(trackerK, v) && (!hasNamespace || namespace != trackerNs) {
return llresult.SimpleResult(path, false, "Value '%v' is repeated", v)
}
}
ust[v] = namespace
return llresult.ValidResult(path)
})
}
// ScopedIsUnique returns a new scope for uniqueness checks.
func ScopedIsUnique() UniqScopeTracker {
return UniqScopeTracker{}
}