valley

package module
v0.0.0-...-d4025e8 Latest Latest
Warning

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

Go to latest
Published: Dec 23, 2019 License: MIT Imports: 7 Imported by: 0

README

Valley Workflow Badge Go Report Card Badge GoDoc Badge

Valley is tool for generating plain Go validation code based on your Go code.

Installation

You can install the latest version of Valley using the following command. Alternatively, you can use a tagged version at the end for a specific release:

$ GO111MODULE=on go get -v github.com/seeruk/valley/cmd/valley@latest

Usage

Valley reads Go source code, and generates validation code based upon it. Valley will look at a given file, pick out it's types and methods and identify types that appear to be configuring validation constraints. That can be any struct type defined in a file, as long as it has any method that returns nothing, and accepts a valley.Type as it's only argument:

package example

import (
    "github.com/seeruk/valley"
    "github.com/seeruk/valley/validation/constraints"
)

// Request ...
type Request struct {
    Inputs []string `valley:"inputs"`
    Page   int      `valley:"page"`
}

// Constraints ...
func (r Request) Constraints(t valley.Type) {
    t.Field(r.Inputs).
        Constraints(constraints.MaxLength(256)). // Applies to the whole []string
        Elements(constraints.MaxLength(16))      // Applies to each string in the []string
    t.Field(r.Page).
        Constraints(constraints.Min(1), constraints.Max(99))
}

See ./example/example.go for a more comprehensive example of usage.

Once you've prepared you Go file, execute Valley, passing the file path as an argument:

$ valley ./example.go

By default this will produce another file alongside the input file (in the above example that would ./example_validate.go). You can customise where the file is output using the -o or --output flag.

Output

If any validation constraints are violated, the generated Validate method will return those violations. They contain a path, the kind of thing they're referencing, a message, and some misc details that vary depending on which constraint was violated. For example:

[
  {
    "path": ".inputs.[0]",
    "path_kind": "element",
    "message": "a value is required"
  }
]

You may have noticed the struct tags on the example Request struct earlier. Those can be used to customise the output in the "path" key in the constraint violation. By default it will use the field name as it's written in the Go source code. You can choose to use existing tags (e.g. a json struct tag) by passing the -t or --tag flag with the name of the struct tag you'd like to use instead. The json struct tag is a very common use-case.

Extending

Currently the only option for extending Valley is to create a custom Valley binary. Don't worry though, this is really straightforward. The main function for Valley is a single line - and it's the only line you should need to use to create a custom binary.

os.Exit(cli.NewApplication(constraints.BuiltIn).Run(os.Args[1:], os.Environ()))

The only part that you need to change is the set of constraints you'd like to use. Valley uses it's exposed BuiltIn constraints which is a map. You can make a copy of this map and add your own, or create your own entirely new set of constraints.

The map's key is the fully qualified name of the constraint function, mapped to the constraint generator (i.e. the function that returns the generated code and any other information like imports and variables to place in the generate file).

Take a look at the BuiltIn constraints to see how they work. A straightforward one to look at is the Valid constraint.

Constraint generators are themselves constrained by the information that Valley is able to provide them. I hope that this information can be expanded upon in the future, but generally speaking this is all information from the source file that is read initially. Eventually I'd like to extend that to the package that file is in, and then further to any packages imported by that package, etc.

Built-In Constraints

The built-in constraints may be used in your code by importing:

import "github.com/seeruk/valley/validation/constraints"

(Note: You can alias the import, and Valley should still successfully generate your validation code)

GoDoc documentation is available for the built-in constraints that should help with understanding how the constraints may be used.

Here's a quick list of all of the built-in constraints (more documentation below):

  • AnyNRequired
  • DeepEquals
  • Equals
  • ExactlyNRequired
  • Length
  • Max
  • MaxLength
  • Min
  • MinLength
  • MutuallyExclusive
  • MutuallyInclusive
  • Nil
  • NotEquals
  • NotNil
  • OneOf
  • Predicate
  • Regexp
  • RegexpString
  • Required
  • TimeAfter
  • TimeBefore
  • TimeStringAfter
  • TimeStringBefore
  • Valid

