render

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2022 License: MIT Imports: 11 Imported by: 0

README

MIT License GitHub go.mod Go version (subdirectory of monorepo) Coverage Status Go Report Card CodeQL

Render

The render package helps manage HTTP request / response payloads. The motivation and ideas for making this package come from go-chi/render.

Every well-designed, robust and maintainable Web Service / REST API also needs well-defined request and response payloads. Together with the endpoint handlers, the request and response payloads make up the contract between your server and the clients calling on it.

Typically in a REST API application, you will have your data models (objects/structs) that hold lower-level runtime application state, and at times you need to assemble, decorate, hide or transform the representation before responding to a client. That server output (response payload) structure, is also likely the input structure to another handler on the server.

This is where render comes in - offering a few simple helpers to provide a simple pattern for managing payload encoding and decoding.

Render is also combined with some helpers for responding to content types and parsing request bodies. Please have a look at the examples. examples.

All feedback is welcome, thank you!

Features

  • Very simple API
  • Render based on Accept header or format query parameter
  • Custom render functions JSON, XML, PlainText ...
  • Header or body pagination response
  • Map of defaults error and statusCode
  • Customizable error handling
  • Easy decoding request body based on content type
  • Switch encoders/decoders with some popular open source lib

Installation

Install render with go get:

  go get github.com/enverbisevac/render

Usage/Examples

package main

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

	"github.com/enverbisevac/render"
)

type Person struct {
	Name string
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	paths := strings.Split(r.URL.Path, "/")
	name := paths[len(paths)-1]
	render.Render(w, r, Person{
		Name: name,
	})
}

func createUser(w http.ResponseWriter, r *http.Request) {
	user := User{}
	if err := render.Decode(r, &user); err != nil {
		render.Error(w, r, err)
		return
	}
	render.Render(w, r, user)
}

func dumbLoader(limit, offset int, total *int) []User {
	*total = 100
	return []User{
		{
			"Enver",
		},
		{
			"Joe",
		},
	}
}

func listUsers(w http.ResponseWriter, r *http.Request) {
	pagination := render.PaginationFromRequest(r)
	data := dumbLoader(pagination.Size(), pagination.Page(), &pagination.Total)
	pagination.Render(w, r, data)
}

func errorHandler(w http.ResponseWriter, r *http.Request) {
	render.Error(w, r, render.ErrNotFound)
}

func main() {
    http.HandleFunc("/hello/", helloHandler)
    http.HandleFunc("/create", createUser)
	http.HandleFunc("/error/", errorHandler)
	http.ListenAndServe(":8088", nil)
}

API Reference

Bind request body to data type v
  func Bind(r *http.Request, v interface{}) error
Parameter Type Description
r *http.Request Required. Handler request param.
v interface{} Required. Pointer to variable.

error will be returned if binding fails

Render responses based on request r headers
  func Render(w http.ResponseWriter, r *http.Request, v interface{}, params ...interface{})
Parameter Type Description
w http.WriterResponse Required. Writer.
r *http.Request Required. Handler request param.
v interface{} Required. Pointer to variable.
params ...interface{} Variadic number of params. (int/string/http.Header)
Render error response and status code based on request r headers
  func Error(w http.ResponseWriter, r *http.Request, err error, params ...interface{})
Parameter Type Description
w http.WriterResponse Required. Writer.
r *http.Request Required. Handler request param.
err error. Required. Error value.
params ...interface{} Variadic number of params. (int/string/http.Header)
Params variadic function parameter

params can be found in almost any function. Param type can be string, http.Header or integer. Integer value represent status code. String or http.header are just for response headers.

render.Render(w, v, http.StatusOK, "Content-Type", "application/json")
// or you can use const ContentTypeHeader, ApplicationJSON
render.Render(w, v, http.StatusOK, render.ContentTypeHeader, render.ApplicationJSON)
render.Render(w, v, "Content-Type", "application/json")
render.Render(w, v, "Content-Type", "application/json", http.StatusOK)
// using http.Header
render.Render(w, v, http.Header{
	"Content-Type": []string{"application/json"},
}, http.StatusOK)
Integrate 3rd party JSON/XML lib

in this example we will replace standard encoder with goccy/go-json.

package main

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

	"github.com/goccy/go-json"

	"github.com/enverbisevac/render"
)

func init() {
	render.JSONEncoder = func(w io.Writer) render.Encoder {
		return json.NewEncoder(w)
	}
}
Pagination

pagination API function PaginationFromRequest accepts single parameter of type *http.Request and returns Pagination object.

pagination := render.PaginationFromRequest(r)

pagination struct contains several read only fields:

type Pagination struct {
	page  int
	size  int
	prev  int
	next  int
	last  int
	Total int
}

only field you can modify is Total field. Query values are mapped to pagination object:

