bind

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Feb 27, 2024 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package bind contains model binding features to be used along *lit.Request.

Binding functions

All the functions of this package are binding functions, that parse the string data from the request and binds the result to a Go struct. Supported functions are:

If any of these functions fails to parse the request (for example, they couldn't bind a non-numeric string into an integer field), they return *Error, that contains a user-friendly message and can be used in the response as is.

Receiving files

Body and Request support uploading of files from multipart form requests, with the only requirements being using the appropriate "file" tag and making targeted fields of type *mime/multipart.FileHeader (or its slice variant). Check the package-level example.

Validations

The bind package is integrated with the github.com/jvcoutinho/lit/validate package. If a struct passed as type parameter for Request, URIParameters, Body, Query or Header implements the github.com/jvcoutinho/lit/validate.Validatable interface with a pointer receiver, these binding functions validate the result and can return a validation error with a user-friendly message.

Example (FileUpload)
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
	"github.com/jvcoutinho/lit/render"
	"github.com/jvcoutinho/lit/validate"
)

type UploadFileRequest struct {
	Directory  string                `form:"directory"`
	FileHeader *multipart.FileHeader `file:"file"`
}

func (r *UploadFileRequest) Validate() []validate.Field {
	return []validate.Field{
		validate.Required(r.FileHeader),
		Directory(&r.Directory),
	}
}

func UploadFile(r *lit.Request) lit.Response {
	req, err := bind.Body[UploadFileRequest](r)
	if err != nil {
		return render.BadRequest(err)
	}

	file, err := req.FileHeader.Open()
	if err != nil {
		return render.InternalServerError(err)
	}
	defer file.Close()

	destination, err := os.Create(path.Join(req.Directory, req.FileHeader.Filename))
	if err != nil {
		return render.InternalServerError(err)
	}
	defer destination.Close()

	if _, err := io.Copy(destination, file); err != nil {
		return render.InternalServerError(err)
	}

	return render.NoContent()
}

func main() {
	r := lit.NewRouter()
	r.POST("/upload", UploadFile)

	f := createTemporaryFile()
	defer os.Remove(f.Name())

	body, contentType := createMultipartBody(f)

	res := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/upload", body)
	req.Header.Set("Content-Type", contentType)

	r.ServeHTTP(res, req)

	fmt.Println(res.Code)
}

func createMultipartBody(f *os.File) (io.Reader, string) {
	var (
		body   = &bytes.Buffer{}
		writer = multipart.NewWriter(body)
	)
	defer writer.Close()

	part, err := writer.CreateFormFile("file", f.Name())
	if err != nil {
		log.Fatal(err)
	}

	if _, err := io.Copy(part, f); err != nil {
		log.Fatal(err)
	}

	if err := writer.WriteField("directory", os.TempDir()); err != nil {
		log.Fatal(err)
	}

	return body, writer.FormDataContentType()
}

func createTemporaryFile() *os.File {
	f, err := os.CreateTemp("", "temporary_file")
	if err != nil {
		log.Fatal(err)
	}

	return f
}

// Directory validates that target is a valid directory.
func Directory(target *string) validate.Field {
	validation := validate.Field{
		Valid:   false,
		Message: "{0} should be a valid directory path",
		Fields:  []any{target},
	}

	if target == nil {
		return validation
	}

	fileInfo, err := os.Stat(*target)
	if err != nil {
		return validation
	}

	validation.Valid = fileInfo.IsDir()

	return validation
}
Output:

204

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrUnsupportedContentType = errors.New("unsupported Content-Type")

Functions

func Body

func Body[T any](r *lit.Request) (T, error)

Body binds the request's body into the fields of a struct of type T.

It checks the Content-Type header to select an appropriated parsing method:

  • "application/json" for JSON parsing
  • "application/xml" or "text/xml" for XML parsing
  • "application/x-yaml" for YAML parsing
  • "application/x-www-form-urlencoded" or "multipart/form-data" for form parsing

Tags from encoding packages, such as "json", "xml" and "yaml" tags, can be used appropriately. For form parsing, use the tag "form".

For files inside multipart forms, use the tag "file". Target fields should also be of type *mime/multipart.FileHeader or []*mime/multipart.FileHeader. The maximum number of bytes stored in memory is 32MB, while the rest is stored in temporary files.