AnyNRequired:

Applicable to: Structs

Description: At least n of the given fields must not be empty (uses the same logic as the Required constraint).

Usage:

t.Constraints(constraints.AnyNRequired(1, v.HomePhone, v.MobilePhone, v.WorkPhone))

DeepEquals

Applicable to: Fields

Description: Values must be deeply equal (i.e. reflect.DeepEqual)

Usage:

t.Field(e.String).Constraints(constraints.DeepEquals("hello"))
t.Field(e.Int).Constraints(constraints.DeepEquals(12))
t.Field(e.Int).Constraints(constraints.DeepEquals(len(e.FloatSlice)*2))
t.Field(e.FloatSlice).Elements(constraints.DeepEquals(math.Pi))

Equals

Applicable to: Fields

Description: Values must be equal.

Usage:

t.Field(e.String).Constraints(constraints.Equals("hello"))
t.Field(e.Int).Constraints(constraints.Equals(12))
t.Field(e.Int).Constraints(constraints.Equals(len(e.FloatSlice)*2))
t.Field(e.FloatSlice).Elements(constraints.Equals(math.Pi))

ExactlyNRequired

Applicable to: Structs

Description: Exactly n of the given fields must not be empty (uses the same logic as the Required constraint).

Usage:

t.Constraints(constraints.ExactlyNRequired(1, v.HomePhone, v.MobilePhone, v.WorkPhone))

Length

Applicable to: Fields

Description: Exactly length must be met.

Usage:

t.Field(e.SomeSlice).Constraints(constraints.Length(12))
t.Field(e.SomeString).Constraints(constraints.Length(8-(e.SomeInt-1)))
t.Field(e.SomeSomeMap).Constraints(constraints.Length(math.MaxInt64))

Max

Applicable to: Fields

Description: Maximum value must not be exceeded.

Usage:

t.Field(e.SomeInt).Constraints(constraints.Max(12))
t.Field(e.SomeFloat).Constraints(constraints.Max(8-(e.SomeInt-1)))

MaxLength

Applicable to: Fields

Description: Maximum length must not be exceeded.

Usage:

t.Field(e.SomeSlice).Constraints(constraints.MaxLength(12))
t.Field(e.SomeString).Constraints(constraints.MaxLength(8-(e.SomeInt-1)))
t.Field(e.SomeSomeMap).Constraints(constraints.MaxLength(math.MaxInt64))

Min

Applicable to: Fields

Description: Minimum value must be met.

Usage:

t.Field(e.SomeInt).Constraints(constraints.Min(12))
t.Field(e.SomeFloat).Constraints(constraints.Min(8-(e.SomeInt-1)))

MinLength

Applicable to: Fields

Description: Minimum length must be met.

Usage:

t.Field(e.SomeSlice).Constraints(constraints.MinLength(12))
t.Field(e.SomeString).Constraints(constraints.MinLength(8-(e.SomeInt-1)))
t.Field(e.SomeSomeMap).Constraints(constraints.MinLength(math.MaxInt8))

MutuallyExclusive

Applicable to: Structs

Description: Only one of the given fields must be set.

Usage:

t.Constraints(constraints.MutuallyExclusive(e.Username, e.EmailAddress))

MutuallyInclusive

Applicable to: Structs

Description: If any one of the given fields is set, then all of the given fields must be set.

Usage:

t.Constraints(constraints.MutuallyInclusive(e.ReceiveMarketing, e.EmailAddress))

Nil

Applicable to: Fields

Description: Value must be nil.

Usage:

t.Field(e.SomePtr).Constraints(constraints.Nil())
t.Field(e.SomeSlice).Constraints(constraints.Nil())
t.Field(e.SomeInterface).Constraints(constraints.Nil())

NotEquals

Applicable to: Fields

Description: Values must not be equal.

Usage:

t.Field(e.SomeInt).Constraints(constraints.Equals(12))
t.Field(e.SomeInt).Constraints(constraints.Equals(e.SomeOtherInt*23))
t.Field(e.SomeInt).Constraints(constraints.Equals(int(math.Max(e.SomeOtherInt, 23))))

