~samwhited/mux

f774f2cc1f14f5fcebb4dfe220e56139ef400345 — Sam Whited 1 year, 1 month ago f1d7599
mux: add API to simplify path normalization
7 files changed, 174 insertions(+), 41 deletions(-)

M example_test.go
M mux.go
M node.go
A norm.go
M options.go
M params.go
M params_test.go
M example_test.go => example_test.go +8 -3
@@ 41,10 41,13 @@ func Example_normalization() {
			// golang.org/x/text/secure/precis package instead of lowercasing.
			normalized := strings.ToLower(username.Raw)

			// If the username had any capital letters, redirect to the canonical
			// username.
			// If the username is not canonical, redirect.
			if normalized != username.Raw {
				newPath := r.URL.Path[:username.Offset] + normalized + r.URL.Path[username.Offset+uint(len(username.Raw)):]
				r = mux.WithParameter(r, username.Name, normalized)
				newPath, err := mux.CanonicalPath(r)
				if err != nil {
					panic(fmt.Errorf("mux_test: error creating canonicalized path: %w", err))
				}
				http.Redirect(w, r, newPath, http.StatusPermanentRedirect)
				return
			}


@@ 55,6 58,8 @@ func Example_normalization() {
	)

	server := httptest.NewServer(m)
	defer server.Close()

	resp, err := http.Get(server.URL + "/profile/Me/personal")
	if err != nil {
		panic(err)

M mux.go => mux.go +13 -2
@@ 46,12 46,17 @@
package mux // import "code.soquee.net/mux"

import (
	"context"
	"fmt"
	"net/http"
	"path"
	"strings"
)

// ctxRoute is a type used as the context key when storing a route on the HTTP
// context for future use.
type ctxRoute struct{}

const (
	typStatic = "static"
	typWild   = "path"


@@ 144,6 149,8 @@ func (mux *ServeMux) handler(r *http.Request) (http.Handler, *http.Request) {
			}
			return mux.notFound, r
		}

		r = r.WithContext(context.WithValue(r.Context(), ctxRoute{}, mux.node.route))
		return h, r
	}



@@ 155,7 162,7 @@ nodeloop:
		if len(node.child) == 1 && node.child[0].typ != typStatic {
			var part, remain string
			part, remain, r = node.child[0].match(path, offset, r)
			offset += uint(len(part)) + 1
			offset++

			// If the type doesn't match, we're done.
			if part == "" {


@@ 175,6 182,8 @@ nodeloop:
					}
					return mux.notFound, r
				}

				r = r.WithContext(context.WithValue(r.Context(), ctxRoute{}, node.child[0].route))
				return h, r
			}
			node = &node.child[0]


@@ 186,7 195,7 @@ nodeloop:
		for _, child := range node.child {
			var part, remain string
			part, remain, r = child.match(path, offset, r)
			offset += uint(len(part)) + 1
			offset++
			// The child did not match, so check the next.
			if part == "" {
				path = remain


@@ 206,6 215,8 @@ nodeloop:
					}
					return mux.notFound, r
				}

				r = r.WithContext(context.WithValue(r.Context(), ctxRoute{}, child.route))
				return h, r
			}


