core.go (112 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package lookslike
import (
"reflect"
"sort"
"strings"
"github.com/elastic/go-lookslike/isdef"
"github.com/elastic/go-lookslike/llpath"
"github.com/elastic/go-lookslike/llresult"
"github.com/elastic/go-lookslike/validator"
)
// Compose combines multiple SchemaValidators into a single one.
func Compose(validators ...validator.Validator) validator.Validator {
return func(actual interface{}) *llresult.Results {
res := make([]*llresult.Results, len(validators))
for idx, validator := range validators {
res[idx] = validator(actual)
}
combined := llresult.NewResults()
for _, r := range res {
r.EachResult(func(path llpath.Path, vr llresult.ValueResult) bool {
combined.Record(path, vr)
return true
})
}
return combined
}
}
// Strict is used when you want any unspecified keys that are encountered to be considered errors.
func Strict(laxValidator validator.Validator) validator.Validator {
return func(actual interface{}) *llresult.Results {
res := laxValidator(actual)
// When validating nil objects the lax validator is by definition sufficient
if actual == nil {
return res
}
// The inner workings of this are a little weird
// We use a hash of dotted paths to track the res
// We can Check if a key had a test associated with it by looking up the laxValidator
// result data
// What's trickier is intermediate maps, maps don't usually have explicit tests, they usually just have
// their properties tested.
// This method counts an intermediate map as tested if a subkey is tested.
// Since the datastructure we have to search is a flattened hashmap of the original map we take that hashmap
// and turn it into a sorted string array, then do a binary prefix search to determine if a subkey was tested.
// It's a little weird, but is fairly efficient. We could stop using the flattened map as a datastructure, but
// that would add complexity elsewhere. Probably a good refactor at some point, but not worth it now.
validatedPaths := []string{}
for k := range res.Fields {
validatedPaths = append(validatedPaths, k)
}
sort.Strings(validatedPaths)
walk(reflect.ValueOf(actual), false, func(woi walkObserverInfo) error {
_, validatedExactly := res.Fields[woi.path.String()]
if validatedExactly {
return nil // This key was tested, passes strict test
}
// Search returns the point just before an actual match (since we ruled out an exact match with the cheaper
// hash Check above. We have to validate the actual match with a prefix Check as well
matchIdx := sort.SearchStrings(validatedPaths, woi.path.String())
if matchIdx < len(validatedPaths) && strings.HasPrefix(validatedPaths[matchIdx], woi.path.String()) {
return nil
}
res.Merge(llresult.StrictFailureResult(woi.path))
return nil
})
return res
}
}
func compile(in interface{}) (validator.Validator, error) {
switch in.(type) {
case isdef.IsDef:
return compileIsDef(in.(isdef.IsDef))
case nil:
// nil can't be handled by the default case of IsEqual
return compileIsDef(isdef.IsNil)
default:
inVal := reflect.ValueOf(in)
switch inVal.Kind() {
case reflect.Map:
return compileMap(inVal)
case reflect.Slice, reflect.Array:
return compileSlice(inVal)
default:
return compileIsDef(isdef.IsEqual(in))
}
}
}
func compileMap(inVal reflect.Value) (validator validator.Validator, err error) {
wo, compiled := setupWalkObserver()
err = walkMap(inVal, true, wo)
return func(actual interface{}) *llresult.Results {
return compiled.Check(actual)
}, err
}
func compileSlice(inVal reflect.Value) (validator validator.Validator, err error) {
wo, compiled := setupWalkObserver()
err = walkSlice(inVal, true, wo)
// Slices are always strict in validation because
// it would be surprising to only validate the first specified values
return Strict(func(actual interface{}) *llresult.Results {
return compiled.Check(actual)
}), err
}
func compileIsDef(def isdef.IsDef) (validator validator.Validator, err error) {
return func(actual interface{}) *llresult.Results {
return def.Check(llpath.Path{}, actual, true)
}, nil
}
func setupWalkObserver() (walkObserver, *CompiledSchema) {
compiled := make(CompiledSchema, 0)
return func(current walkObserverInfo) error {
kind := current.value.Kind()
isCollection := kind == reflect.Map || kind == reflect.Slice
isEmptyCollection := isCollection && current.value.Len() == 0
// We do comparisons on all leaf nodes. If the leaf is an empty collection
// we do a comparison to let us test empty structures.
if !isCollection || isEmptyCollection {
isDef, isIsDef := current.value.Interface().(isdef.IsDef)
if !isIsDef {
isDef = isdef.IsEqual(current.value.Interface())
}
compiled = append(compiled, flatValidator{current.path, isDef})
}
return nil
}, &compiled
}
// MustCompile compiles the given validation, panic-ing if that map is invalid.
func MustCompile(in interface{}) validator.Validator {
compiled, err := compile(in)
if err != nil {
panic(err)
}
return compiled
}