nchi

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Mar 28, 2023 License: BSD-3-Clause, MIT Imports: 7 Imported by: 0

README

nchi - http router with speed, flexbility, and elegance

GoDoc unit tests report card codecov

nchi is a lightweight, elegant, and fast router for building Go HTTP services. It's especially good at helping you write large REST API services that are kept maintainable as your project grows and changes. nchi is built on top of the nject dependency injection framework and the fastest Go http router, httprouter. nchi is a straight-up rip-off of chi substituting nject for context and in the process making it easier to write middleware and and endpoints.

nchi can use standard middleware and it can use dependency-injection middleware. See:

  • nvelope for nject-based middleware
  • chi for chi's middleware collection

Note: if you're using nvelope.DeferredWriter, avoid other middleware that replaces the http.ResponseWriter.

"Standard" middlewhare has one of the following shapes:

  • func(http.HandlerFunc) http.HandlerFunc
  • func(http.Handler) http.Handler

nchi automatically detects standard middleware and translates it for use in an nject-based framework.

Install

go get github.com/muir/nchi

Examples

As easy as:

package main

import (
	"net/http"

	"github.com/muir/nchi"
	"github.com/go-chi/chi/v5/middleware"
)

func main() {
	r := nchi.NewRouter()
	r.Use(middleware.Logger)
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome"))
	})
	http.ListenAndServe(":3000", r)
}

REST Preview:

Here is a little preview of how routing looks like with nchi.

import (
  //...
  "github.com/muir/nchi"
  "github.com/muir/nvelope"
  "github.com/muir/nject"
  "github.com/go-chi/chi/v5/middleware"
)

func main() {
  r := nchi.NewRouter()

  // A good base middleware stack
  r.Use(
     middleware.RequestID, 
     middleware.RealIP, 
     middleware.Logger,
     nchi.DecodeJSON,
  )

  r.Use(func(inner func() error, w http.ResponseWriter) {
     err := inner()
     if err == nil { 
       return
     }
     code := nvelope.GetReturnCode(err)
     w.WriteHeader(code)
     w.Write([]byte(err.Error()))
  })

  // Set a timeout value on the request context (ctx), that will signal
  // through ctx.Done() that the request has timed out and further
  // processing should be stopped.
  r.Use(middleware.Timeout(60 * time.Second))

  r.Get("/", func(w http.ResponseWriter) {
    w.Write([]byte("hi"))
  })

  // RESTy routes for "articles" resource
  r.Route("/articles", func(r nchi.Router) {
    r.With(paginate).Get("/", listArticles)                           // GET /articles
    r.With(paginate).Get("/:month/:day/:year", listArticlesByDate)    // GET /articles/01-16-2017

    r.Post("/", createArticle)                                        // POST /articles
    r.Get("/search", searchArticles)                                  // GET /articles/search

    r.Get("/:articleSlug", getArticleBySlug)                          // GET /articles/home-is-toronto

    // Subrouters:
    r.Route("/:articleID", func(r nchi.Router) {
      r.Use(LoadArticle)
      r.Get("/", getArticle)                                          // GET /articles/123
      r.Put("/", updateArticle)                                       // PUT /articles/123
      r.Delete("/", deleteArticle)                                    // DELETE /articles/123
    })
  })

  // Mount the admin sub-router
  r.Mount("/admin", adminRouter())

  http.ListenAndServe(":3333", r)
}

func LoadArticle(params nchi.Params) (*Article, nject.TerminalError) {
  articleID := params.ByName("articleID")
  article, err := dbGetArticle(articleID)
  if errors.Is(err, sql.NotFound) {
    return nil, nvelope.NotFound(err)
  }
  return article, err
}

func getArticle(article *Article, w http.ResponseWriter) {
  w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
}

Developement status

nchi seems to be working fine for the time being so not much is changing. Please file an issue if there is something you would like changed.

Documentation

Overview

Example

Example shows an injection chain handling a single endpoint using nject, nape, and nvelope.

package main

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

	"github.com/muir/nchi"
	"github.com/muir/nvelope"
)

type PostBodyModel struct {
	Use      string `json:"use"`
	Exported string `json:"exported"`
	Names    string `json:"names"`
}

type ExampleRequestBundle struct {
	Request     PostBodyModel `nvelope:"model"`
	With        *string       `nvelope:"path,name=with"`
	Parameters  int64         `nvelope:"path,name=parameters"`
	Friends     []int         `nvelope:"query,name=friends"`
	ContentType string        `nvelope:"header,name=Content-Type"`
}

type ExampleResponse struct {
	Stuff string `json:"stuff,omitempty"`
	Here  string `json:"here,omitempty"`
}