http://localhost:3000/users?page=1&per_page=10

then you can process your input pagination data:

data := loadUsers(pagination.Size(), pagination.Page(), &pagination.Total)

when we have data then we can render output:

pagination.Render(w, r, data)

Render output can be placed in headers or body of the response, default one is header, this setting can be changed by package variable at init function of your project. List of package variables can be set:

var (
	// PageParam is query name param for current page
	PageParam = "page"
	// PerPageParam is number of items per page
	PerPageParam = "per_page"
	// PerPageDefault sets default number of items on response
	PerPageDefault = 25
	// Linkf is format for Link headers
	Linkf = `<%s>; rel="%s"`
	// PaginationInHeader write pagination in header
	PaginationInHeader = true
	// PaginationBody generates pagination in body
	PaginationBody = DefaultPaginationBody
)
Other API functions
func Blob(w http.ResponseWriter, v []byte, params ...interface{})
func PlainText(w http.ResponseWriter, v string, args ...interface{})
func HTML(w http.ResponseWriter, v string, args ...interface{})
func JSON(w http.ResponseWriter, v interface{}, args ...interface{})
func XML(w http.ResponseWriter, v interface{}, args ...interface{})
func File(w http.ResponseWriter, r *http.Request, fullPath string)
func Attachment(w http.ResponseWriter, r *http.Request, fullPath string)
func Inline(w http.ResponseWriter, r *http.Request, fullPath string)
func NoContent(w http.ResponseWriter)
func Stream(w http.ResponseWriter, r *http.Request, v interface{})

more help on API can be found in Documentation.

Documentation

Documentation

FAQ

Pass status code or response header in Render or Error function

Last parameter is variadic parameter in Render() or Error() function. Then we can use

render.Render(w, r, object, http.StatusAccepted)
// or
render.Render(w, r, object, http.StatusAccepted, "Content-Type", "application/json")
// or
render.Render(w, r, object, "Content-Type", "application/json")
Register global error/status codes
render.ErrorMap[ErrConflict] = http.StatusConflict

we can even wrap the error:

render.Error(w, r, fmt.Errorf("file %s %w", "demo.txt", render.ErrConflict))
Inline error rendering with status code
render.Error(w, r, errors.New("some error"), http.StatusBadRequest)

or

render.Error(w, r, &render.HTTPError{
    Err: errors.New("some error"),
    Status: http.StatusBadRequest,
})
Customize error response
type CustomError struct {
    Module  string `json:"module"`
    Message string `json:"message"`
    Version string `json:"version"`
}

func (e CustomError) Error() string {
    return e.Message
}

render.TreatError = func(r *http.Request, err error) interface{} {
	cerr := &CustomError{}
	if errors.As(err, &cerr) {
		return cerr
    }
    // always return default one
    return render.DefaultErrorRespond(err)
}

Running Tests

To run tests, run the following command

  make test

Acknowledgements

License

MIT

Feedback

If you have any feedback, please reach out enver[@]bisevac.com

Documentation

Overview

Package render helps manage HTTP request / response payloads.

Every well-designed, robust and maintainable Web Service / REST API also needs well-_defined_ request and response payloads. Together with the endpoint handlers, the request and response payloads make up the contract between your server and the clients calling on it.

This is where `render` comes in - offering a few simple helpers to provide a simple pattern for managing payload encoding and decoding.

Index

Constants

View Source
const (
	ApplicationXML     = "application/xml"
	ApplicationXHTML   = "application/xhtml+xml"
	ApplicationJSON    = "application/json"
	ApplicationJSONExt = "application/json; charset=utf-8"
	ApplicationFormURL = "application/x-www-form-urlencoded"
	TextPlain          = "text/plain"
	TextHTML           = "text/html"
	TextXML            = "text/xml"
	TextJavascript     = "text/javascript"
	TextEventStream    = "text/event-stream"
)

MIME types for handling request/response body

View Source
const (
	ContentTypeHeader = "Content-Type"
	AcceptHeader      = "Accept"
)

Header names used in request/response

Variables

