zvalidate

package module
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Mar 16, 2020 License: MIT Imports: 14 Imported by: 6

README

Build Status codecov GoDoc

Simple validation for Go. Some things that make it different from the (many) other libraries:

  • No struct tags – which I don't find a good tool for this – just functions.
  • Validations return parsed values.
  • Easy to display validation errors in UI.
  • Doesn't use reflection (other than type assertions); mostly typed.
  • No external dependencies.
  • Easy to add nested validations.
  • Not tied to HTTP (useful for validating CLI flags, for example).

I originally wrote this at my previous employer (github.com/teamwork/validate), this is an improved (and incompatible) version.

Basic example:

email := "martin@arp42.net"

v := zvalidate.New()
v.Required("email", email)
m := v.Email("email", email)

if v.HasErrors() {
    fmt.Printf("Had the following validation errors:\n%s", v)
}

fmt.Printf("parsed email: %s\n", m.Address)

All validators are just method calls on the Validator struct, and follow the same patterns:

  • The input's zero type (empty string, 0, nil, etc.) is valid. Use the Required() validator if you want to make a parameter required).

  • key string, value [..] are the first two arguments, where key is the parameter name (to display in the error or next to a form) and value is what we want validated (type of value depends on validation).

  • Optionally accept a custom message as the last parameter.

The error text only includes a simple human description such as "must be set" or "must be a valid email". When adding new validations, make sure that they can be displayed properly when joined with commas. A text such as "Error: this field must be higher than 42" would look weird:

must be set, Error: this field must be higher than 42

Validations

List of validations with abbreviated function signature (key string, value [..] omitted):

Function Description
Required() Value must not be the type's zero value
Exclude([]string) Value is not in the exclude list
Include([]string) Value is in the include list
Range(min, max int) Minimum and maximum int value.
Len(min, max int) int Character length of string
Integer() int64 Integer value
Boolean() bool Boolean value
Domain() []string Domain name; returns list of domain labels.
URL() *url.URL Valid URL
Email() mail.Address Email address
IPv4() net.IP IPv4 address
IP() net.IP IPv4 or IPv6 address
HexColor() (uint8, uint8, uint8) Colour as hex triplet (#123456 or #123)
Date(layout string) Parse according to the given layout
Phone() string Looks like a phone number

You can set your own errors with v.Append():

if !some_complex_condition {
    v.Append("foo", "must be a valid foo")
}

Nested validations

Sub() allows adding nested subvalidations; this is useful if a form creates more than one object.

For example:

v := zvalidate.New()
v.Sub("settings", -1, customer.Settings.Validate())

This will merge the Validator object in to v and prefix all the keys with settings., so you'll have settings.timezone (instead of just timezone).

You can also add arrays:

for i, a := range customer.Addresses {
    a.Sub("addresses", i, c.Validate())
}

This will be added as addresses[0].city, addresses[1].city, etc.

If the error is not a Validator then the Error() text will be added as just the key name without subkey (same as v.Append("key", "msg")); this is mostly to support cases like:

func (Customer c) Validate() {
    v := validate.New()

    ok, err := c.isUniqueEmail(c.Email)
    if err != nil {
        return err
    }
    if !ok {
        v.Append("email", "must be unique")
    }

    return v.ErrorOrNil()
}

Displaying errors

The Validator type satisfies the error interface, so you can return them as errors; usually you want to return ErrorOrNil().

The general idea is that validation errors should usually be displayed along the input element, instead of a list in a flash message (but you can do either).

  • To display a flash message or CLI just call String() or HTML().

  • For Go templates there is a TemplateError() helper which can be added to the template.FuncMap. See the godoc for that function for details and an example.

  • For JavaScript Errors is represented as map[string][]string, and marshals well to JSON; in your frontend you just have to find the input belonging to the map key. A simple example might be:

    var display_errors = function(errors) {
        var hidden = '';
        for (var k in errors) {
            if (!errors.hasOwnProperty(k))
                continue;
    
            var elem = document.querySelector('*[name=' + k + ']')
            if (!elem) {
                hidden += k + ': ' + errors[k].join(', ');
                continue;
            }
    
            var err = document.createElement('span');
            err.className = 'err';
            err.innerHTML = 'Error: ' + errors[k].join(', ') + '.';
            elem.insertAdjacentElement('afterend', err);
        }
    
        if (hidden !== '')
            alert(hidden);
    };
    
    display_errors({
        'xxx':    ['oh noes', 'asd'],
        'hidden': ['asd'],
    });
    

caveat: if there is an error with a corresponding form element then that won't be displayed. This is why the above examples Pop() all the errors they want to display, and then display anything that's left. This prevents "hidden" errors.

i18n

There is no direct support for i18n, but the messages are all exported as Message* and can be replaced by your i18n system of choice.

Documentation

Overview

This contains a copy of utf.ValidString() which also considers NULL bytes invalid.

Package zvalidate provides simple validation for Go.

See the README.markdown for an introduction.

Example
package main

import (
	"fmt"

	"zgo.at/zvalidate"
)

func main() {
	email := "martin@arp42.net"

	v := zvalidate.New()
	v.Required("email", email)
	m := v.Email("email", email)

	if v.HasErrors() {
		fmt.Printf("Had the following validation errors:\n%s", v)
	}

	fmt.Printf("parsed email: %s\n", m.Address)

}
Output:

parsed email: martin@arp42.net

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	MessageRequired    = "must be set"
	MessageDomain      = "must be a valid domain"
	MessageURL         = "must be a valid url"
	MessageEmail       = "must be a valid email address"
	MessageIPv4        = "must be a valid IPv4 address"
	MessageIP          = "must be a valid IPv4 or IPv6 address"
	MessageHexColor    = "must be a valid color code"
	MessageLenLonger   = "must be longer than %d characters"
	MessageLenShorter  = "must be shorter than %d characters"
	MessageExclude     = "cannot be ‘%s’"
	MessageInclude     = "must be one of ‘%s’"
	MessageInteger     = "must be a whole number"
	MessageBool        = "must be a boolean"
	MessageDate        = "must be a date as ‘%s’"
	MessagePhone       = "must be a valid phone number"
	MessageRangeHigher = "must be higher than %d"
	MessageRangeLower  = "must be lower than %d"
	MessageUTF8        = "must be UTF-8"
)