func HandleExampleEndpoint(req ExampleRequestBundle) (nvelope.Response, error) {
	if req.ContentType != "application/json" {
		return nil, errors.New("content type must be application/json")
	}
	switch req.Parameters {
	case 666:
		panic("something is not right")
	case 100:
		return nil, nil
	default:
		return ExampleResponse{
			Stuff: *req.With,
		}, nil
	}
}

func Service(r *nchi.Mux) {
	r.Use(
		// order matters and this is a correct order
		nvelope.NoLogger,
		nvelope.InjectWriter,
		nvelope.EncodeJSON,
		nvelope.CatchPanic,
		nvelope.Nil204,
		nvelope.ReadBody,
		nchi.DecodeJSON,
	)
	r.Post("/a/path/:with/:parameters",
		HandleExampleEndpoint,
	)
}

// Example shows an injection chain handling a single endpoint using nject,
// nape, and nvelope.
func main() {
	r := nchi.NewRouter()
	Service(r)
	ts := httptest.NewServer(r)
	client := ts.Client()
	doPost := func(url string, body string) {
		// nolint:noctx
		res, err := client.Post(ts.URL+url, "application/json",
			strings.NewReader(body))
		if err != nil {
			fmt.Println("response error:", err)
			return
		}
		b, err := ioutil.ReadAll(res.Body)
		if err != nil {
			fmt.Println("read error:", err)
			return
		}
		res.Body.Close()
		fmt.Println(res.StatusCode, "->"+string(b))
	}
	doPost("/a/path/joe/37", `{"Use":"yeah","Exported":"uh hu"}`)
	doPost("/a/path/joe/100", `{"Use":"yeah","Exported":"uh hu"}`)
	doPost("/a/path/joe/38", `invalid json`)
	doPost("/a/path/joe/666", `{"Use":"yeah","Exported":"uh hu"}`)

}
Output:

200 ->{"stuff":"joe"}
204 ->
400 ->nchi_test.ExampleRequestBundle model: Could not decode application/json into nchi_test.PostBodyModel: invalid character 'i' looking for beginning of value
500 ->panic: something is not right

Index

Examples

Constants

This section is empty.

Variables

View Source
var DecodeJSON = nvelope.GenerateDecoder(
	nvelope.WithDecoder("application/json", json.Unmarshal),
	nvelope.WithDefaultContentType("application/json"),
	nvelope.WithPathVarsFunction(func(p httprouter.Params) nvelope.RouteVarLookup {
		return p.ByName
	}),
)

DecodeJSON is is a pre-defined special nject.Provider created with nvelope.GenerateDecoder for decoding JSON requests. Use it with the other features of https://github.com/muir/nvelope . DecodeJSON must be paired with nvelope.ReadBody to actually decode JSON.

View Source
var DecodeXML = nvelope.GenerateDecoder(
	nvelope.WithDecoder("application/xml", xml.Unmarshal),
	nvelope.WithDefaultContentType("application/xml"),
	nvelope.WithPathVarsFunction(func(p httprouter.Params) nvelope.RouteVarLookup {
		return p.ByName
	}),
)

DecodeXML is is a pre-defined special nject.Provider created with nvelope.GenerateDecoder for decoding XML requests.Use it with the other features of https://github.com/muir/nvelope . DecodeXML must be paired with nvelope.ReadBody to actually decode XML.

Functions

This section is empty.

Types

type Endpoint added in v0.1.0

type Endpoint string

Endpoint is a type that handlers can accepte as an input. It will be the combined URL path without path variables substituted. If you have

mux.Get("/thing/:thingID", handler)

and handler takes an nchi.Endpoint argument, and there is a request for http://example.com/thing/3802, then the nchi.Endpoint will be "/thing/:thingID".

type Mux

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

func NewRouter

func NewRouter(options ...Option) *Mux

func (*Mux) Bind

func (mux *Mux) Bind() error

Bind validates that the injection chains for all routes are valid. If any are not, an error is returned. If you do not call bind, and there are any invalid injection chains, then routes will panic when used.

func (*Mux) Delete

func (mux *Mux) Delete(path string, providers ...interface{})

Delete establish a route for HTTP DELETE requests.

func (*Mux) Get

func (mux *Mux) Get(path string, providers ...interface{})

Get establish a route for HTTP GET requests

func (*Mux) GlobalOPTIONS

func (mux *Mux) GlobalOPTIONS(providers ...interface{})

GlobalOPTIONS sets a handler that is called on automatically on OPTIONS requests. The handler is only called if HandleOPTIONS is true and no OPTIONS handler for the specific path was set. The "Allowed" header is set before calling the handler.

Only the first GobalOPTIONS call counts.

func (*Mux) Group

func (mux *Mux) Group(f func(mux *Mux))

Group establishes a new Mux at the current path but does not inherit any middlewhere.

func (*Mux) Head

