Skip to content

faceair/jio

Repository files navigation

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