README

jio

jio

Make validation simple and efficient !

Travis branch Coverage Status Go Report Card License GoDoc License

中文文档

Why use jio?

Parameter validation in Golang is really a cursing problem. Defining tags on structs is not easy to extend rules, handwritten validation code makes logic code cumbersome, and the initial zero value of the struct field will also interfere with the validation.

jio tries validate json raw data before deserialization to avoid these problems. Defining validation rules as Schema is easy to read and easy to extend (Inspired by Hapi.js joi library). Rules within Schema can be validated in the order of registration, and context can be used to exchange data between rules, and can access other field data even within a single rule, etc.

jio provides a flexible enough way to make your validation simple and efficient!

How to use?

Validate json string

package main

import (
    "log"

    "github.com/faceair/jio"
)

func main() {
    data := []byte(`{
        "debug": "on",
        "window": {
            "title": "Sample Widget",
            "size": [500, 500]
        }
    }`)
    _, err := jio.ValidateJSON(&data, jio.Object().Keys(jio.K{
        "debug": jio.Bool().Truthy("on").Required(),
        "window": jio.Object().Keys(jio.K{
            "title": jio.String().Min(3).Max(18),
            "size":  jio.Array().Items(jio.Number().Integer()).Length(2).Required(),
        }).Without("name", "title").Required(),
    }))
    if err != nil {
        panic(err)
    }
    log.Printf("%s", data) // {"debug":true,"window":{"size":[500,500],"title":"Sample Widget"}}
}

The above schema defines the following constraints:

  • debug
    • not empty, must be a boolean value when validation end
    • allow on string instead of true
  • window
    • not empty, object
    • not allowed for both name and title
    • The following elements exist
      • title
        • string, can be empty
        • length is between 3 and 18 when not empty
      • size
        • array, not empty
        • there are two child elements of the integer type

Using middleware to validate request body

Take chi as an example, the other frameworks are similar.

package main

import (
    "io/ioutil"
    "net/http"

    "github.com/faceair/jio"
    "github.com/go-chi/chi"
)

func main() {
    r := chi.NewRouter()
    r.Route("/people", func(r chi.Router) {
        r.With(jio.ValidateBody(jio.Object().Keys(jio.K{
            "name":  jio.String().Min(3).Max(10).Required(),
            "age":   jio.Number().Integer().Min(0).Max(100).Required(),
            "phone": jio.String().Regex(`^1[34578]\d{9}$`).Required(),
        }), jio.DefaultErrorHandler)).Post("/", func(w http.ResponseWriter, r *http.Request) {
            body, err := ioutil.ReadAll(r.Body)
            if err != nil {
                panic(err)
            }
            w.Header().Set("Content-Type", "application/json; charset=utf-8")
            w.WriteHeader(http.StatusOK)
            w.Write(body)
        })
    })
    http.ListenAndServe(":8080", r)
}

The second parameter of jio.ValidateBody is called for error handling when the validation fails.

Validate the query parameter with middleware

package main

import (
    "encoding/json"
    "net/http"

    "github.com/faceair/jio"
    "github.com/go-chi/chi"
)

func main() {
    r := chi.NewRouter()
    r.Route("/people", func(r chi.Router) {
        r.With(jio.ValidateQuery(jio.Object().Keys(jio.K{
            "keyword":  jio.String(),
            "is_adult": jio.Bool().Truthy("true", "yes").Falsy("false", "no"),
            "starts_with": jio.Number().ParseString().Integer(),
        }), jio.DefaultErrorHandler)).Get("/", func(w http.ResponseWriter, r *http.Request) {
            query := r.Context().Value(jio.ContextKeyQuery).(map[string]interface{})
            body, err := json.Marshal(query)
            if err != nil {
                panic(err)
            }
            w.Header().Set("Content-Type", "application/json; charset=utf-8")
            w.WriteHeader(http.StatusOK)
            w.Write(body)
        })
    })
    http.ListenAndServe(":8080", r)
}

Note that the original value of the query parameter is string, you may need to convert the value type first (for example, jio.Number().ParseString() or jio.Bool().Truthy(values)).

API Documentation

https://godoc.org/github.com/faceair/jio

Advanced usage

Workflow

