schema

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2019 License: GPL-3.0 Imports: 6 Imported by: 2

README

schema GoDoc GoReport

schema makes it easier to check if map/array structures match a certain schema. Great for testing JSON API's or validating the format of incoming requests and providing error messages to the api user. Also see trial for simple test assertions.

The initial version was built by gfrey and jgroenveld. It was inspired by chancancode/json_expressions

Example

example_test.go

func TestJSON(t *testing.T) {
    reader := getJSONResponse()

    err := schema.MatchJSON(
        schema.Map{
            "id":       schema.IsInteger,
            "name":     "Max Mustermann",
            "age":      42,
            "height":   schema.IsFloat,
            "footsize": schema.IsPresent,
            "address": schema.Map{
                "street": schema.IsString,
                "zip":    schema.IsString,
            },
            "tags": schema.ArrayIncluding("red"),
        },
        reader,
    )

    if err != nil {
        t.Fatal(err)
    }
}

JSON Input

{
    "id": 12,
    "name": "Hans Meier",
    "age": 42,
    "height": 1.91,
    "address": {
        "street": 12
    },
    "tags": ["blue", "green"]
}

err.Error() Output

"address": Missing keys: "zip"
"address.street": is no string but float64
"name": "Hans Meier" != "Max Mustermann"
"tags": red:string(0) not included
Missing keys: "footsize"

Entry Points

schema.Match(schema.Matcher, interface{}) error
schema.MatchJSON(schema.Matcher, io.Reader) error

Matchers

"ConcreteValue"
    Any concrete value like: "Name", 12, true, false, nil

IsPresent
    Is the value given (empty string counts as given).
    This is essentially a wildcard in map values or array elements.

Types
    - IsString
    - IsInt
    - IsFloat
    - IsBool
    - IsTime(format)

Map{"key":Matcher, ...}
    Matches maps where all given keys and values have to match. 
    No extra or missing keys allowed.

MapIncluding{"key":Matcher, ...}
    Matches maps but only checks the given keys and values and ignores extra ones.

Array(Matcher...)
    Matches all array elements in order.

ArrayUnordered(Matcher...)
    Matches all array elements but order is ignored.

ArrayIncluding(Matcher...)
    Reports elements that can not be matched.

ArrayEach(Matcher)
    Each element of the array has to match the given matcher.
    
Capture(name)
    Can be used once or more to capture values and to make sure a value stays the same 
    if it occurs multiple times in a schema. See [capture_test.go](capture_test.go).
    Can also be used to use the captured value in future operations (e.g. multiple requests with the same id).
    
StringEnum(...values)

How to write matchers

To use custom or more specialized matchers, the schema.Matcher interface needs to be implemented. Either via struct or by using schema.MatcherFunc

To report errors, schema.SelfError(message) needs to be used if the data itself is the problem.

schema.Error.Add(field, message) if a subelement of the data is the problem (see Map and Array).

var IsTime = schema.MatcherFunc("IsTime",
    func(data interface{}) *schema.Error {
        s, ok := data.(string)
        if !ok {
            return schema.SelfError("is no valid time: not a string")
        }

        _, err := time.Parse(time.RFC3339, s)
        if err != nil {
            return schema.SelfError("is no valid time: " + err.Error())
        }
        return nil
    },
)

To be more generic with regard to the time format the following pattern can be used:

func IsTime (format string) schema.Matcher
	return schema.MatcherFunc("IsTime",
		func(data interface{}) *schema.Error {
			s, ok := data.(string)
			if !ok {
				return schema.SelfError("is no valid time: not a string")
			}

			_, err := time.Parse(format, s)
			if err != nil {
				return schema.SelfError("is no valid time: " + err.Error())
			}
			return nil
		},
	)
}

Ideas

  • write Optional(Matcher) that matches if key is missing or given matcher is satisfied
  • write Combine(...Matcher) that matches if all given matchers are satisfied

Issues

Numbers

JSON does not differ between integers and floats, ie. there are only numbers. This is why the go JSON library will always return a float64 value if no type was specified (unmarshalling into an interface{} type). This requires some magic internally and can result in false positives. For very large or small integers errors could occur due to rounding errors. If something has no fractional value it is assumed to be equal to an integer (42.0 == 42).

Array Matchers

For arrays there are matcher variants for including and unordered. They take the following steps:

  • Order the given matchers, where concrete values are matched first, then all matchers except the most generic IsPresent, and finally all IsPresent matchers. This order guarantees that the most specific values are matched first.
  • For each of the ordered matchers, verify one of the remaining values matches.
  • Keep a log of all matched values.

This will work in most of the cases, but might fail for some weird nested structures where something like a backtracking approach would be required.

Documentation

Overview

Package schema makes it easier to check if map/array structures match a certain schema. Great for testing JSON API's.

