Code Monkey home page Code Monkey logo

jio's Introduction

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

jio's People

Contributors

faceair avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

jio's Issues

Slice of errors for validation

It might be good have multiple errors for fields. Right now validation return error message only about one field, not all failed.

`ObjectSchema` keys are unconditionally added to the JSON object validated

package main

import (
	"bytes"
	"log"

	"github.com/faceair/jio"
)

func main() {
	orig := []byte(`{}`)
	data := []byte(`{}`)
	schema := jio.Object().Keys(jio.K{
		"foo": jio.String(),
	})

	if _, err := jio.ValidateJSON(&data, schema); err != nil {
		log.Fatal(err)
	}

	if !bytes.Equal(orig, data) {
		log.Printf("Want %s, got %s.", orig, data)
	}
}

prints

Want {}, got {"foo":null}.

Keys() needs to set skip to false after iteration.

Using the README example with a small change (add optional "foo") will result in the Without() being ignored. This is because AnySchema.Optional() calls ctx.Skip():

package main

import (
    "fmt"
    "log"

    "github.com/faceair/jio"
)

func jioNotAllowed(ctx *jio.Context) {
    ctx.Abort(fmt.Errorf("Attribute `%s` not allowed in this context.", ctx.FieldPath()))
    return
}

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(),
            "foo":   jio.Any(),
        }).Without("name", "title").Required(),
    }))
    if err != nil {
        panic(err)
    }
    log.Printf("%s", data)
}

This results in no errors, even though we should expect "panic: field window contains title"

If you remove the "foo" line, it works correctly:

panic: field `window` contains title

The problem is in the ObjectSchema.Keys() - it needs to set ctx.skip to false after the for loop.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.