NotNil

Applicable to: Fields

Description: Value must not be nil.

Usage:

t.Field(e.SomePtr).Constraints(constraints.NotNil())
t.Field(e.SomeSlice).Constraints(constraints.NotNil())
t.Field(e.SomeInterface).Constraints(constraints.NotNil())

One Of

Applicable to: Fields

Description: Value must be one of the given allowed values.

Usage:

t.Field(e.SomeString).Constraints(constraints.OneOf("Hello, World!", "Hello, GitHub!"))

Predicate

Applicable to: Fields

Description: Pass a custom predicate that will be rendered as a violation, returning a given message as the description of any violation.

Usage:

t.Field(e.String).Constraints(constraints.Predicate(
    strings.HasPrefix(e.String, "custom") && len(e.String) == 32,
    "value must be a valid custom ID",
))

Regexp

Applicable to: Fields

Description: Value must match the given reference to a compiled *regexp.Regexp instance.

Usage:

t.Field(e.String).Constraints(constraints.Regexp(valley.PatternUUID))

RegexpString

Applicable to: Fields

Description: Value must match the given regular expression string. The regular expression string will be used to create a package-local variable with a unique name that will compile when imported.

Usage:

t.Field(e.String).Constraints(constraints.RegexpString("^Example$"))

Required

Applicable to: Fields

Description: Value is required, behaves like (and sometimes uses) reflect.Value.IsZero().

Usage:

t.Field(e.Nested).Constraints(constraints.Required())

TimeAfter

Applicable to: Fields

Description: Value must be after the given time. The value may either be be an existing time.Time value, or you can pass in an expression using something like time.Date.

Usage:

