Browse Source

A wild web framework appears

They say that every programmer builds a web framework at some point. This one is
mine.

The basic idea behind this one is that I wanted a Sinatra for Go, and I couldn't
find one anywhere. Furthermore, net/http is in many ways really close to what I
want out of a Sinatra-in-Go, and many of the frameworks I did find seemed to
reinvent too much, or were incompatible with net/http in weird ways, or used too
much questionable reflection magic. So long story short, I wrote my own.

This implementation is only half-baked, and among other things it's missing a
whole lot of tests.
Carl Jackson 12 years ago
parent
commit
ed438ca532
5 changed files with 782 additions and 0 deletions
  1. +184
    -0
      web/middleware.go
  2. +78
    -0
      web/mux.go
  3. +121
    -0
      web/pattern.go
  4. +277
    -0
      web/router.go
  5. +122
    -0
      web/web.go

+ 184
- 0
web/middleware.go View File

@ -0,0 +1,184 @@
package web
import (
"fmt"
"log"
"net/http"
"sync"
)
// Maximum size of the pool of spare middleware stacks
const mPoolSize = 32
type mLayer struct {
fn func(*C, http.Handler) http.Handler
name string
}
type mStack struct {
lock sync.Mutex
stack []mLayer
pool chan *cStack
router Handler
}
// Constructing a middleware stack involves a lot of allocations: at the very
// least each layer will have to close over the layer after (inside) it, and
// perhaps a context object. Instead of doing this on every request, let's cache
// fully assembled middleware stacks (the "c" stands for "cached").
type cStack struct {
C
m http.Handler
}
func (s *cStack) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.C = C{}
s.m.ServeHTTP(w, r)
}
func (s *cStack) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
s.C = c
s.m.ServeHTTP(w, r)
}
func (m *mStack) appendLayer(name string, fn interface{}) {
var ml mLayer
ml.name = name
switch fn.(type) {
case func(http.Handler) http.Handler:
unwrapped := fn.(func(http.Handler) http.Handler)
ml.fn = func(c *C, h http.Handler) http.Handler {
return unwrapped(h)
}
case func(*C, http.Handler) http.Handler:
ml.fn = fn.(func(*C, http.Handler) http.Handler)
default:
log.Fatalf(`Unknown middleware type %v. Expected a function `+
`with signature "func(http.Handler) http.Handler" or `+
`"func(*web.C, http.Handler) http.Handler".`, fn)
}
m.stack = append(m.stack, ml)
}
func (m *mStack) findLayer(name string) int {
for i, middleware := range m.stack {
if middleware.name == name {
return i
}
}
return -1
}
func (m *mStack) invalidate() {
old := m.pool
m.pool = make(chan *cStack, mPoolSize)
close(old)
for _ = range old {
}
}
func (m *mStack) newStack() *cStack {
m.lock.Lock()
defer m.lock.Unlock()
cs := cStack{}
router := m.router
cs.m = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
router.ServeHTTPC(cs.C, w, r)
})
for i := len(m.stack) - 1; i >= 0; i-- {
cs.m = m.stack[i].fn(&cs.C, cs.m)
}
return &cs
}
func (m *mStack) alloc() *cStack {
select {
case cs := <-m.pool:
if cs == nil {
return m.alloc()
}
return cs
default:
return m.newStack()
}
}
func (m *mStack) release(cs *cStack) {
// It's possible that the pool has been invalidated and therefore
// closed, in which case we'll start panicing, which is dumb. I'm not
// sure this is actually better than just grabbing a lock, but whatever.
defer func() {
recover()
}()
select {
case m.pool <- cs:
default:
}
}
// Append the given middleware to the middleware stack. See the documentation
// for type Mux for a list of valid middleware types.
//
// No attempt is made to enforce the uniqueness of middleware names.
func (m *mStack) Use(name string, middleware interface{}) {
m.lock.Lock()
defer m.lock.Unlock()
m.appendLayer(name, middleware)
m.invalidate()
}
// Insert the given middleware immediately before a given existing middleware in
// the stack. See the documentation for type Mux for a list of valid middleware
// types. Returns an error if no middleware has the name given by "before."
//
// No attempt is made to enforce the uniqueness of names. If the insertion point
// is ambiguous, the first (outermost) one is chosen.
func (m *mStack) Insert(name string, middleware interface{}, before string) error {
m.lock.Lock()
defer m.lock.Unlock()
i := m.findLayer(before)
if i < 0 {
return fmt.Errorf("web: unknown middleware %v", before)
}
m.appendLayer(name, middleware)
inserted := m.stack[len(m.stack)-1]
copy(m.stack[i+1:], m.stack[i:])
m.stack[i] = inserted
m.invalidate()
return nil
}
// Remove the given named middleware from the middleware stack. Returns an error
// if there is no middleware by that name.
//
// If the name of the middleware to delete is ambiguous, the first (outermost)
// one is chosen.
func (m *mStack) Abandon(name string) error {
m.lock.Lock()
defer m.lock.Unlock()
i := m.findLayer(name)
if i < 0 {
return fmt.Errorf("web: unknown middleware %v", name)
}
copy(m.stack[i:], m.stack[i+1:])
m.stack = m.stack[:len(m.stack)-1 : len(m.stack)]
m.invalidate()
return nil
}
// Returns a list of middleware currently in use.
func (m *mStack) Middleware() []string {
m.lock.Lock()
defer m.lock.Unlock()
stack := make([]string, len(m.stack))
for i, ml := range m.stack {
stack[i] = ml.name
}
return stack
}

+ 78
- 0
web/mux.go View File

@ -0,0 +1,78 @@
package web
import (
"net/http"
)
/*
An HTTP multiplexer, much like net/http's ServeMux.
Routes may be added using any of the various HTTP-method-specific functions.
When processing a request, when iterating in insertion order the first route
that matches both the request's path and method is used.
There are two other differences worth mentioning between web.Mux and
http.ServeMux. First, string patterns (i.e., Sinatra-like patterns) must match
exactly: the "rooted subtree" behavior of ServeMux is not implemented. Secondly,
unlike ServeMux, Mux does not support Host-specific patterns.
If you require any of these features, remember that you are free to mix and
match muxes at any part of the stack.
In order to provide a sane API, many functions on Mux take interface{}'s. This
is obviously not a very satisfying solution, but it's probably the best we can
do for now. Instead of duplicating documentation on each method, the types
accepted by those functions are documented here.
A middleware (the untyped parameter in Use() and Insert()) must be one of the
following types:
- func(http.Handler) http.Handler
- func(c *web.C, http.Handler) http.Handler
All of the route-adding functions on Mux take two untyped parameters: pattern
and handler. Pattern must be one of the following types:
- string (interpreted as a Sinatra pattern)
- regexp.Regexp
- web.Pattern
Handler must be one of the following types:
- http.Handler
- web.Handler
- func(w http.ResponseWriter, r *http.Request)
- func(c web.C, w http.ResponseWriter, r *http.Request)
*/
type Mux struct {
mStack
router
}
// Create a new Mux without any routes or middleware.
func New() *Mux {
mux := Mux{
mStack: mStack{
stack: make([]mLayer, 0),
pool: make(chan *cStack),
},
router: router{
routes: make([]route, 0),
notFound: parseHandler(http.NotFound),
},
}
mux.mStack.router = HandlerFunc(mux.router.route)
return &mux
}
// Serve a request with the given Mux. Satisfies the http.Handler interface.
func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
stack := m.mStack.alloc()
defer m.mStack.release(stack)
stack.ServeHTTP(w, r)
}
// Serve a context dependent request with the given Mux. Satisfies the
// web.Handler interface.
func (m *Mux) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
stack := m.mStack.alloc()
defer m.mStack.release(stack)
stack.ServeHTTPC(c, w, r)
}

+ 121
- 0
web/pattern.go View File

@ -0,0 +1,121 @@
package web
import (
"fmt"
"net/http"
"regexp"
"strings"
)
type regexpPattern struct {
re *regexp.Regexp
names []string
}
func (p regexpPattern) Prefix() string {
prefix, _ := p.re.LiteralPrefix()
return prefix
}
func (p regexpPattern) Match(r *http.Request, c *C) bool {
matches := p.re.FindStringSubmatch(r.URL.Path)
if matches == nil {
return false
}
if c.UrlParams == nil && len(matches) > 0 {
c.UrlParams = make(map[string]string, len(matches)-1)
}
for i := 1; i < len(matches); i++ {
c.UrlParams[p.names[i]] = matches[i]
}
return true
}
func parseRegexpPattern(re *regexp.Regexp) regexpPattern {
rnames := re.SubexpNames()
// We have to make our own copy since package regexp forbids us
// from scribbling over the slice returned by SubexpNames().
names := make([]string, len(rnames))
for i, rname := range rnames {
if rname == "" {
rname = fmt.Sprintf("$%d", i)
}
names[i] = rname
}
return regexpPattern{
re: re,
names: names,
}
}
type stringPattern struct {
raw string
pats []string
literals []string
isPrefix bool
}
func (s stringPattern) Prefix() string {
return s.literals[0]
}
func (s stringPattern) Match(r *http.Request, c *C) bool {
path := r.URL.Path
matches := make([]string, len(s.pats))
for i := 0; i < len(s.pats); i++ {
if !strings.HasPrefix(path, s.literals[i]) {
return false
}
path = path[len(s.literals[i]):]
m := strings.IndexRune(path, '/')
if m == -1 {
m = len(path)
}
if m == 0 {
// Empty strings are not matches, otherwise routes like
// "/:foo" would match the path "/"
return false
}
matches[i] = path[:m]
path = path[m:]
}
// There's exactly one more literal than pat.
if s.isPrefix {
if strings.HasPrefix(path, s.literals[len(s.pats)]) {
return false
}
} else {
if path != s.literals[len(s.pats)] {
return false
}
}
if c.UrlParams == nil && len(matches) > 0 {
c.UrlParams = make(map[string]string, len(matches)-1)
}
for i, match := range matches {
c.UrlParams[s.pats[i]] = match
}
return true
}
func parseStringPattern(s string, isPrefix bool) stringPattern {
matches := patternRe.FindAllStringSubmatchIndex(s, -1)
pats := make([]string, len(matches))
literals := make([]string, len(matches)+1)
n := 0
for i, match := range matches {
a, b := match[2], match[3]
literals[i] = s[n : a-1] // Need to leave off the colon
pats[i] = s[a:b]
n = b
}
literals[len(matches)] = s[n:]
return stringPattern{
raw: s,
pats: pats,
literals: literals,
isPrefix: isPrefix,
}
}

+ 277
- 0
web/router.go View File

@ -0,0 +1,277 @@
package web
import (
"log"
"net/http"
"regexp"
"strings"
"sync"
)
type method int
const (
mCONNECT method = 1 << iota
mDELETE
mGET
mHEAD
mOPTIONS
mPATCH
mPOST
mPUT
mTRACE
// We only natively support the methods above, but we pass through other
// methods. This constant pretty much only exists for the sake of mALL.
mIDK
mALL method = mCONNECT | mDELETE | mGET | mHEAD | mOPTIONS | mPATCH |
mPOST | mPUT | mTRACE | mIDK
)
type route struct {
// Theory: most real world routes have a string prefix which is both
// cheap(-ish) to test against and pretty selective. And, conveniently,
// both regexes and string patterns give us this out-of-box.
prefix string
method method
pattern Pattern
handler Handler
}
type router struct {
lock sync.Mutex
routes []route
notFound Handler
}
// A pattern determines whether or not a given request matches some criteria.
// They are often used in routes, which are essentially (pattern, methodSet,
// handler) tuples. If the method and pattern match, the given handler is used.
//
// Built-in implementations of this interface are used to implement regular
// expression and string matching.
type Pattern interface {
// In practice, most real-world routes have a string prefix that can be
// used to quickly determine if a pattern is an eligible match. The
// router uses the result of this function to optimize away calls to the
// full Match function, which is likely much more expensive to compute.
// If your Pattern does not support prefixes, this function should
// return the empty string.
Prefix() string
// Returns true if the request satisfies the pattern. This function is
// free to examine both the request and the context to make this
// decision. After it is certain that the request matches, this function
// should mutate or create c.UrlParams if necessary.
Match(r *http.Request, c *C) bool
}
var patternRe = regexp.MustCompile(`/:([^/]+)`)
func parsePattern(p interface{}, isPrefix bool) Pattern {
switch p.(type) {
case Pattern:
return p.(Pattern)
case *regexp.Regexp:
return parseRegexpPattern(p.(*regexp.Regexp))
case string:
return parseStringPattern(p.(string), isPrefix)
default:
log.Fatalf("Unknown pattern type %v. Expected a web.Pattern, "+
"regexp.Regexp, or a string.", p)
}
panic("log.Fatalf does not return")
}
type netHttpWrap struct {
http.Handler
}
func (h netHttpWrap) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Handler.ServeHTTP(w, r)
}
func (h netHttpWrap) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
h.Handler.ServeHTTP(w, r)
}
func parseHandler(h interface{}) Handler {
switch h.(type) {
case Handler:
return h.(Handler)
case http.Handler:
return netHttpWrap{h.(http.Handler)}
case func(c C, w http.ResponseWriter, r *http.Request):
f := h.(func(c C, w http.ResponseWriter, r *http.Request))
return HandlerFunc(f)
case func(w http.ResponseWriter, r *http.Request):
f := h.(func(w http.ResponseWriter, r *http.Request))
return netHttpWrap{http.HandlerFunc(f)}
default:
log.Fatalf("Unknown handler type %v. Expected a web.Handler, "+
"a http.Handler, or a function with signature func(C, "+
"http.ResponseWriter, *http.Request) or "+
"func(http.ResponseWriter, http.Request)", h)
}
panic("log.Fatalf does not return")
}
func httpMethod(mname string) method {
switch mname {
case "CONNECT":
return mCONNECT
case "DELETE":
return mDELETE
case "GET":
return mGET
case "HEAD":
return mHEAD
case "OPTIONS":
return mOPTIONS
case "PATCH":
return mPATCH
case "POST":
return mPOST
case "PUT":
return mPUT
case "TRACE":
return mTRACE
default:
return mIDK
}
}
func (rt *router) route(c C, w http.ResponseWriter, r *http.Request) {
m := httpMethod(r.Method)
for _, route := range rt.routes {
if route.method&m == 0 ||
!strings.HasPrefix(r.URL.Path, route.prefix) ||
!route.pattern.Match(r, &c) {
continue
}
route.handler.ServeHTTPC(c, w, r)
return
}
rt.notFound.ServeHTTPC(c, w, r)
}
func (rt *router) handleUntyped(p interface{}, m method, h interface{}) {
pat := parsePattern(p, false)
rt.handle(pat, m, parseHandler(h))
}
func (rt *router) handle(p Pattern, m method, h Handler) {
// We're being a little sloppy here: we assume that pointer assignments
// are atomic, and that there is no way a locked append here can affect
// another goroutine which looked at rt.routes without a lock.
rt.lock.Lock()
defer rt.lock.Unlock()
rt.routes = append(rt.routes, route{
prefix: p.Prefix(),
method: m,
pattern: p,
handler: h,
})
}
// This is a bit silly, but I've renamed the method receivers in the public
// functions here "m" instead of the standard "rt", since they will eventually
// be shown on the documentation for the Mux that they are included in.
// Dispatch to the given handler when the pattern matches, regardless of HTTP
// method. See the documentation for type Mux for a description of what types
// are accepted for pattern and handler.
func (m *router) Handle(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mALL, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// CONNECT. See the documentation for type Mux for a description of what types
// are accepted for pattern and handler.
func (m *router) Connect(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mCONNECT, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// DELETE. See the documentation for type Mux for a description of what types
// are accepted for pattern and handler.
func (m *router) Delete(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mDELETE, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// GET. See the documentation for type Mux for a description of what types are
// accepted for pattern and handler.
func (m *router) Get(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mGET, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// HEAD. See the documentation for type Mux for a description of what types are
// accepted for pattern and handler.
func (m *router) Head(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mHEAD, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// OPTIONS. See the documentation for type Mux for a description of what types
// are accepted for pattern and handler.
func (m *router) Options(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mOPTIONS, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// PATCH. See the documentation for type Mux for a description of what types are
// accepted for pattern and handler.
func (m *router) Patch(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mPATCH, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// POST. See the documentation for type Mux for a description of what types are
// accepted for pattern and handler.
func (m *router) Post(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mPOST, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// PUT. See the documentation for type Mux for a description of what types are
// accepted for pattern and handler.
func (m *router) Put(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mPUT, handler)
}
// Dispatch to the given handler when the pattern matches and the HTTP method is
// TRACE. See the documentation for type Mux for a description of what types are
// accepted for pattern and handler.
func (m *router) Trace(pattern interface{}, handler interface{}) {
m.handleUntyped(pattern, mTRACE, handler)
}
/*
Dispatch to the given handler when the given (Sinatra-like) pattern matches a
prefix of the path. This function explicitly takes a string parameter since you
can implement this behavior with a regular expression using the standard
Handle() function.
This function is probably most helpful when implementing sub-routing: an admin
application, for instance, can expose a single handler, and can be hooked up all
at once by attaching a sub-route at "/admin".
Notably, this function does not strip the matched prefix from downstream
handlers, so in the above example the handler would recieve requests with path
"/admin/foo/bar", for example, instead of just "/foo/bar". Luckily, this is a
problem easily surmountable by middleware.
See the documentation for type Mux for a description of what types are accepted
for handler.
*/
func (m *router) Sub(pattern string, handler interface{}) {
pat := parsePattern(pattern, true)
m.handle(pat, mALL, parseHandler(handler))
}
// Set the fallback (i.e., 404) handler for this mux. See the documentation for
// type Mux for a description of what types are accepted for handler.
func (m *router) NotFound(handler interface{}) {
m.notFound = parseHandler(handler)
}

+ 122
- 0
web/web.go View File

@ -0,0 +1,122 @@
/*
Package web is a microframework inspired by Sinatra.
The underlying philosophy behind this package is that net/http is a very good
HTTP library which is only missing a few features. If you disagree with this
statement (e.g., you think that the interfaces it exposes are not especially
good, or if you're looking for a comprehensive "batteries included" feature
list), you're likely not going to have a good time using this library. In that
spirit, we have attempted wherever possible to be compatible with net/http. You
should be able to insert any net/http compliant handler into this library, or
use this library with any other net/http compliant mux.
This package attempts to solve three problems that net/http does not. First, it
allows you to specify URL patterns with Sinatra-like named wildcards and
regexps. Second, it allows you to write reconfigurable middleware stacks. And
finally, it allows you to attach additional context to requests, in a manner
that can be manipulated by both compliant middleware and handlers.
A usage example:
m := web.New()
Use your favorite HTTP verbs:
var legacyFooHttpHandler http.Handler // From elsewhere
m.Get("/foo", legacyFooHttpHandler)
m.Post("/bar", func(w http.ResponseWriter, r *http.Request) {
w.Write("Hello world!")
})
Bind parameters using either Sinatra-like patterns or regular expressions:
m.Get("/hello/:name", func(c *web.C, w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", c.UrlParams["name"])
})
pattern := regexp.MustCompile(`^/ip/(?P<ip>(?:\d{1,3}\.){3}\d{1,3})$`)
m.Get(pattern, func(c *web.C, w http.ResponseWriter, r *http.Request) {
fmt.Printf(w, "Info for IP address %s:", c.UrlParams["ip"])
})
Middleware are functions that wrap http.Handlers, just like you'd use with raw
net/http. Middleware functions can optionally take a context parameter, which
will be threaded throughout the middleware stack and to the final handler, even
if not all of these things do not support contexts. Middleware are encouraged to
use the Env parameter to pass data to other middleware and to the final handler:
m.Use(func(h http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) {
log.Println("Before request")
h.ServeHTTP(w, r)
log.Println("After request")
}
return http.HandlerFunc(handler)
})
m.Use(func(c *web.C, h http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("user")
if err == nil {
c.Env["user"] = cookie.Raw
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(handler)
})
m.Get("/baz", func(c web.C, w http.ResponseWriter, r *http.Request) {
if user, ok := c.Env["user"], ok {
w.Write("Hello " + string(user))
} else {
w.Write("Hello Stranger!")
}
})
*/
package web
import (
"net/http"
)
/*
Per-request context object. Threaded through all compliant middleware layers and
to the final request handler.
As an implementation detail, references to these structs are reused between
requests to reduce allocation churn, but the maps they contain are created fresh
on every request. If you are closing over a context (especially relevant for
middleware), you should not close over either the UrlParams or Env objects,
instead accessing them through the context whenever they are required.
*/
type C struct {
// The parameters parsed by the mux from the URL itself. In most cases,
// will contain a map from programmer-specified identifiers to the
// strings that matched those identifiers, but if a unnamed regex
// capture is used, it will be assigned to the special identifiers "$1",
// "$2", etc.
UrlParams map[string]string
// A free-form environment, similar to Rack or PEP 333's environments.
// Middleware layers are encouraged to pass data to downstream layers
// and other handlers using this map, and are even more strongly
// encouraged to document and maybe namespace they keys they use.
Env map[string]interface{}
}
// A superset of net/http's http.Handler, which also includes a mechanism for
// serving requests with a context. If your handler does not support the use of
// contexts, we encourage you to use http.Handler instead.
type Handler interface {
http.Handler
ServeHTTPC(C, http.ResponseWriter, *http.Request)
}
// Like net/http's http.HandlerFunc, but supports a context object. Implements
// both http.Handler and web.Handler free of charge.
type HandlerFunc func(C, http.ResponseWriter, *http.Request)
func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h(C{}, w, r)
}
func (h HandlerFunc) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
h(c, w, r)
}

Loading…
Cancel
Save