Messages for the validations; this can be changed for i18n.

Functions

func TemplateError added in v1.2.0

func TemplateError(k string, v *Validator) template.HTML

TemplateError displays validation errors for the given key.

This will Pop() errors and modify the Validator in-place, so we can see if there are any "hidden" errors later on.

Example
package main

import (
	"html/template"
	"os"

	"zgo.at/zvalidate"
)

func main() {
	funcs := template.FuncMap{
		"validate":   zvalidate.TemplateError,
		"has_errors": zvalidate.TemplateHasErrors,
	}

	t := template.Must(template.New("").Funcs(funcs).Parse(`
<input name="xxx">
{{validate "xxx" .Validate}}

{{if has_errors .Validate}}Hidden: {{.Validate.HTML}}{{end}}
	`))

	v := zvalidate.New()
	v.Append("xxx", "oh noes")
	v.Append("hidden", "sneaky")

	t.Execute(os.Stdout, map[string]interface{}{
		"Validate": &v,
	})

}
Output:

<input name="xxx">
<span class="err">Error: oh noes</span>

Hidden: <ul class='zvalidate'>
<li><strong>hidden</strong>: sneaky.</li>
</ul>

func TemplateHasErrors added in v1.2.0

func TemplateHasErrors(v *Validator) bool

TemplateHasErrors reports if there are any validation errors.

This is useful because "and" evaluates all arguments, and this will error out:

{{if and .Validate .Validate.HasErrors}}

Types

type Validator

type Validator struct {
	Errors map[string][]string `json:"errors"`
}

Validator hold the validation errors.

Typically you shouldn't create this directly but use the New() function.

func New

func New() Validator

New initializes a new Validator.

func (*Validator) Append

func (v *Validator) Append(key, value string, format ...interface{})

Append a new error.

func (*Validator) Boolean

func (v *Validator) Boolean(key, value string, message ...string) bool

Boolean parses as string as a boolean.

func (Validator) Code

func (v Validator) Code() int

Code returns the HTTP status code for the error. Satisfies the guru.coder interface in github.com/teamwork/guru.

func (*Validator) Date

func (v *Validator) Date(key, value, layout string, message ...string) time.Time

Date parses a string in the given date layout.

func (*Validator) Domain

func (v *Validator) Domain(key, value string, message ...string) []string

Domain parses a domain as individual labels.

A domain must consist of at least two labels. So "com" or "localhost" – while technically valid domain names – are not accepted, whereas "example.com" or "me.localhost" are. For the overwhelming majority of applications this makes the most sense.

This works for internationalized domain names (IDN), either as UTF-8 characters or as punycode.

func (*Validator) Email

func (v *Validator) Email(key, value string, message ...string) mail.Address

Email parses an email address.

func (Validator) Error

func (v Validator) Error() string

Error interface.

func (Validator) ErrorJSON