M node.go => node.go +7 -5
@@ 9,6 9,7 @@ import (
type node struct {
	name     string
	typ      string
	route    string
	handlers map[string]http.Handler

	child []node


@@ 65,11 66,12 @@ func (n *node) match(path string, offset uint, r *http.Request) (part string, re
func addValue(r *http.Request, name, typ, raw string, offset uint, val interface{}) *http.Request {
	if name != "" {
		pinfo := ParamInfo{
			Value:  val,
			Raw:    raw,
			Name:   name,
			Type:   typ,
			Offset: offset,
			Value: val,
			Raw:   raw,
			Name:  name,
			Type:  typ,

			offset: offset,
		}
		return r.WithContext(context.WithValue(r.Context(), ctxParam(name), pinfo))
	}

A norm.go => norm.go +95 -0
@@ 0,0 1,95 @@
package mux

import (
	"context"
	"errors"
	"net/http"
	"strings"
)

var (
	errNoRoute = errors.New("mux: no route was found in the context")
	errNoParam = errors.New("mux: context was missing an expected parameter")
)

// WithParam returns a shallow copy of r with a new context that shadows the
// given route parameter.
// If the parameter does not exist, the original request is returned unaltered.
//
// Because WithParameter is used to normalize request parameters after the route
// has already been resolved, all replaced parameters are of type string.
func WithParameter(r *http.Request, name, val string) *http.Request {
	pinfo, ok := Param(r, name)
	if !ok {
		return r
	}

	pinfo.Value = val
	pinfo.Raw = val
	pinfo.Type = typString
	return r.WithContext(context.WithValue(r.Context(), ctxParam(name), pinfo))
}

// CanonicalPath returns the request path by applying the route parameters found
// in the context to the route used to match the given request.
// This value may be different from r.URL.Path if some form of normalization has
// been applied to a route parameter, in which case the user may choose to issue
// a redirect to the canonical path.
func CanonicalPath(r *http.Request) (string, error) {
	route := r.Context().Value(ctxRoute{}).(string)
	if route == "" {
		return "", errNoRoute
	}
	hasTrailingSlash := strings.HasSuffix(route, "/")
	oldPath := strings.TrimPrefix(r.URL.Path, "/")

	var canonicalPath strings.Builder
	// Give us a comfortable capacity so that we have to resize the buffer less
	// often.
	canonicalPath.Grow(len(route))

	for {
		var component, pathComponent string
		pathComponent, oldPath = nextPart(oldPath)

		component, route = nextPart(route)
		if component == "" {
			// Add back any trailing slash consumed by nextPart.
			if hasTrailingSlash {
				err := canonicalPath.WriteByte('/')
				if err != nil {
					return "", err
				}
			}
			break
		}
		err := canonicalPath.WriteByte('/')
		if err != nil {
			return "", err
		}
		name, typ := parseParam(component)
		switch {
		case typ == typStatic:
			_, err = canonicalPath.WriteString(name)
			if err != nil {
				return "", err
			}
		case name == "":
			_, err = canonicalPath.WriteString(pathComponent)
			if err != nil {
				return "", err
			}
		default:
			pinfo, ok := Param(r, name)
			if !ok {
				return "", errNoParam
			}
			_, err = canonicalPath.WriteString(pinfo.Raw)
			if err != nil {
				return "", err
			}
		}
	}

	return canonicalPath.String(), nil
}

M options.go => options.go +3 -0
@@ 82,6 82,7 @@ func Handle(method, r string, h http.Handler) Option {
			if _, ok := pointer.handlers[method]; ok {
				panic(fmt.Sprintf(alreadyRegistered, method, r))
			}
			pointer.route = r
			pointer.handlers[method] = h
			return
		}


@@ 117,6 118,7 @@ func Handle(method, r string, h http.Handler) Option {
						// If this is the path we want to register and no handler has been
						// registered for it, add one:
						if _, ok := child.handlers[method]; !ok {
							pointer.child[i].route = r
							pointer.child[i].handlers[method] = h
							continue pathloop
						} else {


@@ 138,6 140,7 @@ func Handle(method, r string, h http.Handler) Option {
				handlers: make(map[string]http.Handler),
			}
			if remain == "" {
				n.route = r
				n.handlers[method] = h
			}


M params.go => params.go +5 -3
@@ 19,9 19,11 @@ type ParamInfo struct {
	// Type type of the route component that the parameter was matched against
	// (for example "int" in "{name int}")
	Type string
	// The offset in the path where this parameter was found (for example if "10"
	// is parsed out of the path "/10" the offset is 1)
	Offset uint

	// offset is the number of the component in the route. Eg. a param foo in the
	// route /{foo int} has offset 1 (zero being the root node, which is never a
	// parameter).
	offset uint
}

// Param returns the named route parameter from the requests context.

M params_test.go => params_test.go +43 -28
@@ 3,7 3,6 @@ package mux_test
import (
	"net/http"
	"net/http/httptest"
	"reflect"
	"strconv"
	"testing"



@@ 22,32 21,28 @@ var paramsTests = [...]struct {
		path:   "/user/123/-11/me/1.123",
		params: []mux.ParamInfo{
			{
				Value:  uint64(123),
				Raw:    "123",
				Name:   "account",
				Type:   "uint",
				Offset: 6,
				Value: uint64(123),
				Raw:   "123",
				Name:  "account",
				Type:  "uint",
			},
			{
				Value:  int64(-11),
				Raw:    "-11",
				Name:   "user",
				Type:   "int",
				Offset: 10,
				Value: int64(-11),
				Raw:   "-11",
				Name:  "user",
				Type:  "int",
			},
			{
				Value:  "me",
				Raw:    "me",
				Name:   "name",
				Type:   "string",
				Offset: 14,
				Value: "me",
				Raw:   "me",
				Name:  "name",
				Type:  "string",
			},
			{
				Value:  float64(1.123),
				Raw:    "1.123",
				Name:   "f",
				Type:   "float",
				Offset: 17,
				Value: float64(1.123),
				Raw:   "1.123",
				Name:  "f",
				Type:  "float",
			},
		},
	},


@@ 61,11 56,10 @@ var paramsTests = [...]struct {
		path:   "/one/two/three",
		params: []mux.ParamInfo{
			{
				Value:  "two/three",
				Raw:    "two/three",
				Name:   "other",
				Type:   "path",
				Offset: 5,
				Value: "two/three",
				Raw:   "two/three",
				Name:  "other",
				Type:  "path",
			},
		},
	},


@@ 94,6 88,10 @@ var paramsTests = [...]struct {
		routes: []string{"unclean/../path"},
		panics: true,
	},
	9: {
		routes: []string{"/{}/"},
		path:   "/b/",
	},
}

// Used as an HTTP status code code to make sure the test path matches at


@@ 106,6 104,14 @@ const (

func paramsHandler(t *testing.T, params []mux.ParamInfo) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		p, err := mux.CanonicalPath(r)
		if err != nil {
			t.Errorf("Error while generating canonical path: %v", err)
		}
		if p != r.URL.Path {
			t.Errorf("Unexpected path generated from context: want=%q, got=%q", r.URL.Path, p)
		}

		w.WriteHeader(testStatusCode)
		for _, v := range params {
			pinfo, ok := mux.Param(r, v.Name)


@@ 113,8 119,17 @@ func paramsHandler(t *testing.T, params []mux.ParamInfo) http.HandlerFunc {
				t.Errorf("No such parameter found %q", v.Name)
				continue
			}
			if !reflect.DeepEqual(pinfo, v) {
				t.Errorf("Param do not match: want=%+v, got=%+v)", v, pinfo)
			if pinfo.Value != v.Value {
				t.Errorf("Param values do not match: want=%v, got=%v)", v.Value, pinfo.Value)
			}
			if pinfo.Raw != v.Raw {
				t.Errorf("Param raw values do not match: want=%q, got=%q)", v.Raw, pinfo.Raw)
			}
			if pinfo.Name != v.Name {
				t.Errorf("Param names do not match: want=%q, got=%q)", v.Name, pinfo.Name)
			}
			if pinfo.Type != v.Type {
				t.Errorf("Param types do not match: want=%s, got=%s)", v.Type, pinfo.Type)
			}
		}
	}