binding

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 12, 2023 License: MIT Imports: 9 Imported by: 4

README

binding is reflectionless data binding for Go

GoDoc

binding

Reflectionless data binding for Go's net/http

Features

  • HTTP request data binding
  • Data validation (custom and built-in)
  • Error handling

Benefits

  • Moves data binding, validation, and error handling out of your application's handler
  • Reads Content-Type to deserialize form, multipart form, and JSON data from requests
  • No middleware: just a function call
  • Usable in any setting where net/http is present (Negroni, gocraft/web, std lib, etc.)
  • No reflection

Usage example

package main

import (
	"fmt"
	"net/http"

	"github.com/mholt/binding"
)

// First define a type to hold the data
// (If the data comes from JSON, see: http://mholt.github.io/json-to-go)
type ContactForm struct {
	User struct {
		ID int
	}
	Email   string
	Message string
}

// Then provide a field mapping (pointer receiver is vital)
func (cf *ContactForm) FieldMap(req *http.Request) binding.FieldMap {
	return binding.FieldMap{
		&cf.User.ID: "user_id",
		&cf.Email:   "email",
		&cf.Message: binding.Field{
			Form:     "message",
			Required: true,
		},
	}
}

// Now your handlers can stay clean and simple
func handler(resp http.ResponseWriter, req *http.Request) {
	contactForm := new(ContactForm)
	if errs := binding.Bind(req, contactForm); errs != nil {
		http.Error(resp, errs.Error(), http.StatusBadRequest)
		return
	}
	fmt.Fprintf(resp, "From:    %d\n", contactForm.User.ID)
	fmt.Fprintf(resp, "Message: %s\n", contactForm.Message)
}

func main() {
	http.HandleFunc("/contact", handler)
	http.ListenAndServe(":3000", nil)
}

Multipart/form-data usage example

package main

import (
	"bytes"
	"fmt"
	"github.com/mholt/binding"
	"io"
	"log"
	"mime/multipart"
	"net/http"
)

// We expect a multipart/form-data upload with
// a file field named 'data'
type MultipartForm struct {
	Data *multipart.FileHeader `json:"data"`
}

func (f *MultipartForm) FieldMap(req *http.Request) binding.FieldMap {
	return binding.FieldMap{
		&f.Data: "data",
	}
}

// Handlers are still clean and simple
func handler(resp http.ResponseWriter, req *http.Request) {
	multipartForm := new(MultipartForm)
	if errs := binding.Bind(req, multipartForm); errs != nil {
		http.Error(resp, errs.Error(), http.StatusBadRequest)
		return
	}

	// To access the file data you need to Open the file
	// handler and read the bytes out.
	var fh io.ReadCloser
	var err error
	if fh, err = multipartForm.Data.Open(); err != nil {
		http.Error(resp,
			fmt.Sprint("Error opening Mime::Data %v", err),
			http.StatusInternalServerError)
		return
	}
	defer fh.Close()
	dataBytes := bytes.Buffer{}
	var size int64
	if size, err = dataBytes.ReadFrom(fh); err != nil {
		http.Error(resp,
			fmt.Sprint("Error reading Mime::Data %v", err),
			http.StatusInternalServerError)
		return
	}
	// Now you have the attachment in databytes.
	// Maximum size is default is 10MB.
	log.Printf("Read %v bytes with filename %s",
		size, multipartForm.Data.Filename)
}

func main() {
	http.HandleFunc("/upload", handler)
	http.ListenAndServe(":3000", nil)
}

You can test from CLI using the excellent httpie client

http -f POST localhost:3000/upload data@myupload

Custom data validation

You may optionally have your type implement the binding.Validator interface to perform your own data validation. The .Validate() method is called after the struct is populated.

func (cf ContactForm) Validate(req *http.Request) error {
	if cf.Message == "Go needs generics" {
		return binding.Errors{
			binding.NewError([]string{"message"}, "ComplaintError", "Go has generics. They're called interfaces.")
		}
	}
	return nil
}

Binding custom types

For types you've defined, you can bind form data to it by implementing the Binder interface. Here's a contrived example:

type MyBinder map[string]string

func (t MyBinder) Bind(fieldName string, strVals []string) error {
	t["formData"] = strVals[0]
	return nil
}

