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

Add mapval doc.go #7944

Merged
merged 1 commit into from
Aug 21, 2018
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
4 changes: 2 additions & 2 deletions libbeat/common/mapval/compiled_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package mapval
import "github.com/elastic/beats/libbeat/common"

type flatValidator struct {
path Path
path path
isDef IsDef
}

Expand All @@ -31,7 +31,7 @@ type CompiledSchema []flatValidator
func (cs CompiledSchema) Check(actual common.MapStr) *Results {
results := NewResults()
for _, pv := range cs {
actualV, actualKeyExists := pv.path.GetFrom(actual)
actualV, actualKeyExists := pv.path.getFrom(actual)

if !pv.isDef.optional || pv.isDef.optional && actualKeyExists {
var checkRes *Results
Expand Down
8 changes: 5 additions & 3 deletions libbeat/common/mapval/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ func Optional(id IsDef) IsDef {
return id
}

// Map is the type used to define schema definitions for Compile.
// Map is the type used to define schema definitions for Compile and to represent an arbitrary
// map of values of any type.
type Map map[string]interface{}

// Slice is a convenience []interface{} used to declare schema defs.
// Slice is a convenience []interface{} used to declare schema defs. You would typically nest this inside
// a Map as a value, and it would be able to match against any type of non-empty slice.
type Slice []interface{}

// Validator is the result of Compile and is run against the map you'd like to test.
Expand All @@ -56,7 +58,7 @@ func Compose(validators ...Validator) Validator {

combined := NewResults()
for _, r := range results {
r.EachResult(func(path Path, vr ValueResult) bool {
r.EachResult(func(path path, vr ValueResult) bool {
combined.record(path, vr)
return true
})
Expand Down
23 changes: 23 additions & 0 deletions libbeat/common/mapval/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 mapval is used to validate JSON-like nested map data-structure against a set of expectations. Its key features are allowing custom, function defined validators for values, and allowing the composition of multiple validation specs.

See the example below for more details. Most key functions include detailed examples of their use within this documentation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the doc_test Example()` function will be show in the godocs? Nice, didn't know.

*/
package mapval
206 changes: 206 additions & 0 deletions libbeat/common/mapval/doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// 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 mapval

import (
"fmt"
"strings"

"github.com/elastic/beats/libbeat/common"
)

func Example() {
// Let's say we want to validate this map
data := common.MapStr{"foo": "bar", "baz": "bot", "count": 1}

// We can validate the data by creating a mapval.Validator
// Validators are functions created by compiling the special mapval.Map
// type. This is a map[string]interface{} that can be compiled
// into a series of checks.
//
// Literal values in a mapval.Map are checked for equality.
// More complex checks can be done using values of the mapval.IsDef
// type. In this case, we're using an IsDef to see if the "foo" key
// contains the string "a", and we're using a literal to check that the
// "baz" key contains the exact value "bot".
validator := MustCompile(Map{
"foo": IsStringContaining("a"),
"baz": "bot",
})

// When being used in test-suites, you should use mapvaltest.Test to execute the validator
// This produces easy to read test output, and outputs one failed assertion per failed matcher
// See the docs for mapvaltest for more info
// mapvaltest.Test(t, validator, data)

// If you need more control than mapvaltest.Test provides, you can use the results directly
results := validator(data)

// The Results.Valid property indicates if the validator passed
fmt.Printf("Results.Valid: %t\n", results.Valid)

// Results.Errors() returns one error per failed match
fmt.Printf("There were %d errors\n", len(results.Errors()))

// Results.Fields is a map of paths defined in the input mapval.Map to the result of their validation
// This is useful if you need more control
fmt.Printf("Over %d fields\n", len(results.Fields))

// You may be thinking that the validation above should have failed since there was an
// extra key, 'count', defined that was encountered. By default mapval does not
// consider extra data to be an error. To change that behavior, wrap the validator
// in mapval.Strict()
strictResults := Strict(validator)(data)

fmt.Printf("Strict Results.Valid: %t\n", strictResults.Valid)

// You can check an exact field for an error
fmt.Printf("For the count field specifically .Valid is: %t\n", strictResults.Fields["count"][0].Valid)

// And get error objects for each error
for _, err := range strictResults.Errors() {
fmt.Println(err)
}

// And even get a new Results object with only invalid fields included
strictResults.DetailedErrors()
}

func ExampleCompose() {
// Composition is useful when you need to share common validation logic between validators.
// Let's imagine that we want to validate maps describing pets.

pets := []common.MapStr{
{"name": "rover", "barks": "often", "fur_length": "long"},
{"name": "lucky", "barks": "rarely", "fur_length": "short"},
{"name": "pounce", "meows": "often", "fur_length": "short"},
{"name": "peanut", "meows": "rarely", "fur_length": "long"},
}

// We can see that all pets have the "fur_length" property, but that only cats meow, and dogs bark.
// We can concisely encode this in mapval using mapval.Compose.
// We can also see that both "meows" and "barks" contain the same enums of values.
// We'll start by creating a composed IsDef using the IsAny composition, which creates a new IsDef that is
// a logical 'or' of its IsDef arguments

isFrequency := IsAny(IsEqual("often"), IsEqual("rarely"))

petValidator := MustCompile(Map{
"name": IsNonEmptyString,
"fur_length": IsAny(IsEqual("long"), IsEqual("short")),
})
dogValidator := Compose(
petValidator,
MustCompile(Map{"barks": isFrequency}),
)
catValidator := Compose(
petValidator,
MustCompile(Map{"meows": isFrequency}),
)

for _, pet := range pets {
var petType string
if dogValidator(pet).Valid {
petType = "dog"
} else if catValidator(pet).Valid {
petType = "cat"
}
fmt.Printf("%s is a %s\n", pet["name"], petType)
}

// Output:
// rover is a dog
// lucky is a dog
// pounce is a cat
// peanut is a cat
}

func ExampleOptional() {
dataNoError := common.MapStr{"foo": "bar"}
dataError := common.MapStr{"foo": "bar", "error": true}

validator := MustCompile(Map{"foo": "bar", "error": Optional(IsEqual(true))})

// Both inputs pass
fmt.Printf("Validator classifies both maps as true: %t", validator(dataNoError).Valid && validator(dataError).Valid)

// Output:
// Validator classifies both maps as true: true
}

func ExampleIs() {
// More advanced validations can be used with built-in and custom functions.
// These are represented with the IfDef type

data := common.MapStr{"foo": "bar", "count": 1}

// Values can also be tested programatically if a mapval.IsDef is used as a value
// Here we'll define a custom IsDef using the mapval DSL, then validate it.
// The Is() function is the preferred way to costruct IsDef objects.
startsWithB := Is("starts with b", func(path path, v interface{}) *Results {
vStr, ok := v.(string)
if !ok {
return SimpleResult(path, false, "Expected a string, got a %t", v)
}

if strings.HasPrefix(vStr, "b") {
return ValidResult(path)
}

return SimpleResult(path, false, "Expected string to start with b, got %v", vStr)
})

funcValidator := MustCompile(Map{"foo": startsWithB})

funcValidatorResult := funcValidator(data)

fmt.Printf("Valid: %t", funcValidatorResult.Valid)

// Output:
// Valid: true
}

func ExampleMap() {
v := MustCompile(Map{
"foo": IsStringContaining("a"),
"baz": "bot",
})

data := common.MapStr{
"foo": "bar",
"baz": "bot",
}

fmt.Printf("Result is %t", v(data).Valid)

// Output:
// Result is true
}

func ExampleSlice() {
v := MustCompile(Map{
"foo": Slice{"foo", IsNonEmptyString},
})

data := common.MapStr{"foo": []string{"foo", "something"}}

fmt.Printf("Result is %t", v(data).Valid)

// Output:
// Result is true
}
Loading