If the Content-Type header is not set, Body defaults to JSON parsing. If it is not supported, it returns ErrUnsupportedContentType.

If *T implements validate.Validatable (with a pointer receiver), Body calls validate.Fields on the result and can return validate.Error.

If T is not a struct type, Body panics.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodPost, "/books", strings.NewReader(`
		{"name": "Percy Jackson", "publishYear": 2009}
	`))

	r := lit.NewRequest(req)

	type RequestBody struct {
		Name        string `json:"name"`
		PublishYear int    `json:"publishYear"`
	}

	body, err := bind.Body[RequestBody](r)
	if err == nil {
		fmt.Println(body.Name)
		fmt.Println(body.PublishYear)
	}

}
Output:

Percy Jackson
2009
Example (Form)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodGet, "/books", nil)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.URL.RawQuery = "publishYear=2009&name=Percy%20Jackson"

	r := lit.NewRequest(req)

	type RequestBody struct {
		Name        string `form:"name"`
		PublishYear int    `form:"publishYear"`
	}

	body, err := bind.Body[RequestBody](r)
	if err == nil {
		fmt.Println(body.Name)
		fmt.Println(body.PublishYear)
	}

}
Output:

Percy Jackson
2009
func Header[T any](r *lit.Request) (T, error)

Header binds the request's header into the fields of a struct of type T. Targeted fields should be exported and annotated with the tag "header" (case-insensitive). Otherwise, they are ignored.

If any field couldn't be bound, Header returns Error.

If *T implements validate.Validatable (with a pointer receiver), Header calls validate.Fields on the result and can return validate.Error.

If T is not a struct type, Header panics.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodGet, "/books", nil)
	req.Header.Add("Content-Length", "150")
	req.Header.Add("Authorization", "Bearer uPSsoa65gqkFv2Z6sZ3rZCZwnCjzaXe8TNdk0bJCFFJGrH6wmnzyK4evHBtTuvVH")

	r := lit.NewRequest(req)

	type Header struct {
		ContentLength uint   `header:"Content-Length"`
		Authorization string `header:"Authorization"`
	}

	h, err := bind.Header[Header](r)
	if err == nil {
		fmt.Println(h.ContentLength)
		fmt.Println(h.Authorization)
	}

}
Output:

150
Bearer uPSsoa65gqkFv2Z6sZ3rZCZwnCjzaXe8TNdk0bJCFFJGrH6wmnzyK4evHBtTuvVH

func HeaderField

func HeaderField[T primitiveType | time.Time](r *lit.Request, header string) (T, error)

HeaderField binds a field from the request's header into a value of type T. T can be either a primitive type or a time.Time.

HeaderField consider header as case-insensitive.

If the value can't be bound into T, HeaderField returns Error.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodGet, "/books", nil)
	req.Header.Add("Content-Length", "150")
	req.Header.Add("Authorization", "Bearer uPSsoa65gqkFv2Z6sZ3rZCZwnCjzaXe8TNdk0bJCFFJGrH6wmnzyK4evHBtTuvVH")

	r := lit.NewRequest(req)

	contentLength, err := bind.HeaderField[int](r, "Content-Length")
	if err == nil {
		fmt.Println(contentLength)
	}

	authorization, err := bind.HeaderField[string](r, "authorization") // case-insensitive
	if err == nil {
		fmt.Println(authorization)
	}

}
Output:

150
Bearer uPSsoa65gqkFv2Z6sZ3rZCZwnCjzaXe8TNdk0bJCFFJGrH6wmnzyK4evHBtTuvVH

func Query

func Query[T any](r *lit.Request) (T, error)

Query binds the request's query parameters into the fields of a struct of type T. Targeted fields should be exported and annotated with the tag "query". Otherwise, they are ignored.

If a field can't be bound, Query returns Error.

If *T implements validate.Validatable (with a pointer receiver), Query calls validate.Fields on the result and can return validate.Error.