Example:

func TestJSON(t *testing.T) {
    reader := getJSONResponse()

    err := schema.MatchJSON(
        schema.Map{
            "id":       schema.IsInteger,
            "name":     "Max Mustermann",
            "age":      42,
            "height":   schema.IsFloat,
            "footsize": schema.IsPresent,
            "address":  schema.Map{
                "street": schema.IsString,
                "zip":    schema.IsString,
            },
            "tags": schema.ArrayIncluding("red"),
        },
        reader,
    )

    if err != nil {
        t.Fatal(err)
    }
}

JSON Input

{
    "id": 12,
    "name": "Hans Meier",
    "age": 42,
    "height": 1.91,
    "address": {
        "street": 12
    },
    "tags": ["blue", "green"]
}

err.Error() Output

"address": Missing keys: "zip"
"address.street": is no string but float64
"name": "Hans Meier" != "Max Mustermann"
"tags": red:string(0) not included
Missing keys: "footsize"

See https://github.com/jgroeneveld/schema for more examples.

Also see https://github.com/jgroeneveld/trial for lightweight assertions.

Index

Constants

This section is empty.

Variables

View Source
var IsBool = MatcherFunc("IsBool", isBool)

IsBool checks if a value is a bool

View Source
var IsFloat = MatcherFunc("IsFloat", isFloat)

IsFloat checks if a value is a float

View Source
var IsInteger = MatcherFunc("IsInteger", isInteger)

IsInteger checks if a value is an integer

View Source
var IsPresent = MatcherFunc("IsPresent", isPresent)

IsPresent is a placeholder for Map to check if something "just exists"

View Source
var IsString = MatcherFunc("IsString", isString)

IsString checks if a value is an string

Functions

func Match

func Match(m Matcher, data interface{}) error

Match wraps matcher.Match for nil error handling.

func MatchJSON

func MatchJSON(m Matcher, r io.Reader) error

MatchJSON wraps Match with a reader for JSON raw data.

Types

type CaptureMatcher

type CaptureMatcher interface {
	Matcher
	Equals(interface{}) bool
	CapturedValue() interface{}
}

CaptureMatcher is the exposed interface for Capture

func Capture

func Capture(name string) CaptureMatcher

Capture can be used once or more to capture values and to make sure a value stays the same

type Error

type Error struct {
	Errors map[string]string
}

Error is used to record errors that happen during a schema check

func SelfError

func SelfError(msg string) *Error

SelfError is an error without any field it describes

func (*Error) Add

func (e *Error) Add(field, message string)

Add adds an error

func (*Error) Any

func (e *Error) Any() bool

Any checks if there are any errors

func (*Error) Error

func (e *Error) Error() string

func (*Error) Merge

func (e *Error) Merge(otherField string, other *Error)

Merge merges another error into this error to have a error tree.

type Map

type Map map[string]interface{}

Map checks each key for a specific value or matcher.

func (Map) Match

func (m Map) Match(data interface{}) *Error

Match is the actual matching function to obey the Matcher Interface

type MapIncluding

type MapIncluding map[string]interface{}

MapIncluding checks if a map contains keys that match the given values or matchers

func (MapIncluding) Match

func (m MapIncluding) Match(data interface{}) *Error

Match is the actual matching function to obey the Matcher Interface

type Matcher

type Matcher interface {
	Match(data interface{}) *Error
}

Matcher is the interface for all matchers

func Array

func Array(exps ...interface{}) Matcher

Array compares all values of the array one by one. Either specific or other Matchers.

func ArrayEach

func ArrayEach(exp interface{}) Matcher

ArrayEach checks all values of an array against the given value or Matcher

func ArrayIncluding

func ArrayIncluding(exps ...interface{}) Matcher

ArrayIncluding checks that all given Matchers or values are present in the array.

func ArrayUnordered

func ArrayUnordered(exps ...interface{}) Matcher

ArrayUnordered checks that all values or matchers are satisfied, ignoring the order of the array.

func IsTime

func IsTime(format string) Matcher

IsTime checks if its a parsable time format.

func StringEnum

func StringEnum(values ...string) Matcher

StringEnum checks if a value is included in the given Enumeration

type MatcherFuncImpl

type MatcherFuncImpl struct {
	Name string
	Fun  func(data interface{}) *Error
}

MatcherFuncImpl is a wrapper for a function to obey the Matcher interface

func MatcherFunc

func MatcherFunc(name string, fun func(data interface{}) *Error) *MatcherFuncImpl

MatcherFunc turns a func into a Matcher

func (*MatcherFuncImpl) Match

func (f *MatcherFuncImpl) Match(data interface{}) *Error

Match is the actual matching function

func (*MatcherFuncImpl) String

func (f *MatcherFuncImpl) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL