Git Product home page Git Product logo

kami's Introduction

kami GoDoc CircleCI

import "github.com/guregu/kami" or import "gopkg.in/guregu/kami.v2"

kami (神) is a tiny web framework using context for request context and httptreemux for routing. It includes a simple system for running hierarchical middleware before and after requests, in addition to log and panic hooks. Graceful restart via einhorn is also supported.

kami is designed to be used as central registration point for your routes, middleware, and context "god object". You are encouraged to use the global functions, but kami supports multiple muxes with kami.New().

You are free to mount kami.Handler() wherever you wish, but a helpful kami.Serve() function is provided.

Here is a presentation about the birth of kami, explaining some of the design choices.

Both context and x/net/context are supported.

Example

A contrived example using kami and context to localize greetings.

Skip ⏩

// Our webserver
package main

import (
	"fmt"
	"net/http"
	"context"

	"github.com/guregu/kami"

	"github.com/my-github/greeting" // see package greeting below
)

func greet(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	hello := greeting.FromContext(ctx)
	name := kami.Param(ctx, "name")
	fmt.Fprintf(w, "%s, %s!", hello, name)
}

func main() {
	ctx := context.Background()
	ctx = greeting.WithContext(ctx, "Hello") // set default greeting
	kami.Context = ctx                       // set our "god context", the base context for all requests

	kami.Use("/hello/", greeting.Guess) // use this middleware for paths under /hello/
	kami.Get("/hello/:name", greet)     // add a GET handler with a parameter in the URL
	kami.Serve()                        // gracefully serve with support for einhorn and systemd
}
// Package greeting stores greeting settings in context.
package greeting

import (
	"net/http"
	"context"

	"golang.org/x/text/language"
)

// For more information about context and why we're doing this,
// see https://blog.golang.org/context
type ctxkey int

var key ctxkey = 0

var greetings = map[language.Tag]string{
	language.AmericanEnglish: "Yo",
	language.Japanese:        "こんにちは",
}

// Guess is kami middleware that examines Accept-Language and sets
// the greeting to a better one if possible.
func Guess(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context {
	if tag, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")); err == nil {
		for _, t := range tag {
			if g, ok := greetings[t]; ok {
				ctx = WithContext(ctx, g)
				return ctx
			}
		}
	}
	return ctx
}

// WithContext returns a new context with the given greeting.
func WithContext(ctx context.Context, greeting string) context.Context {
	return context.WithValue(ctx, key, greeting)
}

// FromContext retrieves the greeting from this context,
// or returns an empty string if missing.
func FromContext(ctx context.Context) string {
	hello, _ := ctx.Value(key).(string)
	return hello
}

Usage

  • Set up routes using kami.Get("/path", handler), kami.Post(...), etc. You can use named parameters or wildcards in URLs like /hello/:name/edit or /files/*path, and access them using the context kami gives you: kami.Param(ctx, "name"). See the routing rules and routing priority. The following kinds of handlers are accepted:
    • types that implement kami.ContextHandler
    • func(context.Context, http.ResponseWriter, *http.Request)
    • types that implement http.Handler
    • func(http.ResponseWriter, *http.Request)
  • All contexts that kami uses are descended from kami.Context: this is the "god object" and the namesake of this project. By default, this is context.Background(), but feel free to replace it with a pre-initialized context suitable for your application.
  • Builds targeting Google App Engine will automatically wrap the "god object" Context with App Engine's per-request Context.
  • Add middleware with kami.Use("/path", kami.Middleware). Middleware runs before requests and can stop them early. More on middleware below.
  • Add afterware with kami.After("/path", kami.Afterware). Afterware runs after requests.
  • Set kami.Cancel to true to automatically cancel all request's contexts after the request is finished. Unlike the standard library, kami does not cancel contexts by default.
  • You can provide a panic handler by setting kami.PanicHandler. When the panic handler is called, you can access the panic error with kami.Exception(ctx).
  • You can also provide a kami.LogHandler that will wrap every request. kami.LogHandler has a different function signature, taking a WriterProxy that has access to the response status code, etc.
  • Use kami.Serve() to gracefully serve your application, or mount kami.Handler() somewhere convenient.

Middleware

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

Middleware differs from a HandlerType in that it returns a new context. You can take advantage of this to build your context by registering middleware at the approriate paths. As a special case, you may return nil to halt execution of the middleware chain.

Middleware is hierarchical. For example, a request for /hello/greg will run middleware registered under the following paths, in order:

  1. /
  2. /hello/
  3. /hello/greg

Within a path, middleware is run in the order of registration.

func init() {
	kami.Use("/", Login)
	kami.Use("/private/", LoginRequired)
}

// Login returns a new context with the appropiate user object inside
func Login(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context {
	if u, err := user.GetByToken(ctx, r.FormValue("auth_token")); err == nil {
		ctx = user.NewContext(ctx, u)
	}
	return ctx
}

// LoginRequired stops the request if we don't have a user object
func LoginRequired(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context {
	if _, ok := user.FromContext(ctx); !ok {
		w.WriteHeader(http.StatusForbidden)
		// ... render 503 Forbidden page
		return nil
	}
	return ctx
}	

Named parameters, wildcards, and middleware

Named parameters and wildcards in middleware are supported now. Middleware registered under a path with a wildcard will run after all hierarchical middleware.

kami.Use("/user/:id/edit", CheckAdminPermissions)  // Matches only /user/:id/edit
kami.Use("/user/:id/edit/*", CheckAdminPermissions)  // Matches all inheriting paths, behaves like non-parameterized paths

Vanilla net/http middleware

kami can use vanilla http middleware as well. kami.Use accepts functions in the form of func(next http.Handler) http.Handler. Be advised that kami will run such middleware in sequence, not in a chain. This means that standard loggers and panic handlers won't work as you expect. You should use kami.LogHandler and kami.PanicHandler instead.

The following example uses goji/httpauth to add HTTP Basic Authentication to paths under /secret/.

import (
	"github.com/goji/httpauth"
	"github.com/guregu/kami"
)

func main() {
	kami.Use("/secret/", httpauth.SimpleBasicAuth("username", "password"))
	kami.Get("/secret/message", secretMessageHandler)
	kami.Serve()
}

Afterware

type Afterware func(context.Context, mutil.WriterProxy, *http.Request) context.Context
func init() {
	kami.After("/", cleanup)
}

Running after the request handler, afterware is useful for cleaning up. Afterware is like a mirror image of middleware. Afterware also runs hierarchically, but in the reverse order of middleware. Wildcards are evaluated before hierarchical afterware.

For example, a request for /hello/greg will run afterware registered under the following paths:

  1. /hello/greg
  2. /hello/
  3. /

This gives afterware under specific paths the ability to use resources that may be closed by /.

Unlike middleware, afterware returning nil will not stop the remaining afterware from being evaluated.

kami.After("/path", afterware) supports many different types of functions, see the docs for kami.AfterwareType for more details.

Independent stacks with *kami.Mux

kami was originally designed to be the "glue" between multiple packages in a complex web application. The global functions and kami.Context are an easy way for your packages to work together. However, if you would like to use kami as an embedded server within another app, serve two separate kami stacks on different ports, or otherwise would like to have an non-global version of kami, kami.New() may come in handy.

Calling kami.New() returns a fresh *kami.Mux, a completely independent kami stack. Changes to kami.Context, paths registered with kami.Get() et al, and global middleware registered with kami.Use() will not affect a *kami.Mux.

Instead, with mux := kami.New() you can change mux.Context, call mux.Use(), mux.Get(), mux.NotFound(), etc.

*kami.Mux implements http.Handler, so you may use it however you'd like!

// package admin is an admin panel web server plugin
package admin

import (
	"net/http"
	"github.com/guregu/kami"
)

// automatically mount our secret admin stuff
func init() {
	mux := kami.New()
	mux.Context = adminContext
	mux.Use("/", authorize)
	mux.Get("/admin/memstats", memoryStats)
	mux.Post("/admin/die", shutdown)
	//  ...
	http.Handle("/admin/", mux)
}

License

MIT

Acknowledgements

kami's People

Contributors

andaru avatar asgaines avatar atotto avatar greenpart avatar guregu avatar rybit avatar someone1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kami's Issues

Some errors have occurred when executing the build with Go 1.9

Hello,

When I executed the build with Go 1.9, some errors have occurred.

vendor/github.com/guregu/kami/handler_17.go:47:2: duplicate case func("context".Context, http.ResponseWriter, *http.Request) in type switch
	previous case at vendor/github.com/guregu/kami/handler_17.go:45:2
vendor/github.com/guregu/kami/middleware_17.go:151:2: duplicate case func("context".Context, http.ResponseWriter, *http.Request) "context".Context in type switch
	previous case at vendor/github.com/guregu/kami/middleware_17.go:149:2
vendor/github.com/guregu/kami/middleware_17.go:207:2: duplicate case func("context".Context, mutil.WriterProxy, *http.Request) "context".Context in type switch
	previous case at vendor/github.com/guregu/kami/middleware_17.go:205:2
vendor/github.com/guregu/kami/middleware_17.go:215:2: duplicate case func("context".Context, *http.Request) "context".Context in type switch
	previous case at vendor/github.com/guregu/kami/middleware_17.go:211:2
vendor/github.com/guregu/kami/middleware_17.go:223:2: duplicate case func("context".Context) "context".Context in type switch
	previous case at vendor/github.com/guregu/kami/middleware_17.go:219:2
vendor/github.com/guregu/kami/middleware_17.go:235:2: duplicate case func("context".Context, http.ResponseWriter, *http.Request) "context".Context in type switch
	previous case at vendor/github.com/guregu/kami/middleware_17.go:231:2

As an example, it appears to be occurring here.
In Go 1.9, is that the treatment of "context" and "netcontext" become equivalent...?

Anyway, I think it may be necessary to add a build tag...

Thanks!

Making kami use a struct so it can be used multiple times?

Hi,

Thank you for your beautiful framework. I was considering building almost exactly the same and then I found kami.

However for my use-case it would be great if kami used a struct. So it could be instantiated multiple times and the router would be local to that instance. We could extend Kami to do this by some rewriting while allowing the current global methods to stay (by using a default struct for the global methods).

Do you think this is a good idea?

Consider changing the router to httptreemux

I was working on porting an old API when I came across some paths that don't play well with httprouter's matching engine. Turns out this is a known issue: julienschmidt/httprouter#73

For example, these two paths collide, even if there's no path under /posts/:id.

  • GET /posts/latest
  • GET /posts/:id/edit

Apparently this will be fixed in httprouter v2, but who knows when that will happen. A solution can be found with httptreemux, which is based on httprouter.

Pros

  • More flexible URL matching

Cons

  • Slightly slower
  • Backwards compatibility might be annoying
  • kami would be removed from httprouter's README.md 😢

If backwards compatibility ends up being impossible, we can consider this for v2.

Allow telling the mux its handler pattern

Hi,

When using kami with its Handler function, or using an independent mux, you currently have to repeat the Handler pattern where the mux is attached to in all its subsequent routes. This, while annoying, also also causes whoever is building the routes to actually know beforehand where the mux itself will be attached. That might not be possible (i.e. if you are building some sort of component that others are meant to use wherever they like).

Instead, give the mux (and kami) the ability to receive the handler pattern:

kami.Pattern = "/admin/"
...
mux.Pattern = "/web/"

and instead of writing "/admin/memstats", just write "/memstats" as the route.

Support middleware hierarchy for wildcards

Hello,

Currently, when registering middleware on paths with wildcards, you have to use the entire path, not just a prefix.

For example:
kami.Use("/users/", CheckAdminPermission) would affect /users/:id/likes/count because it contains the "/users/" prefix.

However:
kami.Use("/users/:id/", CheckAdminPermission) would not affect /users/:id/likes/count because of the wildcard ":id"

Would it be possible to support registering prefixes with wildcards in the future? Could you also update the documentation to say that the full path has to be used for wildcards?

I've also noticed that you can only register one middleware function per path with a wildcard. Is it feasible to add support for registering multiple middleware functions on a single path?

Thanks, and excellent project!

Afterware should follow same rules as middleware

After loading both a middleware and afterware layer to the same path, I noticed that OPTIONS requests were not being fed through the middleware layer, but were being processed by the afterware layer. Since the afterware layer was dependent upon setup in the middleware, this caused errors which required a patch by checking against the request method in the afterware.

The rules for determining the processing of a request should be identical for middleware and afterware.

Go 1.7 changes

kami still works of course, but I'd like to support the now-standard context library.

  • Add support for function signatures with context instead of x/net/context
  • Have middleware update *http.Request's context

I think if I'm careful with build tags we can have support for both without any breaking changes.

Access path params without starting the kami server

Hi -

I am trying to test the methods I have attached to paths, but I'd rather do it without firing up the whole web server.

I have been using the httptest and making my contexts manually but I can't seem to add a path parameter. I think it is because there is no way to add one in the kami api.

In code what I am doing looks like this:

// in the test
 ctx := testContext(token("stranger", "[email protected]", nil))
 recorder := httptest.NewRecorder()
 req, _ := http.NewRequest("GET", fmt.Sprintf("https://not-real/%s", testUser.ID), nil)

// .... test setup
 api.GetThingy(ctx, recorder, req)
/// validate 

and in the api method GetThingy I do a kami.Params(ctx, "id") and the route is defined as kame.Get("/thingy/:id", api.GetThingy)

I could start the webserver up and actually pass through all the middlewares and such, but I'd like to just test the method directly without all the HTTP overhead.

Unwrap path parameters into the Context

Perhaps this is a matter of taste, but I think it would make the package more general if path parameters could (also) be retrieved via the standard ctx.Value(key).(string) idiom, not just through kami.Param(ctx, key).

Info on how to test handlers?

So I have a project written using the kami web framework and I am beginning to write tests for each of my handlers in turn, is there any documentation on best practices for testing and how to use kami handlers during those tests? I have looked specifically at this guide https://elithrar.github.io/article/testing-http-handlers-go/ and was wondering if you had any guides on how specifically to accomplish these styles of functional handler tests with kami.

Routing Group like interface not exposed in router

A Routing Group like interface from the dimfeld/httptreemux package used for kami's router is not exposed on the kami.Mux object. This would simplify larger projects when building URLs.

For example, if I wanted to setup my application with multiple API versions, I could have my code split between server/endpoint logic as follows:

package server - > holds a simple main.go that imports packages from various API imports, sets up a user auth middleware on the context
package api/foo/v1 -> has a LoadRoutes(ctx context.Context, prefix *kami.Mux) function that adds its own group of routes building on the prefix supplied by the prefix router passed in. (e.g. if the group router that was passed in is for '/api/', this adds the group of routes for '/v1/' which when added to the group translates to '/api/v1')
package api/foo/v2 -> has a LoadRoutes(ctx context.Context, prefix *kami.Mux) that is the same as v1 except adds the group of routes for v2

A quick (not thought out) example:
main.go:

package server

import (
    //...

    " api/foo/v1"
    " api/foo/v2"

    "github.com/guregu/kami"
)

func init() {
    // Setup Auth middleware and other things
    kami.Use('/', Login)
    // ... Other things...

    group := kami.NewGroup('/api')
    v1.LoadRoutes(kami.Context, group)
    v2.LoadRoutes(kami.Context, group)
}

api_v1.go:

package v1

import (
    //...
    "github.com/guregu/kami"
)

func LoadRoutes(ctx context.Context, prefix  *kami.Mux) {
    v1Prefix := prefix.NewGroup("/v1")
    v1Prefix.Context = ctx
    v1Prefix.Get('/list', ListHandler)
    // Other routes...
}

api_v2.go:

package v2

import (
    //...
    "github.com/guregu/kami"
)

func LoadRoutes(ctx context.Context, prefix  *kami.Mux) {
    v2Prefix := prefix.NewGroup("/v2")
    v2Prefix.Context = ctx
    v2Prefix.Get('/list', ListHandler)
    // Other routes...
}

I'm interested to hear thoughts on this and see whether or not this feature could be added to kami!

Add kami to TechEmpowered framework benchmarks

I've copied the go-raw/goji implementations and modified it for kami here: https://github.com/someone1/FrameworkBenchmarks/tree/master/frameworks/Go/kami

I was wondering if there was any interest in tweaking this so that it uses kami in a more "production" kind of way. As you can see I added a middleware to add the Server response header vs setting per handler as it seems like the way you'd tackle this in any framework. I don't know if there's really more to do here but I thought I'd ask before putting in a PR. I don't really use the context object in anyway, would it make sense to use it to get the database object vs using a global variable in an actual production application?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.