If you can't add a method to the type, you can still specify a Binder func in the field spec. Here's a contrived example that binds an integer (not necessary, but you get the idea):

func (t *MyType) FieldMap(req *http.Request) binding.FieldMap {
	return binding.FieldMap{
		"a-key": binding.Field{
			Form: "number",
			Binder: func(fieldName string, formVals []string) error {
				val, err := strconv.Atoi(formVals[0])
				if err != nil {
					return binding.Errors{binding.NewError([]string{fieldName}, binding.DeserializationError, err.Error())}
				}
				t.SomeNumber = val
				return nil
			},
		},
	}
}

The Errors type has a convenience method, Add, which you can use to append to the slice if you prefer.

Supported types (forms)

The following types are supported in form deserialization by default. (JSON requests are delegated to encoding/json.)

  • uint, *uint, []uint, uint8, *uint8, []uint8, uint16, *uint16, []uint16, uint32, *uint32, []uint32, uint64, *uint64, []uint64
  • int, *int, []int, int8, *int8, []int8, int16, *int16, []int16, int32, *int32, []int32, int64, *int64, []int64
  • float32, *float32, []float32, float64, *float64, []float64
  • bool, *bool, []bool
  • string, *string, []string
  • time.Time, *time.Time, []time.Time
  • *multipart.FileHeader, []*multipart.FileHeader

Documentation

Overview

Package binding deserializes data from HTTP requests into a struct ready for your application to use (without reflection). It also facilitates data validation and error handling.

Example (FieldBinder)
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strconv"

	"github.com/mholt/binding"
)

type MyType struct {
	SomeNumber int
}

func (t *MyType) FieldMap(req *http.Request) binding.FieldMap {
	return binding.FieldMap{
		"a-key": binding.Field{
			Form: "number",
			Binder: func(fieldName string, formVals []string) error {
				val, err := strconv.Atoi(formVals[0])
				if err != nil {
					return binding.Errors{binding.NewError([]string{fieldName}, binding.DeserializationError, err.Error())}
				}
				t.SomeNumber = val
				return nil
			},
		},
	}
}

func main() {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		b := new(MyType)
		if err := binding.Bind(req, b); err != nil {
			w.WriteHeader(500)
			w.Write([]byte(err.Error()))
			return
		}
		fmt.Fprintf(w, "%d", b.SomeNumber)
	}))
	defer ts.Close()

	resp, err := http.DefaultClient.PostForm(ts.URL, url.Values{"number": []string{"1008"}})
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()

	io.Copy(os.Stdout, resp.Body)

}
Output:

1008

Index

Examples

Constants

View Source
const (
	RequiredError        = "RequiredError"
	ContentTypeError     = "ContentTypeError"
	DeserializationError = "DeserializationError"
	TypeError            = "TypeError"
)
View Source
const (
	StatusUnprocessableEntity = 422
)

Variables

View Source
var (
	// Maximum amount of memory to use when parsing a multipart form.
	// Set this to whatever value you prefer; default is 10 MB.
	MaxMemory = int64(1024 * 1024 * 10)

	// If no TimeFormat is specified for a time.Time field, this
	// format will be used by default when parsing.
	TimeFormat = time.RFC3339
)

Functions

func Bind

func Bind(req *http.Request, userStruct FieldMapper) error

Bind takes data out of the request and deserializes into a struct according to the Content-Type of the request. If no Content-Type is specified, there better be data in the query string, otherwise an error will be produced.

A non-nil return value may be an Errors value.

func Form

func Form(req *http.Request, userStruct FieldMapper) error

Form deserializes form data out of the request into a struct you provide. This function invokes data validation after deserialization.

func Json

func Json(req *http.Request, userStruct FieldMapper) error

Json deserializes a JSON request body into a struct you specify using the standard encoding/json package (which uses reflection). This function invokes data validation after deserialization.

func MultipartForm

func MultipartForm(req *http.Request, userStruct FieldMapper) error

MultipartForm reads a multipart form request and deserializes its data and files into a struct you provide. Files should be deserialized into *multipart.FileHeader fields.

func URL added in v0.5.0

func URL(req *http.Request, userStruct FieldMapper) error

URL reads data out of the query string into a struct you provide. This function invokes data validation after deserialization.

func Validate

func Validate(req *http.Request, userStruct FieldMapper) error