Each Schema is made up of a series of rules, for example:

jio.String().Min(5).Max(10).Alphanum().Lowercase()

In this example, String Schema has 4 rules, which are Min(5) Max(10) Alphanum() Lowercase(), will also validate in order Min(5) Max(10) Alphanum() Lowercase(). If a rule validation fails, the Schema's validation stops and throws an error.

In order to improve the readability of the code, these three built-in rules will validate first.

  • Required()
  • Optional()
  • Default(value)

For example:

jio.String().Min(5).Max(10).Alphanum().Lowercase().Required()

The actual validation order will be Required() Min(5) Max(10) Alphanum() Lowercase().

After validate all the rules, finally we check if the basic type of the data is the type of Schema. If not, the Schema will throw an error.

Validator Context

Data transfer in the workflow depends on context, the structure is like this:

Type Context struct {
    Value interface{} // Raw data, you can also reassign to change the result
}
func (ctx *Context) Ref(refPath string) (value interface{}, ok bool) { // Reference other field data
}
func (ctx *Context) Abort(err error) { // Terminate the validation and throw an error
  ...
}
func (ctx *Context) Skip() { // Skip subsequent rules
  ...
}

Let's try to customize a validation rule. Add a rule to use the Transform method:

jio.String().Transform(func(ctx *jio.Context) {
    If ctx.Value != "faceair" {
        ctx.Abort(errors.New("you are not faceair"))
    }
})

The custom rule we added means throwing a you are not faceair error when the original data is not equal to faceair.

In fact, the built-in validation rules work in a similar way. For example, the core code of Optional() is:

If ctx.Value == nil {
  ctx.Skip()
}

You can also reassign ctx.Value to change the original data. For example, the built-in Lowercase() converts the original string to lowercase. The core code is:

ctx.Value = strings.ToLower(ctx.Value)

References and Priority

In most cases, the rules only use the data of the current field, but sometimes it needs to work with other fields. For example:

{
    "type": "ip", // enumeration value, `ip` or `domain`
    "value": "8.8.8.8"
}

The validation rules of this value is determined by the value of type and can be written as

jio.Object().Keys(jio.K{
        "type": jio.String().Valid("ip", "domain").SetPriority(1).Default("ip"),
        "value": jio.String().
            When("type", "ip", jio.String().Regex(`^\d+\.\d+\.\d+\.\d+$`)).
            When("type", "domain", jio.String().Regex(`^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0 -9]\.[a-zA-Z]{2,}$`)).Required(),
})

The When function can reference other field data, and if it is successful, apply the new validation rule to the current data.

In addition, you may notice that there is a SetPriority method in the rules of type. If the input data is:

{
    "value": "8.8.8.8"
}

When the priority is not set, the validation rule of value may be executed first. At this time, the value of the reference type will be null, and the validation will fail. Because there are validation rules that refer to each other, there may be a validation sequence requirement. When we want a field under the same Object to be validated first, we can set it to a larger priority value (default value 0).

If you want to reference data from other fields in your custom rules, you can use the Ref method on the context. If the referenced data is a nested object, the path to the referenced field needs to be concatenated with . . For example, if you want to reference name under people object then the reference path is people.name:

{
    "type": "people",
    "people": {
        "name": "faceair"
    }
}

License

MIT

Expand ▾ Collapse ▴

Documentation

Index

Constants

View Source
const (
	// ContextKeyQuery save query map to context with this key
	ContextKeyQuery contextKey = iota
	// ContextKeyBody save body map to context with this key
	ContextKeyBody
)

Variables

This section is empty.

Functions

func DefaultErrorHandler

func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error)

DefaultErrorHandler handle and respond the error

func ValidateBody

func ValidateBody(schema Schema, errorHandler func(http.ResponseWriter, *http.Request, error)) func(next http.Handler) http.Handler

ValidateBody validate the request's body using the schema. If the verification fails, the errorHandler will be used to handle the error.

func ValidateJSON

func ValidateJSON(dataRaw *[]byte, schema Schema) (dataMap map[string]interface{}, err error)

ValidateJSON validate the provided json bytes using the schema.

func ValidateQuery