View Source
var (
	// JSONDecoder is a package-level variable set to our default JSON decoder
	// function.
	JSONDecoder = DefaultJSONDecoder
	// XMLDecoder is a package-level variable set to our default XML decoder
	// function.
	XMLDecoder = DefaultXMLDecoder
	// FormDecoder is a package-level variable set to our default Form decoder
	// function.
	FormDecoder = DefaultFormDecoder
)
View Source
var (
	// ErrInvalidToken is returned when the api request token is invalid.
	ErrInvalidToken = errors.New("invalid or missing token")

	// ErrUnauthorized is returned when the user is not authorized.
	ErrUnauthorized = errors.New("Unauthorized")

	// ErrForbidden is returned when user access is forbidden.
	ErrForbidden = errors.New("Forbidden")

	// ErrNotFound is returned when a resource is not found.
	ErrNotFound = errors.New("not found")
)
View Source
var (
	// PageParam is query name param for current page
	PageParam = "page"
	// PerPageParam is number of items per page
	PerPageParam = "per_page"
	// PerPageDefault sets default number of items on response
	PerPageDefault = 25
	// Linkf is format for Link headers
	Linkf = `<%s>; rel="%s"`
	// PaginationInHeader write pagination in header
	PaginationInHeader = true
	// PaginationBody generates pagination in body
	PaginationBody = DefaultPaginationBody
)
View Source
var (
	// JSONEncoder is a package variable set to default JSON encoder
	JSONEncoder = DefaultJSONEncoder
	// XMLEncoder is a package variable set to default XML encoder
	XMLEncoder = DefaultXMLEncoder
)

Decode is a package-level variable set to our DefaultDecoder. We do this because it allows you to set render.Decode to another function with the same function signature, while also utilizing the render.DefaultDecoder() function itself. Effectively, allowing you to easily add your own logic to the package defaults. For example, maybe you want to impose a limit on the number of bytes allowed to be read from the request body.

View Source
var DefaultContentType = ContentTypeJSON

DefaultContentType is a package-level variable set to our default content type

View Source
var ErrUnableToParseContentType = errors.New("render: unable to automatically decode the request content type")

ErrUnableToParseContentType is an error for unknown content type

ErrorMap contains predefined errors with assigned status code.

Respond is a package-level variable set to our default Responder. We do this because it allows you to set render.Respond to another function with the same function signature, while also utilizing the render.Responder() function itself. Effectively, allowing you to easily add your own logic to the package defaults. For example, maybe you want to test if v is an error and respond differently, or log something before you respond.

View Source
var TreatError = DefaultErrorRespond

TreatError is a package-level variable set to default function with basic error message response. Any error provided will have just a simple struct with field message describing the error. Developer can create custom function for treating error responses, for example:

render.TreatError = func(r *http.Request, err error) interface{} {
	cerr := &CustomError{}
	if errors.As(err, &cerr) {
		return &customResponse{
			Message: cerr.Err,
			Version: "1.0",
			...
	}

    return &HTTPError{
      Message: "some error message",
      Status: http.StatusCreated,
   }
}

and render.Error(w, r, err) will create response based of your treat function.

Functions

func Attachment

func Attachment(w http.ResponseWriter, r *http.Request, fullPath string)

Attachment sends a response as attachment, prompting client to save the file.

func Bind

func Bind(r *http.Request, v interface{}) error

Bind decodes a request body and executes the Binder method of the payload structure.

func Blob

func Blob(w http.ResponseWriter, v []byte, params ...interface{})

Blob writes raw bytes to the response, the default Content-Type as application/octet-stream, params is optional which can be int or string type. Int will provide status code and string is for header pair values

for example:

Blob(w, v) Blob(w, v, http.StatusOK) Blob(w, v, http.StatusOK, "Content-Type", "application/json") or using constants ContentTypeHeader and ApplicationJSON Blob(w, v, http.StatusOK, "Content-Type", "application/json") Blob(w, v, "Content-Type", "application/json") Blob(w, v, "Content-Type", "application/json", http.StatusOK) you can pass http.Header struct

Blob(w, v, http.Header{
	 "Content-Type": []string{"application/json"},
}, http.StatusOK)

the order of the parameters does not matter.

func DecodeForm

func DecodeForm(r io.Reader, v interface{}) error

DecodeForm decodes a given reader into an interface using the form decoder.

func DecodeJSON

func DecodeJSON(r io.Reader, v interface{}) error

DecodeJSON decodes a given reader into an interface using the json decoder.

func DecodeXML

func DecodeXML(r io.Reader, v interface{}) error

DecodeXML decodes a given reader into an interface using the xml decoder.

func DefaultDecoder

func DefaultDecoder(r *http.Request, v interface{}) (err error)

DefaultDecoder detects the correct decoder for use on an HTTP request and marshals into a given interface.

func DefaultErrorRespond

func DefaultErrorRespond(r *http.Request, err error) interface{}

DefaultErrorRespond returns ErrorResponse object for later processing

func DefaultPaginationBody added in v0.3.0

func DefaultPaginationBody(r *http.Request, p Pagination, v interface{}) interface{}

DefaultPaginationBody returns custom pagination body

func DefaultResponder

func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}, params ...interface{})

DefaultResponder handles streaming JSON and XML responses, automatically setting the Content-Type based on request headers or query param `format`. Default content type is JSON.

func Error

func Error(w http.ResponseWriter, r *http.Request, err error, params ...interface{})