func (v Validator) ErrorJSON() ([]byte, error)

ErrorJSON for reporting errors as JSON.

func (*Validator) ErrorOrNil

func (v *Validator) ErrorOrNil() error

ErrorOrNil returns nil if there are no errors, or the Validator object if there are.

This makes it a bit more elegant to return from a function:

if v.HasErrors() {
    return v
}
return nil

Can now be:

return v.ErrorOrNil()

func (*Validator) Exclude

func (v *Validator) Exclude(key, value string, exclude []string, message ...string)

Exclude validates that the value is not in the exclude list.

This list is matched case-insensitive.

func (*Validator) HTML added in v1.2.0

func (v *Validator) HTML() template.HTML

HTML representation of all errors, or a blank string if there are none.

func (*Validator) HasErrors

func (v *Validator) HasErrors() bool

HasErrors reports if this validation has any errors.

func (*Validator) HexColor

func (v *Validator) HexColor(key, value string, message ...string) (uint8, uint8, uint8)

HexColor parses a color as a hex triplet (e.g. #ffffff or #fff).

func (*Validator) IP added in v1.1.0

func (v *Validator) IP(key, value string, message ...string) net.IP

IP parses an IPv4 or IPv6 address.

func (*Validator) IPv4

func (v *Validator) IPv4(key, value string, message ...string) net.IP

IPv4 parses an IPv4 address.

func (*Validator) Include

func (v *Validator) Include(key, value string, include []string, message ...string)

Include validates that the value is in the include list.

This list is matched case-insensitive.

func (*Validator) Integer

func (v *Validator) Integer(key, value string, message ...string) int64

Integer parses a string as an integer.

func (*Validator) Len

func (v *Validator) Len(key, value string, min, max int, message ...string) int

Len validates the character (rune) length of a string.

A maximum of 0 indicates there is no upper limit.

func (*Validator) Merge

func (v *Validator) Merge(other Validator)

Merge errors from another validator in to this one.

func (*Validator) Phone

func (v *Validator) Phone(key, value string, message ...string) string

Phone parses a phone number.

There are a great amount of writing conventions for phone numbers: https://en.wikipedia.org/wiki/National_conventions_for_writing_telephone_numbers

This merely checks a field contains 5 to 20 characters "0123456789+\-() .", which is not very strict but should cover all conventions.

Returns the phone number with grouping/spacing characters removed.

func (*Validator) Pop added in v1.2.0

func (v *Validator) Pop(key string) []string

Pop an error, removing all errors for this key.

This is mostly useful when displaying errors next to forms: Pop() all the errors you want to display, and then display anything that's left with a flash message or the like. This prevents "hidden" errors.

Returns nil if there are no errors for this key.

func (*Validator) Range

func (v *Validator) Range(key string, value, min, max int64, message ...string)

Range sets the minimum and maximum value of a integer.

A maximum of 0 indicates there is no upper limit.

func (*Validator) Required

func (v *Validator) Required(key string, value interface{}, message ...string)

Required validates that the value is not the type's zero value.

Currently supported types are string, int, int64, uint, uint64, bool, []string, and mail.Address. It will panic if the type is not supported.

func (*Validator) String

func (v *Validator) String() string

Strings representation of all errors, or a blank string if there are none.

func (*Validator) Sub

func (v *Validator) Sub(key, subKey string, err error)

Sub adds sub-validations.

Errors from the subvalidation are merged with the top-level one, the keys are added as "top.sub" or "top[n].sub".

If the error is not a Validator the text will be added as just the key name without subkey (i.e. the same as v.Append("key", "msg")).

For example:

v := zvalidate.New()
v.Required("name", customer.Name)

// key: "settings.domain"
v.Sub("settings", -1, customer.Settings.Validate())

// key: "addresses[1].city"
for i, a := range customer.Addresses {
    a.Sub("addresses", i, c.Validate())
}

func (*Validator) URL

func (v *Validator) URL(key, value string, message ...string) *url.URL

URL parses an URL.

The URL may consist of a scheme, host, path, and query parameters. Only the host is required.

The host is validated with the Domain() validation. If the scheme is not given "http" will be prepended.

func (*Validator) UTF8 added in v1.2.2

func (v *Validator) UTF8(key, value string, message ...string)

UTF8 validates that this string is valid UTF-8.

Caveat: this will consider NULL bytes *invalid* even though they're valid in UTF-8. Many tools don't accept it (e.g. PostgreSQL and SQLite), there's very rarely a reason to include them in strings, and most uses I've seen is from people trying to insert exploits. So the practical thing to do is just to reject it.

Jump to

Keyboard shortcuts

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