func ValidateQuery(schema Schema, errorHandler func(http.ResponseWriter, *http.Request, error)) func(next http.Handler) http.Handler

ValidateQuery validate the request's query using the schema.

Types

type AnySchema

type AnySchema struct {

	// contains filtered or unexported fields

}

AnySchema match any data type

func Any

func Any() *AnySchema

Any Generates a schema object that matches any data type

func (*AnySchema) Default

func (a *AnySchema) Default(value interface{}) *AnySchema

Default set a default value if the original value is undefined or null.

func (*AnySchema) Equal

func (a *AnySchema) Equal(value interface{}) *AnySchema

Equal check the provided value is equal to the value of the key.

func (*AnySchema) Optional

func (a *AnySchema) Optional() *AnySchema

Optional mark a key as optional which will allow undefined or null as values. When the value of the key is undefined or null, the following check rule will be skip. Used to annotate the schema for readability as all keys are optional by default.

func (*AnySchema) PrependTransform

func (a *AnySchema) PrependTransform(f func(*Context)) *AnySchema

PrependTransform run your transform function before othor rules.

func (*AnySchema) Priority

func (b *AnySchema) Priority() int

func (*AnySchema) Required

func (a *AnySchema) Required() *AnySchema

Required mark a key as required which will not allow undefined or null as value. All keys are optional by default.

func (*AnySchema) Set

func (a *AnySchema) Set(value interface{}) *AnySchema

Set just set a value for the key and don't care the origin value.

func (*AnySchema) SetPriority

func (a *AnySchema) SetPriority(priority int) *AnySchema

SetPriority set priority to the schema. A schema with a higher priority under the same object will be validate first.

func (*AnySchema) Transform

func (a *AnySchema) Transform(f func(*Context)) *AnySchema

Transform append your transform function to rules.

func (*AnySchema) Valid

func (a *AnySchema) Valid(values ...interface{}) *AnySchema

Valid add the provided values into the allowed whitelist and mark them as the only valid values allowed.

func (*AnySchema) Validate

func (a *AnySchema) Validate(ctx *Context)

Validate a value using the schema

func (*AnySchema) When

func (a *AnySchema) When(refPath string, condition interface{}, then Schema) *AnySchema

When add a conditional schema based on another key value The reference path support use `.` access object property, just like javascript. The condition can be a Schema or value. If condition is a schema, then this condition Schema will be used to verify the reference value. If condition is value, then check the condition is equal to the reference value. When the condition is true, the then schema will be applied to the current key value. Otherwise, nothing will be done.

type ArraySchema

type ArraySchema struct {

	// contains filtered or unexported fields

}

ArraySchema match array data type

func Array

func Array() *ArraySchema

Array Generates a schema object that matches array data type

func (*ArraySchema) Check

func (a *ArraySchema) Check(f func(interface{}) error) *ArraySchema

Check use the provided function to validate the value of the key. Throws an error when the value is not a slice.

func (*ArraySchema) Default

func (a *ArraySchema) Default(value interface{}) *ArraySchema

Default same as AnySchema.Default

func (*ArraySchema) Items

func (a *ArraySchema) Items(schemas ...Schema) *ArraySchema

Items check if this value can pass the validation of any schema.

func (*ArraySchema) Length

func (a *ArraySchema) Length(length int) *ArraySchema

Length check if the length of this slice is equal to the provided length.

func (*ArraySchema) Max

func (a *ArraySchema) Max(max int) *ArraySchema

Max check if the length of this slice is less than or equal to the provided length.

func (*ArraySchema) Min

func (a *ArraySchema) Min(min int) *ArraySchema

Min check if the length of this slice is greater than or equal to the provided length.

func (*ArraySchema) Optional

func (a *ArraySchema) Optional() *ArraySchema

Optional same as AnySchema.Optional

func (*ArraySchema) PrependTransform

func (a *ArraySchema) PrependTransform(f func(*Context)) *ArraySchema

PrependTransform same as AnySchema.PrependTransform

func (*ArraySchema) Priority

func (b *ArraySchema) Priority() int

func (*ArraySchema) Required

func (a *ArraySchema) Required() *ArraySchema

Required same as AnySchema.Required

func (*ArraySchema) SetPriority

