chain

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2018 License: MIT Imports: 2 Imported by: 1

README

chain

go get "github.com/codemodus/chain"

Package chain aids the composition of Handler wrapper chains that carry request-scoped data.

Nesting functions is a simple concept. If your handler wrapper order does not need to be composable, do not use a package and avoid adding a dependency to your project. However, nesting functions quickly becomes burdensome as the need for flexibility increases. Add to that the need for request-scoped data, and Chain is a lightweight and complete solution.

Usage

func Convert(hw func(http.Handler) http.Handler) func(Handler) Handler
type Chain
    func New(hws ...func(Handler) Handler) Chain
    func (c Chain) Append(hws ...func(Handler) Handler) Chain
    func (c Chain) End(h Handler) http.Handler
    func (c Chain) EndFn(h HandlerFunc) http.Handler
    func (c Chain) Merge(cs ...Chain) Chain
    func (c Chain) SetContext(ctx context.Context) Chain
type Handler
type HandlerFunc
    func (h HandlerFunc) ServeHTTPContext(ctx context.Context, w http.ResponseWriter, r *http.Request)
Setup
import (
    // ...

    "github.com/codemodus/chain"
    "golang.org/x/net/context"
)

func main() {
    // ...

    ctx := context.Background()
    // Add common data to the context.

    ch0 := chain.New(firstWrapper, secondWrapper).SetContext(ctx)
    ch1 := ch0.Append(chain.Convert(httpHandlerWrapper), fourthWrapper)

    ch2 := chain.New(beforeFirstWrapper).SetContext(ctx)
    ch2 = ch2.Merge(ch1)

    m := http.NewServeMux()
    m.Handle("/1w2w_End1", ch0.EndFn(ctxHandler))
    m.Handle("/1w2w_End2", ch0.EndFn(anotherCtxHandler))
    m.Handle("/1w2wHw4w_End1", ch1.EndFn(ctxHandler))
    m.Handle("/0w1w2wHw4w_End1", ch2.EndFn(ctxHandler))

    // ...
}
Handler Wrapper And Context Usage (Set)
func firstWrapper(n chain.Handler) chain.Handler {
    return chain.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
        // ...
        
        ctx = setMyString(ctx, "Send this down the line.")
    	
        n.ServeHTTPContext(ctx, w, r)
    	
        // ...
    })
}

This function signature will make wrappers compatible with chain. It's simple to make existing wrappers capable of carrying request-scoped data.

Handler Function And Context Usage (Get)
func ctxHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    // ...
    
    if s, ok := getMyString(ctx); ok {
        // s = "Send this down the line."
    }
    
    // ...
}

End-point functions will need to be adapted using chain.HandlerFunc. As a convenience, EndFn will adapt functions with compatible signatures. The prescribed signature is in accordance with practices outlined in the Go Blog.

HTTP Handler Wrapper
func httpHandlerWrapper(n http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // ...

        n.ServeHTTP(w, r)

        // ...
    })
}

A standard http.Handler wrapper is a perfect candidate for chain.Convert. If chain.Convert is used, the added http.Handler wrapper will be compatible with a Chain, but will not be able to make use of the request context.

More Info

net/context?

net/context is made for this need and enables some interesting capabilities. The Go Blog: Context

What if SetContext goes unused?

A context.Background result is provided as a default initial context.Context.
Conversely, it is also possible to set different initial context.Context objects for different chains which may or may not share wrapper content/order.

Context Scope

By not using more broadly scoped context access, a small trick is needed to move data to and from certain points in the request life cycle. For instance, if a final handler adds any data to the context, that data will not be accessible to any wrapper code residing after calls to ServeHTTP/ServeHTTPContext.

An example of resolving this is not being included here as it leaves the scope of the package itself. Though, this package is tested for the capability, so review the relevant test if need be. Convenience functions can be found.

catena

If a project is not in need of a request context, consider using catena. The API is nearly identical to chain, so adding a request context is easy when needs change.

Documentation

View the GoDoc

Benchmarks

These results are for comparison of normally nested functions, and chained functions. Each benchmark includes 10 functions prior to the final handler.

Go1.5
benchmark           iter      time/iter   bytes alloc         allocs
---------           ----      ---------   -----------         ------
BenchmarkChain10     30000     53.55 μs/op     3459 B/op   47 allocs/op
BenchmarkChain10-4   10000    167.94 μs/op     3470 B/op   47 allocs/op
BenchmarkChain10-8   10000    167.07 μs/op     3477 B/op   47 allocs/op
BenchmarkNest10      30000     53.42 μs/op     3460 B/op   47 allocs/op
BenchmarkNest10-4    10000    167.68 μs/op     3471 B/op   47 allocs/op
BenchmarkNest10-8    10000    167.24 μs/op     3480 B/op   47 allocs/op

Documentation

Overview