Validate ensures that all conditions have been met on every field in the populated struct. Validation should occur after the request has been deserialized into the struct.

Types

type Binder

type Binder interface {
	// Bind populates the type with data in []string which comes from the
	// HTTP request. The first argument is the field name.
	Bind(string, []string) error
}

Binder is an interface which can deserialize itself from a slice of string coming from the request. Implement this interface so the type can be populated from form data in HTTP requests.

Example
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"

	"github.com/mholt/binding"
)

type MyBinder map[string]string

func (t MyBinder) Bind(fieldName string, strVals []string) error {
	t["formData"] = strVals[0]
	return nil
}

type MyBinderContainer struct {
	Important MyBinder
}

func (c *MyBinderContainer) FieldMap(req *http.Request) binding.FieldMap {
	return binding.FieldMap{
		&c.Important: "important",
	}
}

func main() {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		v := new(MyBinderContainer)
		v.Important = make(MyBinder)
		if err := binding.Bind(req, v); err != nil {
			w.WriteHeader(500)
			w.Write([]byte(err.Error()))
			return
		}
		fmt.Fprintf(w, v.Important["formData"])
	}))
	defer ts.Close()

	resp, err := http.DefaultClient.PostForm(ts.URL, url.Values{"important": []string{"1008"}})
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()

	io.Copy(os.Stdout, resp.Body)

}
Output:

1008

type Error

type Error interface {
	error
	Fields() []string
	Kind() string
	Message() string
}

An Error is an error that is associated with 0 or more fields of a request.

Fields should return the fields associated with the error. When the return value's length is 0, something is wrong with the request as a whole.

Kind should return a string that can be used like an error code to process or categorize the Error.

Message should return the error message.

func NewError added in v0.5.0

func NewError(fieldNames []string, kind, message string) Error

type Errors

type Errors []Error

Errors may be generated during deserialization, binding, or validation.

func (*Errors) Add

func (e *Errors) Add(fieldNames []string, kind, message string)

Add adds an Error associated with the fields indicated by fieldNames, with the given kind and message.

Use a fieldNames value of length 0 to indicate that the error is about the request as a whole, and not necessarily any of the fields.

kind should be a string that can be used like an error code to process or categorize the error being added.

message should be human-readable and detailed enough to pinpoint and resolve the problem, but it should be brief. For example, a payload of 100 objects in a JSON array might have an error in the 41st object. The message should help the end user find and fix the error with their request.

func (Errors) Error

func (e Errors) Error() string

Error returns a concatenation of all its error messages.

func (*Errors) Has

func (e *Errors) Has(kind string) bool

Has determines whether kind matches the return value of Kind() of any Error in e.

func (*Errors) Len

func (e *Errors) Len() int

Len returns the number of errors.

type Field

type Field struct {

	// Form is the form field name to bind from
	Form string

	// Required indicates whether the field is required. A required
	// field that deserializes into the zero value for that type
	// will generate an error.
	Required bool

	// TimeFormat specifies the time format for time.Time fields.
	TimeFormat string

	// Binder is a function that converts the incoming request value(s)
	// to the field type; in other words, this field is populated
	// by executing this function. Useful when the custom type doesn't
	// implement the Binder interface.
	Binder func(string, []string) error

	// ErrorMessage allows the error the to be customized.
	ErrorMessage string
}

Field describes the properties of a struct field.

type FieldMap

type FieldMap map[interface{}]interface{}

FieldMap is a map of pointers to struct fields -> field names from the request. The values could also be Field structs to specify metadata about the field.

type FieldMapper

type FieldMapper interface {
	// FieldMap returns a map of pointers into which the values will
	// be deserialized to field names from the request's form body.
	FieldMap(*http.Request) FieldMap
}

Only types that are FieldMappers can have request data deserialized into them.

type Validator

type Validator interface {
	// Validate validates that the request is OK. It is recommended
	// that validation be limited to checking values for syntax and
	// semantics, enough to know that you can make sense of the request
	// in your application. For example, you might verify that a credit
	// card number matches a valid pattern, but you probably wouldn't
	// perform an actual credit card authorization here.
	Validate(*http.Request) error
}

Validator can be implemented by your type to handle some rudimentary request validation separately from your application logic.

Jump to

Keyboard shortcuts

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