func (a *ArraySchema) SetPriority(priority int) *ArraySchema

SetPriority same as AnySchema.SetPriority

func (*ArraySchema) Transform

func (a *ArraySchema) Transform(f func(*Context)) *ArraySchema

Transform same as AnySchema.Transform

func (*ArraySchema) Validate

func (a *ArraySchema) Validate(ctx *Context)

Validate same as AnySchema.Validate

func (*ArraySchema) When

func (a *ArraySchema) When(refPath string, condition interface{}, then Schema) *ArraySchema

When same as AnySchema.When

type BoolSchema

type BoolSchema struct {

	// contains filtered or unexported fields

}

BoolSchema match bool data type

func Bool

func Bool() *BoolSchema

Bool Generates a schema object that matches bool data type

func (*BoolSchema) Default

func (b *BoolSchema) Default(value bool) *BoolSchema

Default same as AnySchema.Default

func (*BoolSchema) Equal

func (b *BoolSchema) Equal(value bool) *BoolSchema

Equal same as AnySchema.Equal

func (*BoolSchema) Falsy

func (b *BoolSchema) Falsy(values ...interface{}) *BoolSchema

Falsy allow for additional values to be considered valid booleans by converting them to false during validation.

func (*BoolSchema) Optional

func (b *BoolSchema) Optional() *BoolSchema

Optional same as AnySchema.Optional

func (*BoolSchema) PrependTransform

func (b *BoolSchema) PrependTransform(f func(*Context)) *BoolSchema

PrependTransform same as AnySchema.PrependTransform

func (*BoolSchema) Priority

func (b *BoolSchema) Priority() int

func (*BoolSchema) Required

func (b *BoolSchema) Required() *BoolSchema

Required same as AnySchema.Required

func (*BoolSchema) Set

func (b *BoolSchema) Set(value bool) *BoolSchema

Set same as AnySchema.Set

func (*BoolSchema) SetPriority

func (b *BoolSchema) SetPriority(priority int) *BoolSchema

SetPriority same as AnySchema.SetPriority

func (*BoolSchema) Transform

func (b *BoolSchema) Transform(f func(*Context)) *BoolSchema

Transform same as AnySchema.Transform

func (*BoolSchema) Truthy

func (b *BoolSchema) Truthy(values ...interface{}) *BoolSchema

Truthy allow for additional values to be considered valid booleans by converting them to true during validation.

func (*BoolSchema) Validate

func (b *BoolSchema) Validate(ctx *Context)

Validate same as AnySchema.Validate

func (*BoolSchema) When

func (b *BoolSchema) When(refPath string, condition interface{}, then Schema) *BoolSchema

When same as AnySchema.When

type Context

type Context struct {
	Value interface{}
	Err   error

	// contains filtered or unexported fields

}

Context contains data and toolkit

func NewContext

func NewContext(data interface{}) *Context

NewContext Generates a context object with the provided data.

func (*Context) Abort

func (ctx *Context) Abort(err error)

Abort throw an error and skip the following check rules.

func (*Context) AssertKind

func (ctx *Context) AssertKind(kind reflect.Kind) bool

AssertKind assert the value type and cache.

func (*Context) FieldPath

func (ctx *Context) FieldPath() string

FieldPath the field path of the current value.

func (*Context) Get

func (ctx *Context) Get(name string) (interface{}, bool)

Get returns the value for the given key, ie: (value, true).

func (*Context) Ref

func (ctx *Context) Ref(refPath string) (value interface{}, ok bool)

Ref return the reference value. The reference path support use `.` access object property, just like javascript.

func (*Context) Set

func (ctx *Context) Set(name string, value interface{})

Set is used to store a new key/value pair exclusively for this context.

func (*Context) Skip

func (ctx *Context) Skip()

Skip the following check rules.

type K

type K map[string]Schema

K object keys schema alias

type NumberSchema

type NumberSchema struct {

	// contains filtered or unexported fields

}

NumberSchema match number data type

func Number

func Number() *NumberSchema

Number Generates a schema object that matches number data type

func (*NumberSchema) Ceil

func (n *NumberSchema) Ceil() *NumberSchema

Ceil convert the value to the least integer value greater than or equal to the value.

func (*NumberSchema) Check