Package chain aids the composition of Handler wrapper chains that carry request-scoped data.

Review the test file for examples covering chain manipulation, and a way to pass data to detached scopes (for common use cases race conditions will not be encountered, but caution is warranted). Benchmarks are available showing a negligible increase in processing time and memory consumption, and no increase in memory allocations compared to nesting functions without an aid.

Example
package main

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

	"github.com/codemodus/chain"
	"golang.org/x/net/context"
)

var (
	bTxt0   = "0"
	bTxt1   = "1"
	bTxtA   = "A"
	bTxtEnd = "_END_"
)

func main() {
	ctx := context.Background()
	// Add common data to the context.

	// Each wrapper writes either "0", "1", or "A" to the response body before
	// and after ServeHTTPContext() is called.
	// ctxHandler writes "_END_" to the response body and returns.
	ch00 := chain.New(ctxHandlerWrapper0, ctxHandlerWrapper0).SetContext(ctx)
	ch00A1 := ch00.Append(chain.Convert(httpHandlerWrapperA), ctxHandlerWrapper1)

	ch100A1 := chain.New(ctxHandlerWrapper1).SetContext(ctx)
	ch100A1 = ch100A1.Merge(ch00A1)

	mux := http.NewServeMux()
	mux.Handle("/path_implies_body/00_End", ch00.EndFn(ctxHandler))
	mux.Handle("/path_implies_body/00A1_End", ch00A1.EndFn(ctxHandler))
	mux.Handle("/path_implies_body/100A1_End", ch100A1.EndFn(ctxHandler))

	server := httptest.NewServer(mux)

	rBody0, err := getReqBody(server.URL + "/path_implies_body/00_End")
	if err != nil {
		fmt.Println(err)
	}
	rBody1, err := getReqBody(server.URL + "/path_implies_body/00A1_End")
	if err != nil {
		fmt.Println(err)
	}
	rBody2, err := getReqBody(server.URL + "/path_implies_body/100A1_End")
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println("Chain 0 Body:", rBody0)
	fmt.Println("Chain 1 Body:", rBody1)
	fmt.Println("Chain 2 Body:", rBody2)

}

func getReqBody(url string) (string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return "", err
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	_ = resp.Body.Close()
	return string(body), nil
}

func ctxHandlerWrapper0(n chain.Handler) chain.Handler {
	return chain.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(bTxt0))
		n.ServeHTTPContext(ctx, w, r)
		_, _ = w.Write([]byte(bTxt0))
	})
}

func ctxHandlerWrapper1(n chain.Handler) chain.Handler {
	return chain.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(bTxt1))
		n.ServeHTTPContext(ctx, w, r)
		_, _ = w.Write([]byte(bTxt1))
	})
}

func httpHandlerWrapperA(n http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(bTxtA))
		n.ServeHTTP(w, r)
		_, _ = w.Write([]byte(bTxtA))
	})
}

func ctxHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte(bTxtEnd))
	return
}
Output:

Chain 0 Body: 00_END_00
Chain 1 Body: 00A1_END_1A00
Chain 2 Body: 100A1_END_1A001

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Convert

func Convert(hw func(http.Handler) http.Handler) func(Handler) Handler

Convert takes a http.Handler wrapper and returns a Handler wrapper. This is useful for making standard http.Handler wrappers compatible with a Chain.

Types

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain holds the basic components used to order Handler wrapper chains.

func New

func New(hws ...func(Handler) Handler) Chain

New takes one or more Handler wrappers, and returns a new Chain.

func (Chain) Append

func (c Chain) Append(hws ...func(Handler) Handler) Chain

Append takes one or more Handler wrappers, and appends the value to the returned Chain.

func (Chain) End

func (c Chain) End(h Handler) http.Handler

End takes a Handler and returns an http.Handler.

func (Chain) EndFn

func (c Chain) EndFn(h HandlerFunc) http.Handler

EndFn takes a func that matches the HandlerFunc type, then passes it to End.

func (Chain) Merge

func (c Chain) Merge(cs ...Chain) Chain

Merge takes one or more Chain objects, and appends the values' Handler wrappers to the returned Chain.

func (Chain) SetContext

func (c Chain) SetContext(ctx context.Context) Chain

SetContext takes a context.Context, and updates the stored/initial context of the returned Chain.

type Handler

type Handler interface {
	ServeHTTPContext(context.Context, http.ResponseWriter, *http.Request)
}

Handler interface must be implemented for a function to be able to be wrapped, or served.

type HandlerFunc

type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request)

HandlerFunc is an adapter which allows a function with the appropriate signature to be treated as a Handler.

func (HandlerFunc) ServeHTTPContext

func (h HandlerFunc) ServeHTTPContext(ctx context.Context, w http.ResponseWriter, r *http.Request)

ServeHTTPContext calls h(ctx, w, r)

Jump to

Keyboard shortcuts

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