func (mux *Mux) Head(path string, providers ...interface{})

Head establish a route for HTTP HEAD requests

func (*Mux) Method

func (mux *Mux) Method(method string, path string, providers ...interface{})

Method registers an endpoint handler at the new path (combined with the current path) using a combination of inherited middleware and the providers here.

func (*Mux) MethodNotAllowed

func (mux *Mux) MethodNotAllowed(providers ...interface{})

MethodNotAllowed sets a handler which is called when a request cannot be routed and HandleMethodNotAllowed is true. If it is not set, http.Error with http.StatusMethodNotAllowed is used. The "Allow" header with allowed request methods is set before the handler is called.

Only the first MethodNotFound call counts.

func (*Mux) NotFound

func (mux *Mux) NotFound(providers ...interface{})

NotFound sets a handler which is called when no matching route is found. If it is not set, http.NotFound is used.

Only the first NotFound call counts.

func (*Mux) Options

func (mux *Mux) Options(path string, providers ...interface{})

Options establish a route for HTTP OPTIONS requests.

func (*Mux) PanicHandler

func (mux *Mux) PanicHandler(providers ...interface{})

PanicHandler sets a handler to handle panics recovered from http handlers. It should be used to generate a error page and return the http error code 500 (Internal Server Error). The handler can be used to keep your server from crashing because of unrecovered panics.

The type RecoverInterface can be used to receive the interface{} that is returned from recover().

Alternatively, use the nvelope.CatchPanic middleware to catch panics.

Only the first PanicHandler call counts.

func (*Mux) Patch

func (mux *Mux) Patch(path string, providers ...interface{})

Patch establish a route for HTTP PATCH requests

func (*Mux) Post

func (mux *Mux) Post(path string, providers ...interface{})

Post establish a route for HTTP POST requests

func (*Mux) Put

func (mux *Mux) Put(path string, providers ...interface{})

Put establish a route for HTTP PUT requests

func (*Mux) Route

func (mux *Mux) Route(path string, f func(mux *Mux))

Route establishes a new Mux at a new path (combined with the current path context).

func (*Mux) ServeFiles

func (mux *Mux) ServeFiles(path string, fs http.FileSystem)

ServeFiles serves files from the given file system root. The path must end with "/*filepath", files are then served from the local path /defined/root/dir/*filepath. For example if root is "/etc" and *filepath is "passwd", the local file "/etc/passwd" would be served. Internally a http.FileServer is used, therefore http.NotFound is used instead of the Router's NotFound handler. To use the operating system's file system implementation, use http.Dir:

Currently, ServeFiles does not use any middleware. That may change in a future release.

func (*Mux) ServeHTTP

func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Mux) Use

func (mux *Mux) Use(providers ...interface{})

Use adds additional http middleware (implementing the http.Handler interface) or nject-style providers to the current handler context. These middleware and providers will be injected into the handler chain for any downstream endpoints that are defined after the call to Use.

func (*Mux) With

func (mux *Mux) With(providers ...interface{}) *Mux

With is just like Use except that it returns a new Mux instead of modifying the current one

type Option

type Option func(*rtr)

func WithHandleMethodNotAllowed

func WithHandleMethodNotAllowed(b bool) Option

WithHandleMethodNotAllowed enables/disables, checking if another method is allowed for the current route, if the current request can not be routed. If this is the case, the request is answered with 'Method Not Allowed' and HTTP status code 405. If no other Method is allowed, the request is delegated to the NotFound handler. The default is: enabled..

func WithHandleOPTIONS

func WithHandleOPTIONS(b bool) Option

WIthHandleOPTIONS enables/disables automatic replies to OPTIONS requests. Custom OPTIONS handlers take priority over automatic replies. The default is: enabled.

func WithRedirectFixedPath

func WithRedirectFixedPath(b bool) Option

WithRedirectFixedPath enables/disables trying to fix the current request path, if no handle is registered for it. First superfluous path elements like ../ or // are removed. Afterwards the router does a case-insensitive lookup of the cleaned path. If a handle can be found for this route, the router makes a redirection to the corrected path with status code 301 for GET requests and 307 for all other request methods. For example /FOO and /..//Foo could be redirected to /foo. RedirectTrailingSlash is independent of this option. The default is: enabled.

func WithRedirectTrailingSlash

func WithRedirectTrailingSlash(b bool) Option

WithRedirectTrailingSlash enables/disables automatic redirection if the current route can't be matched but a handler for the path with (without) the trailing slash exists. For example if /foo/ is requested but a route only exists for /foo, the client is redirected to /foo with http status code 301 for GET requests and 307 for all other request methods. The default is: enabled.

type Params

type Params = httprouter.Params

type RecoverInterface

type RecoverInterface interface{}

Jump to

Keyboard shortcuts

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