func (n *NumberSchema) Check(f func(float64) error) *NumberSchema

Check use the provided function to validate the value of the key. Throws an error when the value is not float64.

func (*NumberSchema) Convert

func (n *NumberSchema) Convert(f func(float64) float64) *NumberSchema

Convert use the provided function to convert the value of the key. Throws an error when the value is not float64.

func (*NumberSchema) Default

func (n *NumberSchema) Default(value float64) *NumberSchema

Default same as AnySchema.Default

func (*NumberSchema) Equal

func (n *NumberSchema) Equal(value float64) *NumberSchema

Equal same as AnySchema.Equal

func (*NumberSchema) Floor

func (n *NumberSchema) Floor() *NumberSchema

Floor convert the value to the greatest integer value less than or equal to the value.

func (*NumberSchema) Integer

func (n *NumberSchema) Integer() *NumberSchema

Integer check if the value is integer.

func (*NumberSchema) Max

func (n *NumberSchema) Max(max float64) *NumberSchema

Max check if the value is less than or equal to the provided value.

func (*NumberSchema) Min

func (n *NumberSchema) Min(min float64) *NumberSchema

Min check if the value is greater than or equal to the provided value.

func (*NumberSchema) Optional

func (n *NumberSchema) Optional() *NumberSchema

Optional same as AnySchema.Optional

func (*NumberSchema) ParseString

func (n *NumberSchema) ParseString() *NumberSchema

ParseString convert the string value to float64. Validation will be skipped when this value is not string. But if this value is not a valid number, an error will be thrown.

func (*NumberSchema) PrependTransform

func (n *NumberSchema) PrependTransform(f func(*Context)) *NumberSchema

PrependTransform same as AnySchema.PrependTransform

func (*NumberSchema) Priority

func (b *NumberSchema) Priority() int

func (*NumberSchema) Required

func (n *NumberSchema) Required() *NumberSchema

Required same as AnySchema.Required

func (*NumberSchema) Round

func (n *NumberSchema) Round() *NumberSchema

Round convert the value to the nearest integer, rounding half away from zero.

func (*NumberSchema) Set

func (n *NumberSchema) Set(value float64) *NumberSchema

Set same as AnySchema.Set

func (*NumberSchema) SetPriority

func (n *NumberSchema) SetPriority(priority int) *NumberSchema

SetPriority same as AnySchema.SetPriority

func (*NumberSchema) Transform

func (n *NumberSchema) Transform(f func(*Context)) *NumberSchema

Transform same as AnySchema.Transform

func (*NumberSchema) Valid

func (n *NumberSchema) Valid(values ...float64) *NumberSchema

Valid same as AnySchema.Valid

func (*NumberSchema) Validate

func (n *NumberSchema) Validate(ctx *Context)

Validate same as AnySchema.Validate

func (*NumberSchema) When

func (n *NumberSchema) When(refPath string, condition interface{}, then Schema) *NumberSchema

When same as AnySchema.When

type ObjectSchema

type ObjectSchema struct {

	// contains filtered or unexported fields

}

ObjectSchema match object data type

func Object

func Object() *ObjectSchema

Object Generates a schema object that matches object data type

func (*ObjectSchema) Default

func (o *ObjectSchema) Default(value map[string]interface{}) *ObjectSchema

Default same as AnySchema.Default

func (*ObjectSchema) Keys

func (o *ObjectSchema) Keys(children K) *ObjectSchema

Keys set the object keys's schema

func (*ObjectSchema) Optional

func (o *ObjectSchema) Optional() *ObjectSchema

Optional same as AnySchema.Optional

func (*ObjectSchema) PrependTransform

func (o *ObjectSchema) PrependTransform(f func(*Context)) *ObjectSchema

PrependTransform same as AnySchema.PrependTransform

func (*ObjectSchema) Priority

func (b *ObjectSchema) Priority() int

func (*ObjectSchema) Required

func (o *ObjectSchema) Required() *ObjectSchema

Required same as AnySchema.Required

func (*ObjectSchema) SetPriority

func (o *ObjectSchema) SetPriority(priority int) *ObjectSchema

SetPriority same as AnySchema.SetPriority

func (*ObjectSchema) Transform