t.Field(e.Time).Constraints(constraints.TimeAfter(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeAfter(timeYosemite))

TimeBefore

Applicable to: Fields

Description: Value must be before the given time. The value may either be be an existing time.Time value, or you can pass in an expression using something like time.Date.

Usage:

t.Field(e.Time).Constraints(constraints.TimeBefore(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeBefore(timeYosemite))

TimeStringAfter

Applicable to: Fields

Description: Value must be after the given time string. The value can be a string, or a reference to a string.

Usage:

t.Field(e.Time).Constraints(constraints.TimeAfter(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeAfter(timeYosemite))

TimeStringBefore

Applicable to: Fields

Description: Value must be before the given time string. The value can be a string, or a reference to a string.

Usage:

t.Field(e.Time).Constraints(constraints.TimeBefore(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeBefore(timeYosemite))

Valid

Applicable to: Fields

Description: Calls Validate() on the value, used to validate nested structures.

Usage:

t.Field(e.Nested).Constraints(constraints.Valid())
t.Field(e.NestedSlice).Elements(constraints.Valid())

Motivation

Previously I've implemented validation in Go using reflection, and while reflection isn't actually as slow as you might expect it does come with other issues. By generating validation code instead of resorting to reflection you regain the protection that the Go compiler gives you. Even if the output of Valley is wrong (or you misconfigure the constraints) your application would fail to compile, alerting you to the issue.

On the topic of performance, the code generated by Valley is still a lot faster than reflection. This is for several reasons. One is that I've tried to be quite efficient doing things like building up a path to fields (i.e. reusing memory where possible, and not adding to the path unless a constraint violation occurs, or there's no choice not to). Another is that without using reflection, the checks just become simple if statements and loops - these checks are extremely fast.

Another issue I found with reflection-based approaches is that you have to pass in references to fields to validate as strings (i.e. the name of the field), rather than the fields themselves. This is because you can't retrieve a field name as far as I can tell from a value passed in using reflection. The configuration for Valley needs to be able to compile as Go code. If it's mis-used, Valley will do it's best to tell you what's wrong, and where. References to fields should exist, and your existing tooling, and Go toolchain will tell you if they don't - as well as Valley. On top of that, the generated code also has to compile, further protecting you from runtime panics.

TODO

  • Assess output of all constraints. Many constraints should probably be optional. Are they? Constraints that probably should only apply if a value is set:
    • DeepEquals
    • Equals
    • Length
    • Max
    • MaxLength
    • Min
    • MinLength
    • NotEquals
    • OneOf
    • Regexp
    • RegexpString
    • TimeAfter
    • TimeBefore
    • TimeStringAfter
    • TimeStringBefore
  • Add some benchmarks to the README, preferably against something open source using reflection.
  • The ability to define constraints in a separate file (in the same package, i.e. read the whole package and generate code for the one file based on the context provided by the whole package).
    • Maybe also the ability to define constraints in a function instead of on a method. Maybe also in a function in a separate package... More complex CLI usage there though.
  • Better resolution of underlying types. Right now if a type is imported from any other file or package than the one we're generating code for we can't tell what type it really is (e.g. is it a struct, slice, map, int really?). If we could figure out those underlying types, the tool would be a little more flexible. In particular, Elements and Keys currently only work on plain collection types because that's the only way we can figure out the key / value type to pass to constraint generators.
  • The ability to attach multiple constraints methods to a type, that generate different validate functions (the Valid constraint would need an option to override which method is called).

License

MIT

Contributions

Feel free to open a pull request, or file an issue on GitHub. I always welcome contributions as long as they're for the benefit of all (potential) users of this project.

If you're unsure about anything, feel free to ask about it in an issue before you get your heart set on fixing it yourself.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var InitialPathSize = 32

InitialPathSize sets the default size of a new Path's internal buffer.

View Source
var (
	PatternUUID = regexp.MustCompile(`^[0-9A-f]{8}-[0-9A-f]{4}-[0-9A-f]{4}-[0-9A-f]{4}-[0-9A-f]{12}$`)
)

Built in regular expression patterns.

Functions

func GetFieldAliasFromTag

func GetFieldAliasFromTag(name, tagName, tag string) (string, error)

GetFieldAliasFromTag ...

func TimeMustParse

func TimeMustParse(t time.Time, err error) time.Time

TimeMustParse ...

Types

type Config

type Config struct {
	Types map[string]TypeConfig `json:"types"`
}

Config represents the configuration for generating an entire set of validation code.

type Constraint

type Constraint struct{}

Constraint is used to identify constraints to generate code for in a Go AST.

type ConstraintConfig

type ConstraintConfig struct {
	Predicate ast.Expr
	Name      string     `json:"name"`
	Opts      []ast.Expr `json:"opts"`
	Pos       token.Pos
}

ConstraintConfig represents the configuration passed to a ConstraintGenerator to generate some code. It's used throughout the configuration structure.

type ConstraintGenerator

type ConstraintGenerator func(value Context, fieldType ast.Expr, opts []ast.Expr) (ConstraintGeneratorOutput, error)

ConstraintGenerator is a function that can generate constraint code.

type ConstraintGeneratorOutput

type ConstraintGeneratorOutput struct {
	Imports []Import
	Vars    []Variable
	Code    string
}

ConstraintGeneratorOutput represents the information needed to write some code segments to a new Go file. They can't be written to whilst we're generating code because each constraint could need code to be in different parts of the resulting file (e.g. imports).

type ConstraintViolation

type ConstraintViolation struct {
	Path     string                 `json:"path,omitempty"`
	PathKind string                 `json:"path_kind"`
	Message  string                 `json:"message"`
	Details  map[string]interface{} `json:"details,omitempty"`
}

ConstraintViolation is the result of a validation failure.

type Context

type Context struct {
	Source     Source
	TypeName   string
	Receiver   string
	FieldName  string
	FieldAlias string
	TagName    string
	VarName    string
	Path       string
	PathKind   PathKind

	Constraint      string
	ConstraintNum   int
	BeforeViolation string
	AfterViolation  string
}

Context is used to inform a ConstraintGenerator about it's environment, mainly to do with which part of a type is being validated, and giving important identifiers to ConstraintGenerators.

func (Context) Clone

func (c Context) Clone() Context

Clone returns a clone of this Context by utilising the properties of Go values.

type Field

type Field struct{}

Field represents the options for adding constraints to fields on a type.

func (Field) Constraints

func (f Field) Constraints(_ ...Constraint) Field

Constraints accepts some constraints to generate code for, for a specific field.

func (Field) Elements

func (f Field) Elements(_ ...Constraint) Field

Elements accepts some constraints to generate code for, on the elements of a specific field.

func (Field) Keys

func (f Field) Keys(_ ...Constraint) Field

Keys accepts some constraints to generate code for, on the keys of a specific field.

type FieldConfig

type FieldConfig struct {
	Constraints []ConstraintConfig `json:"constraints"`
	Elements    []ConstraintConfig `json:"elements"`
	Keys        []ConstraintConfig `json:"keys"`
}

FieldConfig represents the configuration needed to generate validation code for a specific field on a specific type.

type Fields

type Fields map[string]Value

Fields is a map from struct field name to Value.

type Import

type Import struct {
	Path  string
	Alias string
}

Import represents information about a Go import that Valley uses to generate code.

type Method

type Method struct {
	Receiver string
	Name     string
	Params   *ast.FieldList
	Results  *ast.FieldList
	Body     *ast.BlockStmt
}

Method represents the information we need about a method in some Go source code.

type Methods

type Methods map[string][]Method

Methods is a map from struct name to Method.

type Module

type Module struct {
	Path string
	Dir  string
}

Module represents the information Valley needs about Go Modules used in the current project.

type Package

type Package struct {
	Name string
}

Package represents the information Valley needs about a Go package.

type Path

type Path struct {
	// contains filtered or unexported fields
}

Path is used to represent the current position in a structure, to output a useful field value to identify where a ConstraintViolation occurred.

func NewPath

func NewPath() *Path

NewPath returns a new Path instance.

func (*Path) String

func (r *Path) String() string

String renders this path as a string, to be sent to the frontend.

func (*Path) TruncateRight

func (r *Path) TruncateRight(n int)

TruncateRight cuts n bytes off of the end of the buffer. The backing array for the buffer does not shrink, meaning we can re-use that memory if we need to.

func (*Path) Write

func (r *Path) Write(in string) int

Write appends the given string to the end of the internal buffer.

type PathKind

type PathKind string

PathKind enumerates possible path kinds that apply to constraint violations.

const (
	PathKindStruct  PathKind = "struct"
	PathKindField   PathKind = "field"
	PathKindElement PathKind = "element"
	PathKindKey     PathKind = "key"
)

All possible PathKind values.

type Source

type Source struct {
	FileName    string
	FileSet     *token.FileSet
	Package     string
	Imports     []Import
	Methods     Methods
	Structs     Structs
	StructNames []string
}

Source represents the information Valley needs about a particular source file.

type Struct

type Struct struct {
	Name       string
	Node       *ast.StructType
	Fields     Fields
	FieldNames []string
}

Struct represents the information we need about a struct in some Go source code.

type Structs

type Structs map[string]Struct

Structs is a map from struct name to Struct.

type Type

type Type struct{}

Type is the "fake" interface used to configure Valley for a Go type. It's methods are known by the config building process.

func (Type) Constraints

func (t Type) Constraints(_ ...Constraint) Type

Constraints accepts some constraints to generate code for.

func (Type) Field

func (t Type) Field(_ interface{}) Field

Field accepts a field to generate constraints for.

func (Type) When

func (t Type) When(_ bool) Type

When accepts a predicate which will be used to wrap generated code to conditionally apply constraints.

type TypeConfig

type TypeConfig struct {
	Constraints []ConstraintConfig     `json:"constraints"`
	Fields      map[string]FieldConfig `json:"fields"`
}

TypeConfig represents the configuration needed to generate validation code for a specific type.

type Value

type Value struct {
	Name string
	Type ast.Expr
	Tag  string
}

Value represents the information we need about a value (e.g. a struct, or a field on a struct) in some Go source code.

type Variable

type Variable struct {
	Name  string
	Value string
}

Variable represents information about a Go variable that Valley uses to generate code.

Directories

Path Synopsis
cmd
Code generated by valley.
Code generated by valley.

Jump to

Keyboard shortcuts

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