Error renders response body with content type based on Accept header of request. Status codes must be >= 400.

func File

func File(w http.ResponseWriter, r *http.Request, fullPath string)

File sends a response with the content of the file.

func HTML

func HTML(w http.ResponseWriter, v string, params ...interface{})

HTML writes a string to the response, setting the Content-Type as text/html.

func Inline

func Inline(w http.ResponseWriter, r *http.Request, fullPath string)

Inline sends a response as inline, opening the file in the browser.

func JSON

func JSON(w http.ResponseWriter, v interface{}, params ...interface{})

JSON marshals 'v' to JSON, automatically escaping HTML and setting the Content-Type as application/json.

func NoContent

func NoContent(w http.ResponseWriter)

NoContent returns a HTTP 204 "No Content" response.

func PlainText

func PlainText(w http.ResponseWriter, v string, params ...interface{})

PlainText writes a string to the response, setting the Content-Type as text/plain.

func Render

func Render(w http.ResponseWriter, r *http.Request, v interface{}, params ...interface{})

Render renders payload and respond to the client request.

func Stream

func Stream(w http.ResponseWriter, r *http.Request, v interface{})

Stream sends a streaming response with status code and content type.

func XML

func XML(w http.ResponseWriter, v interface{}, params ...interface{})

XML marshals 'v' to JSON, setting the Content-Type as application/xml. It will automatically prepend a generic XML header (see encoding/xml.Header) if one is not found in the first 100 bytes of 'v'.

Types

type ContentType

type ContentType int

ContentType is an enumeration of common HTTP content types.

const (
	ContentTypeUnknown ContentType = iota
	ContentTypePlainText
	ContentTypeHTML
	ContentTypeJSON
	ContentTypeXML
	ContentTypeForm
	ContentTypeEventStream
)

ContentTypes handled by this package.

func GetAcceptedContentType

func GetAcceptedContentType(r *http.Request) ContentType

GetAcceptedContentType reads Accept header from request and returns ContentType

func GetContentType

func GetContentType(s string) ContentType

GetContentType returns ContentType value based on input s

func GetRequestContentType

func GetRequestContentType(r *http.Request) ContentType

GetRequestContentType is a helper function that returns ContentType based on context or request headers.

type Decoder added in v0.2.0

type Decoder interface {
	Decode(v interface{}) error
}

Decoder decodes data from reader

func DefaultFormDecoder added in v0.2.0

func DefaultFormDecoder(r io.Reader) Decoder

DefaultFormDecoder returns new Form decoder for decoding form data.

func DefaultJSONDecoder added in v0.2.0

func DefaultJSONDecoder(r io.Reader) Decoder

DefaultJSONDecoder returns new JSON decoder for decoding JSON data.

func DefaultXMLDecoder added in v0.2.0

func DefaultXMLDecoder(r io.Reader) Decoder

DefaultXMLDecoder returns new XML decoder for decoding XML data.

type Encoder added in v0.2.0

type Encoder interface {
	Encode(v interface{}) error
}

Encoder provide method for encoding reader data

func DefaultJSONEncoder added in v0.2.0

func DefaultJSONEncoder(w io.Writer) Encoder

DefaultJSONEncoder creates default JSON encoder

func DefaultXMLEncoder added in v0.2.0

func DefaultXMLEncoder(w io.Writer) Encoder

DefaultXMLEncoder creates default XML encoder

type ErrorResponse

type ErrorResponse struct {
	Message string `json:"message" xml:"message"`
}

ErrorResponse represents a json-encoded API error.

type HTTPError

type HTTPError struct {
	Err    error
	Status int
}

HTTPError helper structure used as error with status code.

func (HTTPError) Error

func (h HTTPError) Error() string

Error method returns error from HTTPError

type Pagination added in v0.3.0

type Pagination struct {
	Total int
	// contains filtered or unexported fields
}

Pagination holds all page related data

func PaginationFromRequest added in v0.3.0

func PaginationFromRequest(r *http.Request) Pagination

PaginationFromRequest loads data from url like: per_page, page etc.

func (Pagination) Last added in v0.3.0

func (p Pagination) Last() int

Last returns last page

func (Pagination) Next added in v0.3.0

func (p Pagination) Next() int

Next returns next page

func (Pagination) Page added in v0.3.0

func (p Pagination) Page() int

Page returns non exported page value

func (Pagination) Prev added in v0.3.0

func (p Pagination) Prev() int

Prev returns previous page

func (Pagination) Render added in v0.3.0

func (p Pagination) Render(w http.ResponseWriter, r *http.Request, v interface{}, params ...interface{})

Render renders payload and respond to the client request.

func (Pagination) Size added in v0.3.0

func (p Pagination) Size() int

Size returns size (per_page) value

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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