func (o *ObjectSchema) Transform(f func(*Context)) *ObjectSchema

Transform same as AnySchema.Transform

func (*ObjectSchema) Validate

func (o *ObjectSchema) Validate(ctx *Context)

Validate same as AnySchema.Validate

func (*ObjectSchema) When

func (o *ObjectSchema) When(refPath string, condition interface{}, then Schema) *ObjectSchema

When same as AnySchema.When

func (*ObjectSchema) With

func (o *ObjectSchema) With(keys ...string) *ObjectSchema

With require the presence of these keys.

func (*ObjectSchema) Without

func (o *ObjectSchema) Without(keys ...string) *ObjectSchema

Without forbids the presence of these keys.

type Schema

type Schema interface {
	Priority() int
	Validate(*Context)
}

Schema interface

type StringSchema

type StringSchema struct {

	// contains filtered or unexported fields

}

StringSchema match string data type

func String

func String() *StringSchema

String Generates a schema object that matches string data type

func (*StringSchema) Alphanum

func (s *StringSchema) Alphanum() *StringSchema

Alphanum check if the string value to only contain a-z, A-Z, and 0-9

func (*StringSchema) Check

func (s *StringSchema) Check(f func(string) error) *StringSchema

Check use the provided function to validate the value of the key. Throws an error when the value is not string.

func (*StringSchema) Convert

func (s *StringSchema) Convert(f func(string) string) *StringSchema

Convert use the provided function to convert the value of the key. Throws an error when the value is not string.

func (*StringSchema) Default

func (s *StringSchema) Default(value string) *StringSchema

Default same as AnySchema.Default

func (*StringSchema) Equal

func (s *StringSchema) Equal(value string) *StringSchema

Equal same as AnySchema.Equal

func (*StringSchema) Length

func (s *StringSchema) Length(length int) *StringSchema

Length check if the length of this string is equal to the provided length.

func (*StringSchema) Lowercase

func (s *StringSchema) Lowercase() *StringSchema

Lowercase convert the string value to lowercase.

func (*StringSchema) Max

func (s *StringSchema) Max(max int) *StringSchema

Max check if the length of this string is less than or equal to the provided length.

func (*StringSchema) Min

func (s *StringSchema) Min(min int) *StringSchema

Min check if the length of this string is greater than or equal to the provided length.

func (*StringSchema) Optional

func (s *StringSchema) Optional() *StringSchema

Optional same as AnySchema.Optional

func (*StringSchema) PrependTransform

func (s *StringSchema) PrependTransform(f func(*Context)) *StringSchema

PrependTransform same as AnySchema.PrependTransform

func (*StringSchema) Priority

func (b *StringSchema) Priority() int

func (*StringSchema) Regex

func (s *StringSchema) Regex(regex string) *StringSchema

Regex check if the value is matched the regex.

func (*StringSchema) Required

func (s *StringSchema) Required() *StringSchema

Required same as AnySchema.Required

func (*StringSchema) Set

func (s *StringSchema) Set(value string) *StringSchema

Set same as AnySchema.Set

func (*StringSchema) SetPriority

func (s *StringSchema) SetPriority(priority int) *StringSchema

SetPriority same as AnySchema.SetPriority

func (*StringSchema) Token

func (s *StringSchema) Token() *StringSchema

Token check if the string value to only contain a-z, A-Z, 0-9, and underscore _

func (*StringSchema) Transform

func (s *StringSchema) Transform(f func(*Context)) *StringSchema

Transform same as AnySchema.Transform

func (*StringSchema) Trim

func (s *StringSchema) Trim() *StringSchema

Trim emoves whitespace from both sides of the string value.

func (*StringSchema) Uppercase

func (s *StringSchema) Uppercase() *StringSchema

Uppercase convert the string value to uppercase.

func (*StringSchema) Valid

func (s *StringSchema) Valid(values ...string) *StringSchema

Valid same as AnySchema.Valid

func (*StringSchema) Validate

func (s *StringSchema) Validate(ctx *Context)

Validate same as AnySchema.Validate

func (*StringSchema) When

func (s *StringSchema) When(refPath string, condition interface{}, then Schema) *StringSchema

When same as AnySchema.When