~samwhited/mux

bc55fd930357a6a02deb832bc1ef704b5f1be6a8 — Sam Whited 1 year, 18 days ago 40b4763
design: add existing design documents to the repo

Previously these design documents were floating around on my hard drive.
To make sure I don't lose them, I have added them to the repo. This will
also hopefuly encourage drive-by patches to be more carefully thought
out since a lot of complexity is hidden behind the type system and the
relatively simple route matching rules in this muxer.
3 files changed, 196 insertions(+), 0 deletions(-)

A design/README.md
A design/mux_norm.md
A design/relaxed_path_matching.md
A design/README.md => design/README.md +6 -0
@@ 0,0 1,6 @@
# Mux Design Documents

This directory contains some design thoughts that I wanted to save while
designing this package. It is not meant to be a formal proposal process, but
feel free to submit patches adding new proposals using a similar format if
desired.

A design/mux_norm.md => design/mux_norm.md +102 -0
@@ 0,0 1,102 @@
# Proposal: Normalization of Route Parameters

**Author(s):** Sam Whited  
**Last updated:** 2020-01-02  
**Status:** implemented

## Abstract

A proposal to simplify normalization of route parameters by including such
functionality in the libraries public API instead of requiring that users
implement it themselves.

## Background

On the popular code hosting service GitHub the following URLs
resolve to the same resource:

 - https://github.com/mellium/xmpp
 - https://github.com/Mellium/xmpp
 - https://github.com/melLiUm/xMPP

It is generally accepted that this is not a good idea [citation needed].
Instead, it is desirable to have a single canonical URL per resource and, if
necessary, redirect non-canonical URLs to their canonical form.
Writing code to do this is not difficult, but it can be tedious and error prone.
Having an HTTP multiplexer specifically provide an API for route normalization
could make this easier, and hopefully more commonplace.


## Requirements

The [Go] multiplexer [`code.soquee.net/mux`] supports matching typed route
parameters that are stored on Go's request [context], and strives to export a
minimal public API.
Changes adding support for path normalization must meet the following design
requirements:

 - Introduce as few new public identifiers as possible
 - Allow replacing multiple route parameters with normalized values given a
   single [`Request`] value
 - Do not perform any mutation of data that could alter other code's view of
   the request, route, or request parameters
 - Do not require that the module (which practices [semver]) undergo a major
   version bump


## Proposal

The following API, which introduces two new identifiers that must meet the mux
module's compatibility guarantees, is proposed:

```go
// 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.
func WithParam(r *http.Request, name, val string) *http.Request {/* … */}

// Path 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 Path(r *http.Request) (string, error) {/* … */}
```

Because normalization of route parameters happens after route matching, the type
is no longer useful and `WithParam` may always set a string type.
If this assumption ends up being wrong, the `val` parameter could be changed to
an empty interface and `WithParam` could do runtime type checking, however,
this would require some sort of error return value in the case of a type that
is not supported and complicates use of the API.
The trade-offs will hopefully become more apparent before version 1.0 when this
API is locked in.

Implementing this API makes the [`ParamInfo.Offset`] field unnecessary.
Because the module has not yet reached version 1.0, this field can be removed
without requiring a major version bump, or it can be left in to prevent
breakage at our discretion.

From the users perspective normalizing a GitHub style URL with the following
route:

    github.com/{username string}/{repo string}

would look like the following:

```go
username := mux.Param(req, "username")
req = mux.WithParam(req, strings.ToLower(username))

repo := mux.Param(req, "repo")
req = mux.WithParam(ctx, strings.ToLower(repo.Raw))

normPath := mux.Path(req)
```

[Go]: https://golang.org/
[context]: https://golang.org/pkg/context/
[`soquee.net/mux`]: https://code.soquee.net/mux/
[`Request`]: https://golang.org/pkg/net/http/#Request
[`ParamInfo.Offset`]: https://godoc.org/code.soquee.net/mux#ParamInfo.Offset
[semver]: https://semver.org/

A design/relaxed_path_matching.md => design/relaxed_path_matching.md +88 -0
@@ 0,0 1,88 @@
# Proposal: Relaxed Path Matching

**Author(s):** Sam Whited  
**Last updated:** 2020-01-02  
**Status:** thinking

## Abstract

A design is proposed for relaxing the rule that requires `path` typed route
parameters to only exist as the last component in a route.
This would allow for greater flexibility in route construction and fewer special
cases in the type system.

## Background

In a typed HTTP router the "path" type always matches the remainder of the
input path. For example, the route:

    /files/{p path}

Matches:

- `/files/img.png`
- `/files/myalbum/img.png`

Right now it must always be the last item in any route otherwise creating the
router fails.

I propose relaxing this restriction by removing the special case that only
allows path type parameters at the end of routes, and making them match exactly
one path component if they are not at the end of the route, or the remainder of
the path if they are.


## Requirements

 - Do not break existing end-of-path matching support
 - No new public types or identifiers
 - Ability to remove the special case in the type system to panic if a `path`
   type route parameter is registered in an incorrect way


## Proposal

This proposal creates a new distinction between the *value* of a route parameter
and the matched path component.
Matching a `path` typed route parameter, the value would now be the remainder of
the path, but it would match against any single path component similar to a
string match:

    /albums/{p path}/cover.png

Would match:

- /albums/myalbum/cover.png
- /albums/whatever/cover.png

But would not match:

- /albums/foo/bar/cover.png

While the previous example with a `path` typed parameter at the end of the route
would behave exactly the same way as it previously did.

The matching behaves the same as using the string type, except that the value
of p would be "myalbum/cover.png" and "whatever/cover.png" instead of just
"myalbum" or "whatever" as it would be if the route had been `/albums/{p
string}/cover.png`.

An empty route parameter (`{}`) is the same as an unnamed parameter of type
string, ie. `{string}` and matches any single path component (but its value is
not saved). This means that with this restriction if we wanted to match any
path with two components, for example, Git repos in a GitHub style URL, instead
of doing:

    /{username string}/{repo string}

And then getting the values of username and repo and joining them together, we
could do something like the following to match any path with 2 components and
pull out the path directly without any extra logic in our HTTP handler:

    /{repo path}/{}

This would match:

- `/samwhited/mux`
- `/foo/bar`
- etc.