If T is not a struct type, Query panics.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodGet, "/books", nil)
	req.URL.RawQuery = "publish_year=2009&name=Percy%20Jackson"

	r := lit.NewRequest(req)

	type BookQuery struct {
		PublishYear uint   `query:"publish_year"`
		Name        string `query:"name"`
	}

	query, err := bind.Query[BookQuery](r)
	if err == nil {
		fmt.Println(query.PublishYear, query.Name)
	}

}
Output:

2009 Percy Jackson
Example (Form)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodGet, "/books", nil)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.URL.RawQuery = "publish_year=2009&name=Percy%20Jackson"

	r := lit.NewRequest(req)

	type BookQuery struct {
		PublishYear uint   `query:"publish_year"`
		Name        string `query:"name"`
	}

	query, err := bind.Query[BookQuery](r)
	if err == nil {
		fmt.Println(query.PublishYear, query.Name)
	}

}
Output:

2009 Percy Jackson

func Request

func Request[T any](r *lit.Request) (T, error)

Request binds the request's body, query, header and URI parameters into the fields of a struct of type T. Targeted fields should be exported and annotated with corresponding binding tags. Otherwise, they are ignored.

It's an optimized combination of the binding functions Body, Query, Header and URIParameters, suitable when you need to read from multiple inputs of the request.

If a field can't be bound, Request returns Error.

If *T implements validate.Validatable (with a pointer receiver), Request calls validate.Fields on the result and can return validate.Error.

If T is not a struct type, Request panics.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	req := httptest.NewRequest(http.MethodPatch, "/users/123",
		strings.NewReader(`{"name": "John", "age": 28}`))
	req.Header.Add("Authorization", "Bearer token")

	r := lit.NewRequest(req).WithURIParameters(map[string]string{"user_id": "123"})

	type Request struct {
		UserID              int    `uri:"user_id"`
		Name                string `json:"name"`
		Age                 int    `json:"age"`
		AuthorizationHeader string `header:"Authorization"`
	}

	request, err := bind.Request[Request](r)
	if err == nil {
		fmt.Println(request.UserID)
		fmt.Println(request.Name)
		fmt.Println(request.Age)
		fmt.Println(request.AuthorizationHeader)
	}

}
Output:

123
John
28
Bearer token

func URIParameter

func URIParameter[T primitiveType | time.Time](r *lit.Request, parameter string) (T, error)

URIParameter binds a request's URI parameter into a value of type T. T can be either a primitive type or a time.Time.

If the value can't be bound into T, URIParameter returns Error.

If parameter is not registered as one of the request's expected parameters, URIParameter panics.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	r := lit.NewRequest(
		httptest.NewRequest(http.MethodGet, "/users/123/books/book_1", nil),
	).WithURIParameters(
		map[string]string{"user_id": "123", "book_id": "book_1"},
	)

	userID, err := bind.URIParameter[int](r, "user_id")
	if err == nil {
		fmt.Println(userID)
	}

}
Output:

123

func URIParameters

func URIParameters[T any](r *lit.Request) (T, error)

URIParameters binds the request's URI parameters into the fields of a struct of type T. Targeted fields should be exported and annotated with the tag "uri". Otherwise, they are ignored.

If a field can't be bound, URIParameters returns Error.

If *T implements validate.Validatable (with a pointer receiver), URIParameters calls validate.Fields on the result and can return validate.Error.

If T is not a struct type, URIParameters panics.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/jvcoutinho/lit"
	"github.com/jvcoutinho/lit/bind"
)

func main() {
	r := lit.NewRequest(
		httptest.NewRequest(http.MethodGet, "/users/123/books/book_1", nil),
	).WithURIParameters(
		map[string]string{"user_id": "123", "book_id": "book_1"},
	)

	type RequestURIParameters struct {
		UserID int    `uri:"user_id"`
		BookID string `uri:"book_id"`
	}

	uri, err := bind.URIParameters[RequestURIParameters](r)
	if err == nil {
		fmt.Println(uri.UserID, uri.BookID)
	}

}
Output:

123 book_1

Types

type Error

type Error struct {
	// Incoming value.
	Value string
	// Target of the binding.
	Target reflect.Type
	// The actual error.
	Err error
}

Error is returned when a binding to a target value fails.

func (Error) Error

func (e Error) Error() string

Jump to

Keyboard shortcuts

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