routing

package
v0.37.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package routing decides which handler answers a request, and what the address of a handler is called.

It is a thin shell over http.ServeMux, which since Go 1.22 already matches methods and path parameters. What this package adds is everything the standard mux has no opinion about: groups with an inherited prefix, name and middleware; a route table that survives registration so a view can build a URL from a name instead of a string literal; the seven REST routes of a resource controller; and the two middlewares that decide whether a request reaches a route at all.

One handler type

A route dispatches to an http.Handler. There is one registration path and one handler type on it -- Get, Post, Put, Patch, Delete, Match, Any and Fallback all take the same thing -- because the shape this replaced had two: Get took an http.HandlerFunc and a controller arrived through a second method, Action, whose job was to build a handler out of a controller method and translate what it returned. That second path was never a second way to route. It was a way to construct a handler, and everything it touched -- the request context, the renderer, the flash, a rejected form -- belongs to the request layer above this one. Constructing the handler happens there now, and a controller action reaches a route the same way any other handler does:

r.Get("/dashboard", adapt(dashboard.Index)).Name("dashboard")

where adapt is the adapter that layer supplies. This package neither declares it nor names one, because the type it builds is the one this package must not import.

Resource takes the adaptation as an argument for the same reason, which is what lets this package register controller routes without importing the type a controller receives. See Adapter.

Names, not paths

A hardcoded "/invoices/"+id compiles and keeps compiling after the route moves. Routes.Route does not: an unknown name or a wrong number of parameters is an error the caller sees, not a 404 the person sees.

Composing middleware is hesape/pipeline, generic over what it wraps, and this package uses it rather than declaring a second one. The request context belongs to hesape/http, which owns what an answer looks like; the half of the signed-URL story that belongs here is SignedRoute and middleware.ValidateSignature, over the Signer in hesape/encryption.

Index

Constants

This section is empty.

Variables

View Source
var ErrTenantlessBinding = errors.New("routing: route binding without a tenant")

ErrTenantlessBinding is returned when a binding is resolved under a Grant that carries no tenant.

It is a refusal and not a warning: a query with no tenant to scope by reads every customer, so the binding fails rather than running unscoped.

Functions

func ContextWithRoute

func ContextWithRoute(ctx context.Context, route *Route) context.Context

ContextWithRoute returns ctx with route stored, for a handler or middleware that needs to install the matched route itself -- a custom dispatcher, or a test. ServeHTTP already does this for every registered route.

func FormatRoutes

func FormatRoutes(routes []*Route) string

FormatRoutes renders the route table for the terminal, grouped by module and sorted by pattern.

It is here, and not in the CLI, so that every consumer prints the same table: the CLI's route list and the route list on the error page are the same rows in the same order, and a developer comparing the two is comparing like with like.

The name column is what a developer copies into Routes.Route instead of typing the path, so a route without one prints short rather than printing an empty column that looks like a value. A deprecated route prints its sunset date last, which is the date a reader plans against.

func Merge

func Merge(attributes, old map[string]any, prependExistingPrefix ...bool) map[string]any

Merge folds a group's attributes into the ones it is nested inside, and returns the combined set.

Prefix concatenates with a slash, name with nothing (the dot is the caller's), namespace with a backslash, where unions with the new winning. Domain and controller replace rather than merge: a nested group that names a domain is declaring one, not adding to one.

prependExistingPrefix defaults to true. False puts the new prefix in front, which is how a group registered under a versioned API root keeps the version segment outermost.

func SignedRoute

func SignedRoute(t *Routes, s *encryption.Signer, name string, ttl time.Duration, params ...string) (string, error)

SignedRoute builds the URL of a named route with a signature on it, valid for ttl.

SignedRoute(r.Table(), signer, "unsubscribe", 30*24*time.Hour, list.ID)

It is what an unsubscribe link and a verification link are made of. The alternative -- a table of tokens -- costs a write, a read, a cleanup job and a decision about what happens when the row is gone; this costs a signature, and a link that has run out says so.

The signature covers the whole address, not just the parameters: appending anything to the query string of a signed link invalidates it, so a route behind ValidateSignature cannot be handed a value its author did not sign.

It signs and it does not verify: middleware.ValidateSignature is the other half, and the Signer itself is in hesape/encryption, which is where a leaf primitive belongs. Building a signed URL is routing's, because the address being signed is a route.

func UniqueMiddleware

func UniqueMiddleware(middleware []pipeline.Middleware[http.Handler]) []pipeline.Middleware[http.Handler]

UniqueMiddleware drops repeats while keeping the first of each, so a middleware a group and a route both attached runs once.

func VerifySignature

func VerifySignature(s *encryption.Signer, r *http.Request) error

VerifySignature reports whether a request arrived on a URL this application signed, and that the signature has not run out.

It returns an error that unwraps to encryption.ErrSignature, and to encryption.ErrExpired when the only thing wrong is the time -- which is the one failure a person can act on, because "ask for another link" is a different sentence from "this link is not ours".

It is exported alongside the middleware because a handler that is reached some other way -- a webhook that also accepts an unsigned call from an authenticated operator -- needs the same check, and a second reading of the query parameter is a second place for the canonicalisation to be missing.

Types

type Adapter

type Adapter[C any] func(func(*C) error) http.Handler

Adapter turns one controller action into an http.Handler.

C is the request context a controller action receives, and it is a type parameter rather than a named type because that type lives above this package: hesape/http owns the request context, the renderer and the answer to a rejected form, and a router that imported it would be a router that had to know what an answer looks like in order to match a path.

The layer that owns C supplies the adapter, and the call reads:

routing.Resource(r, "invoices", InvoiceController{}, adapt)

where adapt is that layer's own function. It is the same one a single route already goes through -- r.Get("/x", adapt(c.Show)) -- passed by name instead of applied, so Resource can apply it to the seven actions it finds.

type BinaryFileResponse

type BinaryFileResponse struct {
	// Path is the file to send.
	Path string
	// Headers are sent alongside the ones http.ServeContent derives from the
	// file itself.
	Headers http.Header
}

BinaryFileResponse is a path on disk plus the headers to send it under. ResponseFactory.Download and File both build one.

The bytes are read when the response is sent, never before, so a large file costs a descriptor and not memory.

func (*BinaryFileResponse) SendContent

func (r *BinaryFileResponse) SendContent(w http.ResponseWriter, req *http.Request) error

SendContent opens the file and hands it to http.ServeContent, which picks the status -- 200, 206 for a range request, 304 for a conditional one -- and streams the bytes.

func (*BinaryFileResponse) ServeHTTP

func (r *BinaryFileResponse) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP makes a BinaryFileResponse an http.Handler. A file that cannot be opened answers 404.

func (*BinaryFileResponse) SetContentDisposition

func (r *BinaryFileResponse) SetContentDisposition(disposition, name, fallback string) *BinaryFileResponse

SetContentDisposition says whether the browser saves the file or shows it, and under what name.

The fallback is the ASCII name a client that does not understand RFC 5987 falls back to, which is what [responseFactoryFallbackName] produces.

type BindingFunc

type BindingFunc func(ctx context.Context, g auth.Grant, value string, route *Route) (any, error)

BindingFunc resolves one route parameter into the value a handler receives.

It is the shape of the closure Router.Bind takes, and of the one RouteBinding.ForModel returns. It takes the context every query needs and the Grant every query is scoped by.

A binder that looks a record up by value alone returns another tenant's row for the same id. Filter by auth.Tenant(g).

func ForCallback

func ForCallback(binder BindingFunc) BindingFunc

ForCallback returns binder unchanged: there is no container here to resolve a name through, so a binder is the closure itself.

func ForModel

func ForModel(class UrlRoutable, callback ...MissingModelFunc) BindingFunc

ForModel turns a record type into a binder: the value from the path is handed to that type's ResolveRouteBinding, under the Grant, and what comes back is what the handler receives.

type Creator

type Creator[C any] interface {
	Create(*C) error
}

Creator answers GET /thing/create -- the empty form.

type Destroyer

type Destroyer[C any] interface {
	Destroy(*C) error
}

Destroyer answers DELETE /thing/{id}.

type Editor

type Editor[C any] interface {
	Edit(*C) error
}

Editor answers GET /thing/{id}/edit -- the filled form.

type Group

type Group struct {
	// Prefix is prepended to the path of every route registered under it.
	Prefix string
	// Name is prepended, dot-joined, to the name of every route registered
	// under it. See Route.Name.
	Name string
	// Middleware wraps every route registered under it, outside any middleware
	// the route gives itself, and inside the middleware of any enclosing group.
	Middleware []pipeline.Middleware[http.Handler]
}

Group is what a sub-router adds to everything registered under it.

It is one struct rather than three arguments because the three are always decided together, and because a positional signature makes the common case -- a prefix and nothing else -- indistinguishable at the call site from the case that also names its routes.

admin := r.Group(routing.Group{
	Prefix:     "/admin",
	Name:       "admin",
	Middleware: []pipeline.Middleware[http.Handler]{auth.RequireRole("admin")},
})

type ImplicitRouteBinding

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

ImplicitRouteBinding resolves a route's parameters into records.

There are no type hints to reflect on here, so the registry is explicit -- Router.Bind and Router.Model -- and this walks the route's parameters against it.

func NewImplicitRouteBinding

func NewImplicitRouteBinding(b *RouteBindings) *ImplicitRouteBinding

NewImplicitRouteBinding creates a resolver backed by the bindings set.

func (*ImplicitRouteBinding) ResolveForRoute

func (b *ImplicitRouteBinding) ResolveForRoute(ctx context.Context, g auth.Grant, route *Route, req *http.Request) error

ResolveForRoute resolves every parameter of the route that has a binder registered, and installs what came back as the value the handler reads.

Two things it refuses

A Grant with no tenant, before it looks anything up: a lookup with nothing to scope by reads every customer.

A child whose parent is a record and whose route is scoped: the child is resolved through the parent -- /clients/{client}/invoices/{invoice} finds the invoice among that client's, so an invoice id belonging to somebody else is a 404 rather than a page. That is the second leak this file exists to close, and it is the one a tenant filter alone does not catch: two clients of the same tenant are still two clients.

type Indexer

type Indexer[C any] interface {
	Index(*C) error
}

Indexer answers GET /thing -- the list.

type MissingModelFunc

type MissingModelFunc func(ctx context.Context, value string) (any, error)

MissingModelFunc decides what a model binding that matched nothing means. It is the third argument of Router.Model, and returning a nil value with a nil error means "answer 404".

type Redirect

type Redirect struct {
	Path    string
	Status  int
	Headers http.Header
	// Session carries flash data attached to the redirect: WithInput, WithErrors.
	Session SessionStore
}

Redirect is the data a handler returns to tell the framework to send a redirect response.

The fields are public so the caller can add cookies, flash messages and headers before returning it.

func (*Redirect) ExceptInput

func (r *Redirect) ExceptInput(keys ...string) *Redirect

ExceptInput flashes everything except the named keys.

func (*Redirect) GetTargetURL

func (r *Redirect) GetTargetURL() string

GetTargetURL returns the path being redirected to.

func (*Redirect) OnlyInput

func (r *Redirect) OnlyInput(keys ...string) *Redirect

OnlyInput flashes only the named keys from the current input.

func (*Redirect) SetTargetURL

func (r *Redirect) SetTargetURL(u string)

SetTargetURL replaces the path.

func (*Redirect) With

func (r *Redirect) With(key string, value any) *Redirect

With flashes the value under key so the next request reads it.

func (*Redirect) WithErrors

func (r *Redirect) WithErrors(errors any) *Redirect

WithErrors flashes errors so the form shows them.

func (*Redirect) WithFragment

func (r *Redirect) WithFragment(fragment string) *Redirect

WithFragment appends a fragment to the URL.

func (*Redirect) WithInput

func (r *Redirect) WithInput(input map[string]any) *Redirect

WithInput flashes the input so the form comes back filled.

func (*Redirect) WithoutFragment

func (r *Redirect) WithoutFragment() *Redirect

WithoutFragment removes the fragment.

func (*Redirect) WithoutInput

func (r *Redirect) WithoutInput() *Redirect

WithoutInput removes the flashed input.

type Redirector

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

Redirector builds RedirectResponses to every place a handler can send the browser: back to the previous page, to a named route, to a path, to the address an unauthenticated visitor was trying to reach.

It holds a UrlGenerator and optionally a session store, and every method that answers a redirect does so by building a URL through the generator and wrapping it.

func NewRedirector

func NewRedirector(g *UrlGenerator) *Redirector

NewRedirector returns a redirector backed by g.

func (*Redirector) Action

func (r *Redirector) Action(action string, parameters map[string]any, status int, headers http.Header) (*Redirect, error)

Action sends the browser to a controller action.

func (*Redirector) Away

func (r *Redirector) Away(path string, status int, headers http.Header) *Redirect

Away sends the browser to an external URL, without validating the destination -- for a third-party integration, not for a path within the application.

func (*Redirector) Back

func (r *Redirector) Back(status int, headers http.Header, fallback string) *Redirect

Back sends the browser where it came from, and to the fallback when it came from nowhere -- a bookmarked form, a link opened in a new tab.

func (*Redirector) GetIntendedUrl

func (r *Redirector) GetIntendedUrl() string

GetIntendedUrl reads the stored URL without removing it.

func (*Redirector) GetUrlGenerator

func (r *Redirector) GetUrlGenerator() *UrlGenerator

GetUrlGenerator returns the generator backing this redirector.

func (*Redirector) Guest

func (r *Redirector) Guest(path string, status int, headers http.Header, secure *bool) *Redirect

Guest sends the browser to a path, storing where it was going so Intended can bring it back after a sign-in.

func (*Redirector) Home

func (r *Redirector) Home(status int, headers http.Header) *Redirect

Home sends the browser to the route named "home", falling back to "/" when there is none.

func (*Redirector) Intended

func (r *Redirector) Intended(def string, status int, headers http.Header, secure *bool) *Redirect

Intended sends the browser to the URL stored by Guest, or to the default.

func (*Redirector) Refresh

func (r *Redirector) Refresh(status int, headers http.Header) *Redirect

Refresh sends the browser to the current URL.

func (*Redirector) Route

func (r *Redirector) Route(name string, parameters map[string]any, status int, headers http.Header) (*Redirect, error)

Route sends the browser to a named route.

func (*Redirector) Secure

func (r *Redirector) Secure(path string, status int, headers http.Header) *Redirect

Secure sends the browser to path over https.

func (*Redirector) SetIntendedUrl

func (r *Redirector) SetIntendedUrl(u string) *Redirector

SetIntendedUrl stores a URL for Intended to read back.

func (*Redirector) SetSession

func (r *Redirector) SetSession(s SessionStore)

SetSession attaches a session store so Intended and Guest can read and write the intended URL.

func (*Redirector) SignedRoute

func (r *Redirector) SignedRoute(name string, parameters map[string]any, expiration time.Time, status int, headers http.Header) (*Redirect, error)

SignedRoute sends the browser to a signed URL for a named route.

func (*Redirector) TemporarySignedRoute

func (r *Redirector) TemporarySignedRoute(name string, expiration time.Time, parameters map[string]any, status int, headers http.Header) (*Redirect, error)

TemporarySignedRoute sends the browser to a signed URL for a named route, expiring at the given time.

func (*Redirector) To

func (r *Redirector) To(path string, status int, headers http.Header, secure *bool) *Redirect

To sends the browser to path.

redirector.To("/invoices/42", 302, nil, nil)

type Requirement added in v0.25.0

type Requirement struct {
	// Route is the route that declared it.
	Route *Route
	// Action is what Can declared.
	Action auth.Action
}

Requirement is a route and the action it requires.

type ResponseFactory

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

ResponseFactory is the one place a handler asks for an answer of a given shape -- a body, a view, JSON, a stream, a file, a redirect -- instead of assembling one by hand.

It holds two collaborators: a ViewRenderer, which is this package's seam onto the view layer, and a Redirector. Every method named Redirect* is a delegation to that redirector and nothing else -- there is one redirect mechanism in this package, not two, which is why those methods answer with the Redirect the Redirector builds rather than with an hhttp.RedirectResponse.

func NewResponseFactory

func NewResponseFactory(view ViewRenderer, redirector *Redirector) *ResponseFactory

NewResponseFactory builds a ResponseFactory from its two collaborators.

Either collaborator may be nil, and the method that needs the missing one says so rather than panicking: a project that never returns a view has no renderer to give.

func (*ResponseFactory) Download

func (f *ResponseFactory) Download(path, name string, headers http.Header, disposition string) *BinaryFileResponse

Download sends a file from disk under a Content-Disposition that makes the browser save it.

It takes the path, and the file is opened when the response is sent rather than read into memory when it is built -- BinaryFileResponse serves it through http.ServeContent, so a range request and a conditional request are answered the way the standard library answers them.

An empty name means the file's own base name. An empty disposition defaults to "attachment".

func (*ResponseFactory) EventStream

func (f *ResponseFactory) EventStream(callback iter.Seq[any], headers http.Header, endStreamWith ...any) *StreamedResponse

EventStream builds a server-sent event stream, with the three headers that stop a proxy or a browser from buffering it.

callback is an iter.Seq: a producer that is pulled, not a slice built in advance. A message that is an *hhttp.StreamedEvent names its own event; anything else is sent under "update", and a value that is neither a string nor a number is encoded as JSON.

endStreamWith is optional and defaults to "</stream>": omit it for that default, and pass nil to close the stream without a final event. Passing an *hhttp.StreamedEvent names the closing event.

The iteration stops when the request context is done.

func (*ResponseFactory) File

func (f *ResponseFactory) File(path string, headers http.Header) *BinaryFileResponse

File sends the raw contents of a file, with no Content-Disposition, so a PDF or an image is shown rather than saved.

It takes the path, for the reason [Download] does.

func (*ResponseFactory) JSON

func (f *ResponseFactory) JSON(data any, status int, headers http.Header, options int) (*hhttp.JsonResponse, error)

JSON builds a response whose body is data encoded as JSON.

options is a bitmask of hhttp's JSON* flags. encoding/json has no counterpart for a hex-escaping default, so 0 -- no flags -- is the default here.

It returns (*hhttp.JsonResponse, error), so data that cannot be encoded is reported rather than panicking.

func (*ResponseFactory) JSONP

func (f *ResponseFactory) JSONP(callback string, data any, status int, headers http.Header, options int) (*hhttp.JsonResponse, error)

JSONP is [JSON] with the payload wrapped in a call to the named callback, which turns the Content-Type into text/javascript.

func (*ResponseFactory) Make

func (f *ResponseFactory) Make(content any, status int, headers http.Header) (*hhttp.Response, error)

Make builds a response around a body.

A status of 0 means 200 with no headers, as it does everywhere else in this package. It returns (*hhttp.Response, error) so a body that cannot be encoded is reported rather than panicking.

func (*ResponseFactory) NoContent

func (f *ResponseFactory) NoContent(status int, headers http.Header) (*hhttp.Response, error)

NoContent builds an empty body under a status that says there is nothing to read. A status of 0 means 204.

func (*ResponseFactory) RedirectGuest

func (f *ResponseFactory) RedirectGuest(path string, status int, headers http.Header, secure *bool) (*Redirect, error)

RedirectGuest delegates to Redirector.Guest: the redirect that remembers where the visitor was going, so a sign-in can send them back there.

func (*ResponseFactory) RedirectTo

func (f *ResponseFactory) RedirectTo(path string, status int, headers http.Header, secure *bool) (*Redirect, error)

RedirectTo delegates to Redirector.To.

func (*ResponseFactory) RedirectToAction

func (f *ResponseFactory) RedirectToAction(action string, parameters map[string]any, status int, headers http.Header) (*Redirect, error)

RedirectToAction delegates to Redirector.Action.

func (*ResponseFactory) RedirectToIntended

func (f *ResponseFactory) RedirectToIntended(def string, status int, headers http.Header, secure *bool) (*Redirect, error)

RedirectToIntended delegates to Redirector.Intended: the other half of [RedirectGuest].

An empty default falls back to "/".

func (*ResponseFactory) RedirectToRoute

func (f *ResponseFactory) RedirectToRoute(route string, parameters map[string]any, status int, headers http.Header) (*Redirect, error)

RedirectToRoute delegates to Redirector.Route.

func (*ResponseFactory) Stream

func (f *ResponseFactory) Stream(callback StreamCallback, status int, headers http.Header) *StreamedResponse

Stream builds a response whose body is written as it is produced, rather than assembled and then sent.

The callback is handed an io.Writer that flushes after every write, so the bytes reach the client as soon as they are written. It also receives a context, because a stream is I/O that outlives the call that started it and the client may hang up mid-body.

func (*ResponseFactory) StreamDownload

func (f *ResponseFactory) StreamDownload(callback StreamCallback, name string, headers http.Header, disposition string) *StreamedResponse

StreamDownload is a [Stream] the browser saves instead of showing, because of the Content-Disposition header.

By the time the callback runs, the status and the headers are already sent and there is no error page left to send, so a failure is wrapped in exceptions.StreamedResponseError rather than returned bare.

An empty name leaves the header off. An empty disposition defaults to "attachment".

func (*ResponseFactory) StreamJSON

func (f *ResponseFactory) StreamJSON(data any, status int, headers http.Header, encodingOptions int) *StreamedResponse

StreamJSON encodes the value straight onto the wire instead of building the whole document in memory first.

It answers with a StreamedResponse whose callback is the encoder, rather than a dedicated streaming-JSON response type.

encodingOptions is the same flag word [JSON] takes.

func (*ResponseFactory) View

func (f *ResponseFactory) View(view string, data any, status int, headers http.Header) (*hhttp.Response, error)

View renders a named view and answers with what it drew.

It goes through ViewRenderer, which is the seam this package already uses to reach the view layer without importing it.

This takes a single view name; a caller that wants a fallback among several names chooses one before calling.

It returns (*hhttp.Response, error), so a missing or failing template is reported rather than panicking.

type Route

type Route struct {
	// Method is the HTTP method the route answers, or ANY for a route
	// registered without one.
	Method string
	// Pattern is the full path, prefixes of every enclosing group included.
	Pattern string
	// Module is the module that registered the route, for grouping in route
	// introspection. It is empty for a route registered outside a module.
	Module string
	// contains filtered or unexported fields
}

Route is one registered route.

It is metadata and the handler the mux dispatches to: what route introspection prints, what the error page shows for the pattern that matched, what a URL is generated from, and the http.Handler that answers. The handler is stored here rather than handed to the mux wrapped and frozen at registration, so a where constraint, a middleware added after registration and a route model binding can all take effect on a route that is already registered -- which is the shape every fluent call returns.

func Resource

func Resource[C any](r *Router, name string, controller any, adapt Adapter[C]) []*Route

Resource registers the REST routes a controller implements.

routing.Resource(r, "invoices", InvoiceController{}, adapt)

The seven, in the conventional order and with the conventional names:

GET    /invoices             index    invoices.index
GET    /invoices/create      create   invoices.create
POST   /invoices             store    invoices.store
GET    /invoices/{id}        show     invoices.show
GET    /invoices/{id}/edit   edit     invoices.edit
PUT    /invoices/{id}        update   invoices.update
PATCH  /invoices/{id}        update   invoices.update
DELETE /invoices/{id}        destroy  invoices.destroy

The order matters: /invoices/create is registered before /invoices/{id} so a GET of "create" reaches the form rather than being read as an id. Go's ServeMux prefers the more specific pattern, and registering in this order keeps the intent readable even where the mux would sort it out anyway.

A controller implementing none of the seven registers nothing and returns zero routes, which is a wiring mistake worth seeing in the route table.

It is a function and not a method on Router because a method cannot take a type parameter in Go, and C has to come from somewhere. The router is the first argument, so the line still reads left to right as "register, on this router, this resource".

func RouteFromContext

func RouteFromContext(ctx context.Context) *Route

RouteFromContext returns the route the request matched, or nil if it did not go through a registered route (a direct mux pattern, or a handler that never set it). It is the read side of the contract the route model binding depends on.

func (*Route) Action

func (rt *Route) Action(action string) *Route

Action is an alias for Uses that takes the action string, for whichever call site reads better with it.

func (*Route) AllowsTrashedBindings

func (rt *Route) AllowsTrashedBindings() bool

AllowsTrashedBindings reports whether WithTrashed was set.

func (*Route) Bind

func (rt *Route) Bind(req *http.Request) *http.Request

Bind associates the route with a request: it installs the route in the request context and returns the bound parameters. The binding state lives in the request, and ServeHTTP already does this for every registered route, so Bind is for a caller that dispatches a route outside the mux (RespondWithRoute).

func (*Route) BindingFieldFor

func (rt *Route) BindingFieldFor(param string) string

BindingFieldFor returns the binding field for a parameter, or empty.

func (*Route) BindingFields

func (rt *Route) BindingFields() map[string]string

BindingFields returns the {param:field} qualifiers parsed off the URI, keyed by parameter. The route model binding uses them to resolve a model by a column other than id.

func (*Route) Block

func (rt *Route) Block(lockSeconds, waitSeconds *int) *Route

Block stops two requests of the same session from running this route at once -- a double-submitted form, mostly -- by holding a lock for lockSeconds and waiting up to waitSeconds to take it.

func (*Route) Can

func (rt *Route) Can(action auth.Action) *Route

Can declares which authorization action this route requires.

r.Delete("/invoices/{id}", destroy).Name("invoices.destroy").Can("invoice.delete")

It declares and it does not decide, and that difference is the whole of what it is for. Nothing in the dispatch reads it: the handler still asks auth.Authorize, the policy still answers, and the repository still refuses without the Grant that answer produced. A route carrying this and no authorization is exactly as open as one carrying neither -- which is why it cannot become a second way to authorize, and must not be read as one.

What it is for is the catalogue. A permissions screen has to list what may be granted, and the honest list is the router: actions written down beside it disagree with the code the first time somebody adds a route. Requirements reads it back.

One route requires one action, because Grant.Check compares one. Declaring a second replaces the first rather than adding to it.

It used to take a string and a list of models, and to append them into the action map -- which nothing ever read. The type is the action the policy is asked about, and the reader is Requirements: what a route requires is now answerable rather than merely recorded.

The declaration is carried to the sibling rows of a single registration -- the PATCH of an update, the later verbs of a Match -- because they are one route to everyone but the mux.

func (*Route) Defaults

func (rt *Route) Defaults(key string, value any) *Route

Defaults sets a default value for a parameter, and returns the route. The redirect and view routes use it to carry a destination or a view name in the same place a bound parameter would live.

func (*Route) Deprecated added in v0.9.0

func (rt *Route) Deprecated(since, sunset time.Time) *Route

Deprecated marks the route as deprecated since the given instant and due to stop answering at sunset. It returns the route so the call chains, the same shape Name returns.

r.Get("/v1/invoices", h).Name("v1.invoices").
	Deprecated(announced, announced.AddDate(1, 0, 0))

Every response the route produces then carries two header fields: the Deprecation field of RFC 9745, holding the instant the route was deprecated, and the Sunset field of RFC 8594, holding the instant it stops answering. A client that reads either one learns the route is going away from the route itself, rather than from an announcement somebody has to remember to send. Route introspection prints the sunset date alongside the route.

Both instants are required, and a sunset before the deprecation date panics. The two fields would otherwise contradict each other on the wire -- RFC 9745 forbids exactly that pair -- and a client that catches a server contradicting itself is a client that stops reading both fields. It panics at registration, where the rest of this package refuses a declaration it cannot honour, so the contradiction is found at boot rather than by whoever was relying on the dates.

The deprecation date is an argument rather than the moment of this call because the moment of this call is process start: it would move forward on every deploy, and a client tracking when the route was deprecated would watch the answer change without anything having been decided.

func (*Route) Domain

func (rt *Route) Domain(domain string) *Route

Domain returns the host the route is declared for, or empty if none. http.ServeMux does not route by host, so this is for URL generation and for Matches.

func (*Route) EnforcesScopedBindings

func (rt *Route) EnforcesScopedBindings() bool

EnforcesScopedBindings reports whether ScopeBindings was set.

func (*Route) ExcludedMiddleware

func (rt *Route) ExcludedMiddleware() []pipeline.Middleware[http.Handler]

ExcludedMiddleware returns the middleware WithoutMiddleware removed.

func (*Route) ForgetParameter

func (rt *Route) ForgetParameter(req *http.Request, name string)

ForgetParameter removes a per-request override, leaving the raw path value the next Parameter call reads.

func (*Route) GatherMiddleware

func (rt *Route) GatherMiddleware() []pipeline.Middleware[http.Handler]

GatherMiddleware returns the full middleware list for the route, group middleware first. It is what the dispatcher reads.

func (*Route) GetAction

func (rt *Route) GetAction(key ...string) any

GetAction returns the action descriptor, or one of its keys, as a map whose keys are uses, controller and anything else set through Uses.

func (*Route) GetActionMethod

func (rt *Route) GetActionMethod() string

GetActionMethod returns the method half of the action string -- the part after @ in "PostController@show" -- or the whole string for a closure.

func (*Route) GetActionName

func (rt *Route) GetActionName() string

GetActionName returns the action string the route was registered with, or "Closure". It is what CurrentRouteAction returns for the route.

func (*Route) GetController

func (rt *Route) GetController() http.Handler

GetController returns the handler the route dispatches to: the http.Handler the registration was given, which is already the thing the mux calls. It returns nil for a route registered without one (which should not happen, but the nil is the honest answer).

func (*Route) GetControllerClass

func (rt *Route) GetControllerClass() string

GetControllerClass returns the controller half of the action string -- the part before @ in "PostController@show" -- or "Closure" for a closure route.

func (*Route) GetDefaults

func (rt *Route) GetDefaults(key ...string) any

GetDefaults returns the defaults map, or one key of it.

func (*Route) GetDeprecation added in v0.9.0

func (rt *Route) GetDeprecation() (since, sunset time.Time)

GetDeprecation returns the two instants Deprecated set: when the route was deprecated, and when it stops answering. Both are zero on a route that is not deprecated.

func (*Route) GetDomain

func (rt *Route) GetDomain() string

GetDomain returns the host declared with Domain.

func (*Route) GetMissing

func (rt *Route) GetMissing() http.Handler

GetMissing returns the handler Missing set, or nil.

func (*Route) GetName

func (rt *Route) GetName() string

GetName returns the name given with Name, or empty. RouteName is the other name for it.

func (*Route) GetOptionalParameterNames

func (rt *Route) GetOptionalParameterNames() map[string]any

GetOptionalParameterNames returns the names of the route's optional path parameters. The values are nil: the map is a set, and the question asked of it is only whether a name is in it.

func (*Route) GetPrefix

func (rt *Route) GetPrefix() string

GetPrefix returns the prefix the route was registered under.

func (*Route) GetURI

func (rt *Route) GetURI() string

GetURI is an alias for URI for the vocabulary that reads it as a getter.

func (*Route) GetValidators

func (rt *Route) GetValidators() []matching.ValidatorInterface

GetValidators returns the four checks that decide whether the route answers a request: path, method, scheme and host.

func (*Route) GetWheres

func (rt *Route) GetWheres() map[string]string

GetWheres returns the where constraints as the string patterns the caller set.

func (*Route) HasParameter

func (rt *Route) HasParameter(name string) bool

HasParameter reports whether name is one of the route's path parameters.

func (*Route) HasParameters

func (rt *Route) HasParameters() bool

HasParameters reports whether the route declares any path parameter. Unlike Parameter and its siblings, it does not need a request.

func (*Route) HttpOnly

func (rt *Route) HttpOnly() bool

HttpOnly reports whether the route answers only plain http, which a route generating a URL for a local development host declares.

func (*Route) HttpsOnly

func (rt *Route) HttpsOnly() bool

HttpsOnly is an alias for Secure.

func (*Route) IsDeprecated added in v0.9.0

func (rt *Route) IsDeprecated() bool

IsDeprecated reports whether Deprecated was called on the route.

func (*Route) IsFallback

func (rt *Route) IsFallback() bool

IsFallback reports whether the route is a fallback route.

func (*Route) LocksFor

func (rt *Route) LocksFor() *int

LocksFor returns the lock duration Block set. nil means the route takes no lock.

func (*Route) Matches

func (rt *Route) Matches(req *http.Request, includeMethod bool) bool

Matches reports whether the route answers the request. Method is checked against the route's verb and its siblings; the path is checked against the pattern, with a parameter segment matching any non-slash. A fallback route only matches if nothing more specific did, which the caller is expected to have tried first.

func (*Route) Methods

func (rt *Route) Methods() []string

Methods returns the HTTP verbs the route answers. A Match registration produces one route per verb, linked through siblings; Methods gathers them.

func (*Route) Middleware

func (rt *Route) Middleware(mws ...pipeline.Middleware[http.Handler]) *Route

Middleware appends middleware to the route, to run after the group's middleware and after anything already attached. Because the chain is read at request time, a Middleware call after registration -- which is the shape every fluent call returns -- takes effect on a route already in the mux.

Per-route middleware set at registration, through the varargs of Get and its siblings, and through this method, is one form: both reach the same slice.

func (*Route) Missing

func (rt *Route) Missing(h http.Handler) *Route

Missing sets the handler that answers when an implicit model binding resolves nothing. The implicit binding (hesape/routing, in the binding files) calls it; this is the declaration.

func (*Route) Name

func (r *Route) Name(name string) *Route

Name gives the route a name, so a URL can be generated from it instead of written by hand.

r.Get("/", home).Name("home")
routing.Resource(r, "invoices", InvoiceController{}, adapt) // names them all

It returns the route so the call chains, and the declaration reads as one line.

The name of every enclosing group is prepended, joined with a dot: Group{Name: "admin"} around .Name("users") gives "admin.users". The dot is joined here rather than left for the caller to remember, because a forgotten dot produces "adminusers", which is a name that works everywhere until somebody reads it.

func (*Route) Named

func (rt *Route) Named(patterns ...string) bool

Named reports whether the route's name matches any of the patterns, using * as a glob wildcard. A route with no name matches nothing.

func (*Route) OriginalParameter

func (rt *Route) OriginalParameter(req *http.Request, name string, def ...string) string

OriginalParameter returns the raw path value for name, before any override a binding installed. It is what a binding compares against to know whether it changed anything.

func (*Route) OriginalParameters

func (rt *Route) OriginalParameters(req *http.Request) map[string]string

OriginalParameters returns every path parameter as it arrived, before any binding replaced an id with the record it names -- which is what a binder compares against to know it changed something.

The request is the argument, so a call against an unbound route is not expressible.

func (*Route) Parameter

func (rt *Route) Parameter(req *http.Request, name string, def ...any) any

Parameter returns the value of a path parameter by name, with the per-request override a binding installed taking precedence over the raw path value. A missing parameter returns def.

func (*Route) ParameterNames

func (rt *Route) ParameterNames() []string

ParameterNames returns the names of the path parameters in the pattern, in order. Optional markers (?) and wildcard markers (...) are stripped.

func (*Route) Parameters

func (rt *Route) Parameters(req *http.Request) map[string]any

Parameters returns every path parameter for the request, overrides included. The map is a fresh copy, so a caller may mutate it.

func (*Route) ParametersWithoutNulls

func (rt *Route) ParametersWithoutNulls(req *http.Request) map[string]any

ParametersWithoutNulls returns Parameters with nil values dropped.

func (*Route) ParentOfParameter

func (rt *Route) ParentOfParameter(req *http.Request, name string) any

ParentOfParameter returns the value of the parameter that precedes name in the parameter list, or empty if name is first or absent. It is what scoped binding uses to resolve a child against its parent.

func (*Route) Prefix

func (rt *Route) Prefix(prefix string) *Route

Prefix prepends a prefix to the route's pattern. The mux is not re-registered, so this is for the URL generator and the group merge, not for moving a route after it has been wired.

func (*Route) PreventsScopedBindings

func (rt *Route) PreventsScopedBindings() bool

PreventsScopedBindings reports whether WithoutScopedBindings was set, which is distinct from leaving it unset.

func (*Route) RequiredAction added in v0.25.0

func (rt *Route) RequiredAction() auth.Action

RequiredAction returns the action Can declared, or empty.

It is not called Action because that name is the controller action string, and the two are different questions: one is which method answers, this is what the caller has to be allowed to do.

func (*Route) RouteName

func (r *Route) RouteName() string

RouteName returns the name given with Name, or empty.

The exported field used to be called Name and nothing ever wrote to it -- a promise the code did not keep. Now Name(...) writes it and this reads it.

func (*Route) Run

func (rt *Route) Run(w http.ResponseWriter, req *http.Request)

Run runs the route's handler without re-running middleware: middleware is the router's job, run is the action's. The route-in-context is installed so Parameter and the binding still work.

func (*Route) Scheme

func (rt *Route) Scheme(scheme string) *Route

Scheme declares the scheme the route answers on, "http" or "https". It is stored as one key of the route's action map. HttpOnly, HttpsOnly and Secure are the readers, and this is what writes what they read.

func (*Route) ScopeBindings

func (rt *Route) ScopeBindings() *Route

ScopeBindings marks the route as enforcing scoped implicit bindings -- a child resolved against its parent parameter. The implicit binding reads it.

func (*Route) Secure

func (rt *Route) Secure() bool

Secure reports whether the route answers only https, which is what makes its generated URL https regardless of how the current request arrived.

func (*Route) ServeHTTP

func (rt *Route) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP is the mux's entry into the route. It is the lazy half of the registration model: where the constraints, the matched listeners, the route-in-context and the middleware chain are applied here, at request time, so a fluent call after registration takes effect on a route already in the mux.

Where constraints are part of matching, so they run first and answer 404 on failure before anything else; then the route is installed in the context and the matched listeners fire; then the middleware chain runs the handler.

func (*Route) SetAction

func (rt *Route) SetAction(action map[string]any) *Route

SetAction sets the action descriptor and returns the route, for the group merge that carries domain and middleware in the same map.

func (*Route) SetActionName

func (r *Route) SetActionName(action string)

SetActionName stores the action name for URL generation via action().

func (*Route) SetBindingFields

func (rt *Route) SetBindingFields(fields map[string]string) *Route

SetBindingFields sets the binding fields, for the group merge and the deserializer.

func (*Route) SetBindingFieldsFromUri

func (rt *Route) SetBindingFieldsFromUri() string

SetBindingFieldsFromUri parses the route's pattern, applies the binding fields to the route, and returns the cleaned URI. It is what the group merge and the URL generator call when a pattern carries {param:field}.

func (*Route) SetControllerMethod

func (r *Route) SetControllerMethod(controller, method string)

SetControllerMethod stores controller and method for this route.

func (*Route) SetDefaults

func (rt *Route) SetDefaults(defaults map[string]any) *Route

SetDefaults replaces the defaults map.

func (*Route) SetFallback

func (rt *Route) SetFallback(fallback bool) *Route

SetFallback marks the route as a fallback. Fallback does this; SetFallback is for the deserializer.

func (*Route) SetParameter

func (rt *Route) SetParameter(req *http.Request, name string, value any)

SetParameter installs a per-request override for name, so a binding that resolves an id into a record hands the record to the handler through Parameter. It mutates the override map ServeHTTP placed in the context, so it does not need to return a new request.

func (*Route) SetRouter

func (rt *Route) SetRouter(router *Router) *Route

SetRouter attaches the router whose matched listeners the route fires and whose binders it resolves through.

func (*Route) SetURI

func (rt *Route) SetURI(uri string) *Route

SetURI sets the pattern the route answers. It is here for the binding and the URL generator, which can rewrite a route's pattern after a group merge; the mux is not re-registered, so a pattern change after registration does not move what the mux dispatches.

func (*Route) SetWheres

func (rt *Route) SetWheres(wheres map[string]string) *Route

SetWheres replaces the where constraints, given as a map.

func (*Route) ToResponse

func (rt *Route) ToResponse(w http.ResponseWriter, req *http.Request)

ToResponse runs the route's full chain -- middleware and handler -- and is what RespondWithRoute calls when it dispatches a named route directly.

func (*Route) URI

func (rt *Route) URI() string

URI returns the path pattern the route answers, prefixes of every enclosing group included.

func (*Route) Uses

func (rt *Route) Uses(action string) *Route

Uses sets the action string -- "PostController@show" or "Closure" -- and returns the route. It is the fluent form of declaring what a route answers, and it is what GetActionName and GetActionMethod read.

func (*Route) WaitsFor

func (rt *Route) WaitsFor() *int

WaitsFor returns the wait duration Block set. nil means the route takes no lock.

func (*Route) Where

func (rt *Route) Where(name, pattern string) *Route

Where sets a regular-expression constraint on a path parameter. A request whose value for that parameter fails the regex is answered 404 before the handler, which is what stops /users/{id} from reaching the database for /users/abc.

Go's ServeMux does not take a regex in a pattern, so the constraint is enforced here, at dispatch, rather than in the mux's matcher. A failing value does not fall through to a sibling route -- ServeMux already picked this one -- but the goal the constraint exists for, keeping a bad value out of the database, holds.

func (*Route) WhereAlpha

func (rt *Route) WhereAlpha(params ...string) *Route

WhereAlpha constrains the given parameters to ASCII letters.

func (*Route) WhereAlphaNumeric

func (rt *Route) WhereAlphaNumeric(params ...string) *Route

WhereAlphaNumeric constrains the given parameters to ASCII letters and digits.

func (*Route) WhereIn

func (rt *Route) WhereIn(param string, values ...string) *Route

WhereIn constrains the given parameters to one of the listed values.

func (*Route) WhereMap

func (rt *Route) WhereMap(wheres map[string]string) *Route

WhereMap sets several where constraints at once.

func (*Route) WhereNumber

func (rt *Route) WhereNumber(params ...string) *Route

WhereNumber constrains the given parameters to digits.

func (*Route) WhereUlid

func (rt *Route) WhereUlid(params ...string) *Route

WhereUlid constrains the given parameters to the ULID alphabet.

func (*Route) WhereUuid

func (rt *Route) WhereUuid(params ...string) *Route

WhereUuid constrains the given parameters to the UUID format.

func (*Route) WithTrashed

func (rt *Route) WithTrashed(with ...bool) *Route

WithTrashed allows soft-deleted records through implicit model binding on this route. The implicit binding (hesape/routing, in the binding files) reads it; this is the declaration.

func (*Route) WithoutBlocking

func (rt *Route) WithoutBlocking() *Route

WithoutBlocking clears any lock Block set.

func (*Route) WithoutMiddleware

func (rt *Route) WithoutMiddleware(mws ...pipeline.Middleware[http.Handler]) *Route

WithoutMiddleware records middleware to remove from the route's chain at request time. A middleware removed here is one a group attached that this route specifically does not want, and the removal is by identity.

func (*Route) WithoutScopedBindings

func (rt *Route) WithoutScopedBindings() *Route

WithoutScopedBindings marks the route as not enforcing scoped bindings.

type RouteBindings

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

RouteBindings holds the parameter binders registered for a router, filled by Router.Bind and Router.Model.

func NewRouteBindings

func NewRouteBindings() *RouteBindings

NewRouteBindings returns an empty set of bindings.

func (*RouteBindings) Bind

func (b *RouteBindings) Bind(key string, binder BindingFunc)

Bind registers a binder for a parameter key.

When a route carries a parameter named key, the binder resolves the raw path value and the handler receives what it returned rather than the string.

func (*RouteBindings) GetBindingCallback

func (b *RouteBindings) GetBindingCallback(key string) BindingFunc

GetBindingCallback returns the binder for a key, or nil.

func (*RouteBindings) Model

func (b *RouteBindings) Model(key string, class UrlRoutable, callback ...MissingModelFunc)

Model registers a model binding for a parameter key: the parameter resolves through the record's own ResolveRouteBinding, which is where the tenant filter lives.

The optional callback says what a value that matched nothing means. Without one, nothing found is nothing bound, which the caller answers 404.

type RouteNotFoundError

type RouteNotFoundError struct {
	Name string
}

RouteNotFoundError is returned when a named route or action is not in the table.

func (*RouteNotFoundError) Error

func (e *RouteNotFoundError) Error() string

type RouteParameterBinder

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

RouteParameterBinder reads a route's parameters off a request.

The mux has already matched the path, so the path values are read back through PathValue; the host half and the defaults are this type's work.

It reads, it does not resolve

What comes back here is what arrived in the URL: strings. Turning a string into a record is ImplicitRouteBinding's job, and that is where the Grant and the tenant filter are. Nothing in this file reaches a database, and nothing in it should: an id read off a path is untrusted input until a policy has been consulted about it.

func NewRouteParameterBinder

func NewRouteParameterBinder(route *Route) *RouteParameterBinder

NewRouteParameterBinder builds a binder for route.

func (*RouteParameterBinder) Parameters

func (b *RouteParameterBinder) Parameters(request *http.Request) map[string]any

Parameters returns every parameter of the route for this request, host parameters and defaults included.

type RouteRegistrar

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

RouteRegistrar collects group attributes fluently and applies them to the routes registered through it: a builder that holds prefix, name, middleware, domain, namespace and where, and that hands them to the router when a route is registered.

The Router methods Prefix, Name, Middleware, Domain and Namespace return one, so the call chains:

r.Prefix("/api").Name("api.").Middleware(auth.RequireLogin).Get("/users", h)

The attributes it carries are the same fields Group carries, plus a where map for the Where* constraints the registrar forwards to each route. It does not hold a group stack: a registrar is one layer, and nesting is the router's Group.

func (*RouteRegistrar) Any

func (reg *RouteRegistrar) Any(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Any registers a route answering every method with the registrar's attributes.

func (*RouteRegistrar) Attribute

func (reg *RouteRegistrar) Attribute(key string, value any) *RouteRegistrar

Attribute sets one attribute on the registrar, for the caller that builds it piecemeal.

func (*RouteRegistrar) Delete

func (reg *RouteRegistrar) Delete(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Delete registers a DELETE route with the registrar's attributes applied.

func (*RouteRegistrar) Domain

func (reg *RouteRegistrar) Domain(domain string) *RouteRegistrar

Domain sets the domain on the registrar and returns it.

func (*RouteRegistrar) Get

func (reg *RouteRegistrar) Get(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Get registers a GET route with the registrar's attributes applied.

func (*RouteRegistrar) Group

func (reg *RouteRegistrar) Group(fn func(*Router))

Group creates a sub-router with the registrar's attributes and runs fn against it. It is the registrar's group, which forwards to Router.Group.

func (*RouteRegistrar) Match

func (reg *RouteRegistrar) Match(methods []string, pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Match registers one handler under several methods, with the registrar's attributes applied.

func (*RouteRegistrar) Middleware

func (reg *RouteRegistrar) Middleware(mws ...pipeline.Middleware[http.Handler]) *RouteRegistrar

Middleware appends middleware to the registrar and returns it.

func (*RouteRegistrar) Name

func (reg *RouteRegistrar) Name(name string) *RouteRegistrar

Name sets the name prefix on the registrar and returns it.

func (*RouteRegistrar) Namespace

func (reg *RouteRegistrar) Namespace(ns string) *RouteRegistrar

Namespace sets the namespace on the registrar and returns it.

func (*RouteRegistrar) Options

func (reg *RouteRegistrar) Options(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Options registers an OPTIONS route with the registrar's attributes applied.

func (*RouteRegistrar) Patch

func (reg *RouteRegistrar) Patch(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Patch registers a PATCH route with the registrar's attributes applied.

func (*RouteRegistrar) Post

func (reg *RouteRegistrar) Post(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Post registers a POST route with the registrar's attributes applied.

func (*RouteRegistrar) Prefix

func (reg *RouteRegistrar) Prefix(prefix string) *RouteRegistrar

Prefix sets the prefix on the registrar and returns it.

func (*RouteRegistrar) Put

func (reg *RouteRegistrar) Put(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Put registers a PUT route with the registrar's attributes applied.

func (*RouteRegistrar) Where

func (reg *RouteRegistrar) Where(name, pattern string) *RouteRegistrar

Where sets a where constraint on the registrar and returns it. Every route registered through the registrar inherits it.

func (*RouteRegistrar) WhereAlpha

func (reg *RouteRegistrar) WhereAlpha(params ...string) *RouteRegistrar

WhereAlpha constrains the given parameters to ASCII letters.

func (*RouteRegistrar) WhereAlphaNumeric

func (reg *RouteRegistrar) WhereAlphaNumeric(params ...string) *RouteRegistrar

WhereAlphaNumeric constrains the given parameters to ASCII letters and digits.

func (*RouteRegistrar) WhereIn

func (reg *RouteRegistrar) WhereIn(param string, values ...string) *RouteRegistrar

WhereIn constrains the given parameter to one of the listed values.

func (*RouteRegistrar) WhereNumber

func (reg *RouteRegistrar) WhereNumber(params ...string) *RouteRegistrar

WhereNumber constrains the given parameters to digits.

func (*RouteRegistrar) WhereUlid

func (reg *RouteRegistrar) WhereUlid(params ...string) *RouteRegistrar

WhereUlid constrains the given parameters to the ULID alphabet.

func (*RouteRegistrar) WhereUuid

func (reg *RouteRegistrar) WhereUuid(params ...string) *RouteRegistrar

WhereUuid constrains the given parameters to the UUID format.

type RouteUri

type RouteUri struct {
	URI           string
	BindingFields map[string]string
}

RouteUri is the parsed URI of a route: the cleaned path and the binding fields a {param:field} qualifier declared.

The parse is what pulls {post:slug} out of the pattern and leaves {post} in its place, so the mux sees a pattern it can match and the route model binding knows which column to resolve by.

func (RouteUri) Parse

func (RouteUri) Parse(uri string) RouteUri

Parse extracts the {param:field} qualifiers into BindingFields and leaves {param} (or {param?}) in the URI.

It is a method on the zero RouteUri rather than a package function, so that a package-level Parse is not left saying nothing about what it parses.

type RouteUrlGenerator

type RouteUrlGenerator struct {

	// DefaultParameters are the named defaults every route inherits, keyed by
	// parameter name or by "name:field" when the route binds by a field.
	DefaultParameters map[string]any

	// DontEncode are the characters restored after encoding, so a URL keeps
	// its separators.
	DontEncode map[string]string
	// contains filtered or unexported fields
}

RouteUrlGenerator is the half of UrlGenerator that turns a route and a bag of parameters into an address.

The work it does that a string concatenation does not: it fills named placeholders, falls back to the defaults, drops optional segments nobody filled, puts what is left over on the query string, and refuses -- rather than emitting a URL with a literal "{invoice}" in it -- when a required placeholder has no value.

func NewRouteUrlGenerator

func NewRouteUrlGenerator(u *UrlGenerator, request *http.Request) *RouteUrlGenerator

NewRouteUrlGenerator returns a generator for u and request.

func (*RouteUrlGenerator) Defaults

func (g *RouteUrlGenerator) Defaults(defaults map[string]any)

Defaults merges defaults into DefaultParameters.

func (*RouteUrlGenerator) To

func (g *RouteUrlGenerator) To(route *Route, parameters map[string]any, absolute bool) (string, error)

To builds the URL of a route.

It returns an error when a placeholder the caller left unfilled would 404 the reader; saying so at the call site is the whole reason a named route beats a string.

type Router

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

Router is a thin shell over http.ServeMux.

The mux already handles methods and path parameters. This exists for groups, per-group middleware and route metadata -- the metadata is what lets the CLI print the routes and what a view uses to build a URL from a name.

It holds no request state. An earlier version carried the view renderer and the flash so that it could build a request context itself, which is what made registering a controller a second registration path; both belong to the layer that owns the request context, and neither is here.

Configuration that is router-level rather than per-sub-router -- the global where patterns, the matched listeners, the middleware aliases and groups, the explicit binders and the view renderer -- lives on the root and is reached through [Router.root], so a sub-router created by Group reads the same map the kernel populated.

func NewRouter

func NewRouter() *Router

NewRouter returns an empty router.

func (*Router) AddRoute

func (r *Router) AddRoute(methods []string, uri string, action http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

AddRoute creates a route and puts it in the table, which is what every verb method goes through.

func (*Router) AliasMiddleware

func (r *Router) AliasMiddleware(name string, mw pipeline.Middleware[http.Handler]) *Router

AliasMiddleware registers a short name for a middleware, for route introspection and for the kernel's resolution.

func (*Router) Any

func (r *Router) Any(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Any registers a route that answers every method.

It is one registration and one row in the table, because that is what the mux does with a pattern that names no method. Reach for it for a webhook endpoint whose sender is not specific about the verb, and for nothing else: a route that also answers POST is a route where the CSRF check and the policy were written for a request shape that is not the one arriving.

func (*Router) Bind

func (r *Router) Bind(key string, binder BindingFunc)

Bind registers a binder for a parameter key: the binder resolves the raw path value into the record the handler expects.

The binder receives the auth.Grant and MUST filter by auth.Tenant(g). A binder that looks a record up by id alone delivers another customer's row through the URL, which is the most direct form of the cross-tenant leak.

func (*Router) Current

func (r *Router) Current(req *http.Request) *Route

Current returns the route the request matched, read from the request context. nil means the request did not go through a registered route.

func (*Router) CurrentRouteAction

func (r *Router) CurrentRouteAction(req *http.Request) string

CurrentRouteAction returns the action string of the current route, or empty.

func (*Router) CurrentRouteName

func (r *Router) CurrentRouteName(req *http.Request) string

CurrentRouteName returns the name of the current route, or empty.

func (*Router) CurrentRouteNamed

func (r *Router) CurrentRouteNamed(req *http.Request, patterns ...string) bool

CurrentRouteNamed reports whether the current route's name matches any of the patterns.

func (*Router) CurrentRouteUses

func (r *Router) CurrentRouteUses(req *http.Request, action string) bool

CurrentRouteUses reports whether the current route's action equals action.

func (*Router) Delete

func (r *Router) Delete(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Delete registers a DELETE route.

func (*Router) Dispatch

func (r *Router) Dispatch(w http.ResponseWriter, req *http.Request)

Dispatch runs the request through the mux, which is what ServeHTTP does. It is here for the surface that reads it as a method; the mux already holds the routes, and dispatch is its job.

func (*Router) DispatchToRoute

func (r *Router) DispatchToRoute(w http.ResponseWriter, req *http.Request)

DispatchToRoute is an alias for Dispatch: there is no separate global pipeline here to skip, since the mux is both the pipeline and the match.

func (*Router) Domain

func (r *Router) Domain(domain string) *RouteRegistrar

Domain returns a registrar that sets the domain on every route. The mux does not route by host, so this is for URL generation and matching.

func (*Router) Fallback

func (r *Router) Fallback(h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Fallback registers what answers everything under this router that no other route matched.

On the root router that is the whole application, which is where a custom 404 page goes. On a group it is that group's subtree: a group under /api can answer its own misses with JSON while the rest of the site answers with a page.

It is registered as the subtree pattern of the group -- "/" at the root, "/api/" under a group prefixed /api -- which http.ServeMux already treats as the least specific match, so every other route in the subtree still wins.

func (*Router) FlushMiddlewareGroups

func (r *Router) FlushMiddlewareGroups() *Router

FlushMiddlewareGroups empties the middleware groups.

func (*Router) ForModule

func (r *Router) ForModule(name string) *Router

ForModule returns a sub-router that tags its routes with the module name, for grouping in route introspection. The kernel calls it for each module.

func (*Router) GatherRouteMiddleware

func (r *Router) GatherRouteMiddleware(route *Route) []pipeline.Middleware[http.Handler]

GatherRouteMiddleware resolves the route's full middleware list, excluded middleware removed.

func (*Router) Get

func (r *Router) Get(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Get registers a GET route.

func (*Router) GetBindingCallback

func (r *Router) GetBindingCallback(key string) BindingFunc

GetBindingCallback returns the binder registered for key, or nil.

func (*Router) GetCurrentRoute

func (r *Router) GetCurrentRoute(req *http.Request) *Route

GetCurrentRoute returns the route the request matched. The request is the argument because a router shared by every goroutine cannot hold the one in flight.

func (*Router) GetGroupStack

func (r *Router) GetGroupStack() []map[string]any

GetGroupStack returns the attributes of every enclosing group, outermost first.

func (*Router) GetLastGroupPrefix

func (r *Router) GetLastGroupPrefix() string

GetLastGroupPrefix returns the prefix of the innermost open group, or empty.

func (*Router) GetMiddleware

func (r *Router) GetMiddleware() map[string]pipeline.Middleware[http.Handler]

GetMiddleware returns the middleware aliases.

func (*Router) GetMiddlewareGroups

func (r *Router) GetMiddlewareGroups() map[string][]pipeline.Middleware[http.Handler]

GetMiddlewareGroups returns the middleware groups.

func (*Router) GetPatterns

func (r *Router) GetPatterns() map[string]string

GetPatterns returns the global where patterns.

func (*Router) GetRoutes

func (r *Router) GetRoutes() *Routes

GetRoutes returns the route collection, for URL generation and route introspection.

func (*Router) GetViewRenderer

func (r *Router) GetViewRenderer() ViewRenderer

GetViewRenderer returns the view renderer, or nil.

func (*Router) Group

func (r *Router) Group(g Group) *Router

Group returns a sub-router with the prefix and name appended and the middleware inherited. The route table is shared with the parent, and so is the root configuration -- patterns, matched listeners, aliases, groups, binders and the view renderer -- so a sub-router reads what the kernel set.

func (*Router) Has

func (r *Router) Has(name string) bool

Has reports whether a route with the given name is registered.

func (*Router) HasGroupStack

func (r *Router) HasGroupStack() bool

HasGroupStack reports whether any route group is currently open.

func (*Router) HasMiddlewareGroup

func (r *Router) HasMiddlewareGroup(name string) bool

HasMiddlewareGroup reports whether a middleware group with the given name exists.

func (*Router) Input

func (r *Router) Input(req *http.Request, key string, def ...any) any

Input returns a parameter of the current route, or def.

func (*Router) Is

func (r *Router) Is(req *http.Request, patterns ...string) bool

Is reports whether the current route's name matches any of the patterns, with * as a glob. It is the alias for CurrentRouteNamed.

func (*Router) Match

func (r *Router) Match(methods []string, pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Match registers one handler under several methods.

r.Match([]string{"GET", "POST"}, "/search", search)

It returns the route of the first method, and that is the row a name is registered against: the others are in the table, they carry the name for display, and generating a URL from it has one answer rather than one per verb. They all point at the same pattern, so there is nothing to choose between.

An empty list of methods panics. It would otherwise register a route that answers nothing, at boot, silently -- and the symptom is a 404 on a path that the route table says exists.

func (*Router) Matched

func (r *Router) Matched(cb func(*Route, *http.Request))

Matched registers a callback that fires after a route matches, before its middleware.

func (*Router) MergeWithLastGroup

func (r *Router) MergeWithLastGroup(attributes map[string]any, prependExistingPrefix ...bool) map[string]any

MergeWithLastGroup merges attributes with the innermost open group's own, through Merge.

func (*Router) Middleware

func (r *Router) Middleware(mws ...pipeline.Middleware[http.Handler]) *RouteRegistrar

Middleware returns a registrar that wraps every route in the given middleware.

func (*Router) MiddlewareGroup

func (r *Router) MiddlewareGroup(name string, mws ...pipeline.Middleware[http.Handler]) *Router

MiddlewareGroup registers a named bundle of middleware the kernel composes.

func (*Router) Model

func (r *Router) Model(key string, class UrlRoutable, callback ...MissingModelFunc)

Model registers a model binding for a parameter key: the parameter resolves through the record type's own ResolveRouteBinding, under the Grant.

There is no container to resolve a class out of, so this takes a zero value of the type instead -- which is the same thing a container would have handed the binder.

func (*Router) Name

func (r *Router) Name(name string) *RouteRegistrar

Name returns a registrar that prepends name to every route's name. The name is dot-joined, as Group.Name is.

func (*Router) Namespace

func (r *Router) Namespace(ns string) *RouteRegistrar

Namespace returns a registrar that sets the namespace on every route. Go has no string-based controller resolution, so the namespace is recorded for display and for the URL generator, and does not change which handler answers.

func (*Router) NewRoute

func (r *Router) NewRoute(methods []string, uri string, action http.Handler) *Route

NewRoute builds a route bound to this router without registering it, for a caller assembling a table of its own.

func (*Router) Options

func (r *Router) Options(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Options registers an OPTIONS route.

func (*Router) Patch

func (r *Router) Patch(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Patch registers a PATCH route.

func (*Router) Pattern

func (r *Router) Pattern(key, pattern string)

Pattern sets a global where pattern, applied to every route at registration.

func (*Router) Patterns

func (r *Router) Patterns(patterns map[string]string)

Patterns sets several global where patterns at once.

func (*Router) PermanentRedirect

func (r *Router) PermanentRedirect(uri, destination string) *Route

PermanentRedirect registers a 301 redirect route.

func (*Router) Post

func (r *Router) Post(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Post registers a POST route.

func (*Router) Prefix

func (r *Router) Prefix(prefix string) *RouteRegistrar

Prefix returns a registrar that prepends prefix to every route registered through it.

func (*Router) PrependMiddlewareToGroup

func (r *Router) PrependMiddlewareToGroup(name string, mw pipeline.Middleware[http.Handler]) *Router

PrependMiddlewareToGroup adds a middleware to the start of a group, if it is not already there.

func (*Router) PushMiddlewareToGroup

func (r *Router) PushMiddlewareToGroup(name string, mw pipeline.Middleware[http.Handler]) *Router

PushMiddlewareToGroup adds a middleware to the end of a group, creating the group if it does not exist, if it is not already there.

func (*Router) Put

func (r *Router) Put(pattern string, h http.Handler, mws ...pipeline.Middleware[http.Handler]) *Route

Put registers a PUT route.

func (*Router) Redirect

func (r *Router) Redirect(uri, destination string, status ...int) *Route

Redirect registers a route that redirects to destination with status. The handler it installs writes the Location header and the status.

It is registered under Any so that a link or form of any verb that lands here is redirected; the status is the caller's to set (302 for Redirect, 301 for PermanentRedirect).

func (*Router) RemoveMiddlewareFromGroup

func (r *Router) RemoveMiddlewareFromGroup(name string, mw pipeline.Middleware[http.Handler]) *Router

RemoveMiddlewareFromGroup removes a middleware from a group by identity.

func (*Router) ResolveMiddleware

func (r *Router) ResolveMiddleware(middleware []pipeline.Middleware[http.Handler], excluded ...[]pipeline.Middleware[http.Handler]) []pipeline.Middleware[http.Handler]

ResolveMiddleware removes the excluded middleware from the list and drops duplicates. A middleware here is already the function to run, so there is no name or priority to resolve first.

func (*Router) RespondWithRoute

func (r *Router) RespondWithRoute(req *http.Request, name string, w http.ResponseWriter) error

RespondWithRoute dispatches the named route directly, bypassing the mux's match: it binds the request to the route and runs it. Used by a caller that has a route name and a request that did not arrive through the mux.

func (*Router) Routes

func (r *Router) Routes() []*Route

Routes returns the registered routes, in registration order.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP dispatches to the underlying mux.

func (*Router) SetViewRenderer

func (r *Router) SetViewRenderer(vr ViewRenderer)

SetViewRenderer wires the view renderer the kernel built, so View routes can render. Without it, a View route answers 500.

func (*Router) SubstituteBindings

func (r *Router) SubstituteBindings(ctx context.Context, g auth.Grant, rt *Route, req *http.Request) error

SubstituteBindings resolves the explicit binders for the route's parameters and installs what they returned as the value the handler reads.

The Grant is a parameter and not something read out of the context, because a parameter cannot be forgotten. Every binding is scoped by auth.Tenant(g), and a Grant with no tenant is refused before anything is looked up.

func (*Router) SubstituteImplicitBindings

func (r *Router) SubstituteImplicitBindings(ctx context.Context, g auth.Grant, rt *Route, req *http.Request) error

SubstituteImplicitBindings runs the implicit binding -- every parameter with a registered model, resolved through it, scoped where the route asked for scoping -- or the callback SubstituteImplicitBindingsUsing installed in its place.

func (*Router) SubstituteImplicitBindingsUsing

func (r *Router) SubstituteImplicitBindingsUsing(callback func(rt *Route, req *http.Request) error) *Router

SubstituteImplicitBindingsUsing installs callback in place of the default implicit binding.

func (*Router) Table

func (r *Router) Table() *Routes

Table returns the route table, for URL generation and route introspection.

func (*Router) Uses

func (r *Router) Uses(req *http.Request, patterns ...string) bool

Uses reports whether the current route's action matches any of the patterns.

func (*Router) View

func (r *Router) View(pattern, view string, data ...map[string]any) *Route

View registers a GET route that renders a view.

The view renderer is the one SetViewRenderer wired; without it, the route answers 500, which is the honest answer to "the view layer was not given to the router". The handler it installs calls the renderer, and the data map is carried in the route's defaults -- the same place Redirect carries its destination, which keeps the two redirect-and-view routes shaped alike.

type Routes

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

Routes is the table of registered routes, and the index by name.

func NewRoutes

func NewRoutes() *Routes

NewRoutes returns an empty table.

A Router builds its own; this is for the caller that has to hold one before there is a router -- a test, and the URL generator the view layer is handed at boot.

func (*Routes) Add

func (t *Routes) Add(route *Route) *Route

Add registers a route in the table and returns it. It is the exported form of the add the router uses, for a caller that holds a table it built separately -- a test, or a compiled route collection.

func (*Routes) All

func (t *Routes) All() []*Route

All returns the routes in registration order, for route introspection.

func (*Routes) Count

func (t *Routes) Count() int

Count returns the number of routes registered.

func (*Routes) GetByAction

func (t *Routes) GetByAction(action string) *Route

GetByAction returns the route whose action string is action, or nil. The action string is what Uses set -- "PostController@show" or "Closure". The lookup is a scan, because the action is set after registration through a fluent call, and an index maintained at registration would miss it.

func (*Routes) GetByName

func (t *Routes) GetByName(name string) *Route

GetByName returns the route registered with name, or nil. It is the exported form the URL generator reads.

func (*Routes) GetRoutes

func (t *Routes) GetRoutes() []*Route

GetRoutes returns the routes in registration order. It is an alias for All.

func (*Routes) GetRoutesByMethod

func (t *Routes) GetRoutesByMethod(method string) []*Route

GetRoutesByMethod returns the routes that answer the given method, in registration order. A route registered with ANY answers every method.

func (*Routes) GetRoutesByName

func (t *Routes) GetRoutesByName() map[string]*Route

GetRoutesByName returns a copy of the name index.

func (*Routes) HasNamedRoute

func (t *Routes) HasNamedRoute(name string) bool

HasNamedRoute reports whether a route with the given name is registered.

func (*Routes) Match

func (t *Routes) Match(req *http.Request) *Route

Match returns the route that answers the request, or nil. It is the table's side of what the mux does: the mux has already picked a route and dispatched it, but Current, a middleware that runs before the handler, and a custom dispatcher all need to find the route from the request, and they do it here.

Non-fallback routes are tried first, in registration order; a fallback route is returned only if nothing else matched.

func (*Routes) Must

func (t *Routes) Must(name string, params ...string) string

Must is Route for the places that cannot handle an error -- a template helper, mostly. It returns the message as the href, so a broken link says what is wrong instead of pointing at "/".

func (*Routes) Refresh

func (t *Routes) Refresh()

Refresh rebuilds the name index from the route list. A name set fluently after the route was added to the table -- through Name, which writes the field and then calls register -- is already indexed; Refresh is for the caller that rewrites names in bulk and needs the index to follow.

func (*Routes) RefreshActionLookups

func (t *Routes) RefreshActionLookups()

RefreshActionLookups is a no-op: the action index is a scan, not a maintained index, so there is nothing to rebuild.

func (*Routes) RefreshNameLookups

func (t *Routes) RefreshNameLookups()

RefreshNameLookups is an alias for Refresh.

func (*Routes) Requirements added in v0.25.0

func (t *Routes) Requirements() []Requirement

Requirements lists the routes that declare a required action, in registration order.

It is the catalogue, and the router is the enumeration. A list of actions kept beside the routes would be wrong the first time somebody added one without remembering it, and a permissions screen reading that list would offer a permission no route requires -- or leave out one that some route does.

The sibling rows of a single registration are left out, so a resource route registered as PUT and PATCH is one requirement rather than two.

func (*Routes) Route

func (t *Routes) Route(name string, params ...string) (string, error)

Route builds the path of a named route, filling the parameters in order.

Route("home")                  -> "/"
Route("invoices.show", "42")   -> "/invoices/42"

A hardcoded "/invoices/"+id compiles and keeps compiling after the route moves. This does not: an unknown name or a wrong number of parameters is an error the caller sees, not a 404 the person sees.

It returns an error rather than panicking, because a URL is often built from data -- and a panic in a template renderer takes the whole page down to report something a broken link would have said better.

type SessionStore

type SessionStore interface {
	Get(key string, def ...any) any
	Put(key string, value any)
	Pull(key string, def ...any) any
	PreviousURL() string
}

SessionStore is what UrlGenerator reads the previous URL and the intended URL from.

Declared here rather than importing hesape/session, so that a package that only generates URLs does not depend on the session implementation.

type Shower

type Shower[C any] interface {
	Show(*C) error
}

Shower answers GET /thing/{id} -- one record.

type Storer

type Storer[C any] interface {
	Store(*C) error
}

Storer answers POST /thing -- the form submission.

type StreamCallback

type StreamCallback func(ctx context.Context, w io.Writer) error

StreamCallback is the callback ResponseFactory.Stream, StreamDownload and EventStream take. It writes to w, which flushes after every write.

The context is the request's: it is done when the client hangs up.

type StreamedResponse

type StreamedResponse struct {
	// Callback writes the body.
	Callback StreamCallback
	// Status is the status code, 200 when zero.
	Status int
	// Headers are sent before the first byte of the body.
	Headers http.Header
	// contains filtered or unexported fields
}

StreamedResponse is a status, a set of headers and a body that is produced while it is being sent. ResponseFactory.Stream, StreamJSON, EventStream and StreamDownload all build one.

The fields are public because the whole point of returning a response instead of writing one is that a caller may still add a header to it.

func (*StreamedResponse) SendContent

func (r *StreamedResponse) SendContent(w http.ResponseWriter, req *http.Request) error

SendContent writes the headers, the status and then the body, flushing as it goes.

It returns what the callback returned, which ServeHTTP cannot: by the time the callback fails the status line is already sent, so there is nowhere left to report it to the client, and a caller that wants to log it needs this.

func (*StreamedResponse) ServeHTTP

func (r *StreamedResponse) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP makes a StreamedResponse an http.Handler, so a route may answer with one directly. It is SendContent with the arguments a handler is given, mirroring how hhttp.Response does it.

type Updater

type Updater[C any] interface {
	Update(*C) error
}

Updater answers PUT and PATCH /thing/{id}.

type UrlGenerator

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

UrlGenerator builds every address an application needs: the route named "invoices.show", the asset at "/css/app.css", the absolute address of the page being viewed.

It is the single answer to "where is this thing". A view that concatenates "/invoices/" with an id compiles and keeps compiling after the route moves; a call through UrlGenerator does not, and the developer finds out at build time rather than from a broken link.

Parameters are a map here. The positional shape -- route("invoices.show", 42), matched against the pattern in order -- is Routes.Route, because a Go map has no order to match against.

func NewUrlGenerator

func NewUrlGenerator(routes *Routes, request *http.Request, assetRoot ...string) *UrlGenerator

NewUrlGenerator returns a generator backed by routes.

The request may be nil for a generator that only builds relative URLs: a CLI command that prints a list of links, or a test that has no incoming request to read the scheme and host from.

func (*UrlGenerator) Action

func (g *UrlGenerator) Action(action string, parameters map[string]any, absolute bool) (string, error)

Action returns the URL of a controller action, written as "InvoiceController@show".

func (*UrlGenerator) Asset

func (g *UrlGenerator) Asset(path string, secure *bool) string

Asset returns the URL of an application asset.

func (*UrlGenerator) AssetFrom

func (g *UrlGenerator) AssetFrom(root, path string, secure *bool) string

AssetFrom returns the URL of an asset served from a custom root, such as a CDN.

func (*UrlGenerator) Current

func (g *UrlGenerator) Current() string

Current returns the URL of the current request without its query string.

func (*UrlGenerator) Defaults

func (g *UrlGenerator) Defaults(defaults map[string]any)

Defaults sets default values for named route parameters -- the locale segment every URL carries, typically.

func (*UrlGenerator) ForceHttps

func (g *UrlGenerator) ForceHttps(force ...bool)

ForceHttps is ForceScheme("https").

func (*UrlGenerator) ForceRootUrl deprecated

func (g *UrlGenerator) ForceRootUrl(root string)

ForceRootUrl sets the origin every generated URL is built from.

Deprecated: use UseOrigin.

func (*UrlGenerator) ForceScheme

func (g *UrlGenerator) ForceScheme(scheme string)

ForceScheme forces every generated URL to a scheme. An empty scheme clears the force.

func (*UrlGenerator) Format

func (g *UrlGenerator) Format(root, path string, route ...*Route) string

Format joins a root and a path into one URL, through the host and path formatters when they are set.

func (*UrlGenerator) FormatHostUsing

func (g *UrlGenerator) FormatHostUsing(callback func(root string, route *Route) string) *UrlGenerator

FormatHostUsing sets the callback Format uses to rewrite the host of a generated URL.

func (*UrlGenerator) FormatParameters

func (g *UrlGenerator) FormatParameters(parameters map[string]any) map[string]any

FormatParameters replaces every value that knows its own route key -- a record passed where an id was expected -- with that key.

This takes the map shape a route's parameters have. The list shape is applied inline by To, over the same conversion.

func (*UrlGenerator) FormatPathUsing

func (g *UrlGenerator) FormatPathUsing(callback func(path string, route *Route) string) *UrlGenerator

FormatPathUsing sets the callback Format uses to rewrite the path of a generated URL.

func (*UrlGenerator) FormatRoot

func (g *UrlGenerator) FormatRoot(scheme string, root ...string) string

FormatRoot returns the base URL -- scheme and host -- for the request, or for the root given.

func (*UrlGenerator) FormatScheme

func (g *UrlGenerator) FormatScheme(secure *bool) string

FormatScheme returns the scheme with its "://" suffix.

func (*UrlGenerator) Full

func (g *UrlGenerator) Full() string

Full returns the full URL of the current request, query string included.

func (*UrlGenerator) GetDefaultParameters

func (g *UrlGenerator) GetDefaultParameters() map[string]any

GetDefaultParameters returns the default route parameters Defaults set.

func (*UrlGenerator) GetRequest

func (g *UrlGenerator) GetRequest() *http.Request

GetRequest returns the current request, or nil.

func (*UrlGenerator) GetRootControllerNamespace

func (g *UrlGenerator) GetRootControllerNamespace() string

GetRootControllerNamespace returns the root controller namespace.

func (*UrlGenerator) HasCorrectSignature

func (g *UrlGenerator) HasCorrectSignature(request *http.Request, absolute bool, ignoreQuery ...string) bool

HasCorrectSignature reports whether the signature on the request matches the address the request arrived on.

Every key the resolver returns is tried, so a key being rotated out still opens the links it signed.

func (*UrlGenerator) HasValidRelativeSignature

func (g *UrlGenerator) HasValidRelativeSignature(request *http.Request, ignoreQuery ...string) bool

HasValidRelativeSignature is HasValidSignature checked against the relative address.

func (*UrlGenerator) HasValidSignature

func (g *UrlGenerator) HasValidSignature(request *http.Request, absolute bool, ignoreQuery ...string) bool

HasValidSignature reports whether the request carries a signature that is both correct and unexpired.

func (*UrlGenerator) IsValidUrl

func (g *UrlGenerator) IsValidUrl(path string) bool

IsValidUrl reports whether path is already a complete URL rather than one this generator should build.

func (*UrlGenerator) PathFormatter

func (g *UrlGenerator) PathFormatter() func(path string, route *Route) string

PathFormatter returns the path formatter in use, or one that changes nothing.

func (*UrlGenerator) Previous

func (g *UrlGenerator) Previous(fallback ...string) string

Previous reads the Referer header first, then the session, then the fallback, then "/".

func (*UrlGenerator) PreviousPath

func (g *UrlGenerator) PreviousPath(fallback ...string) string

PreviousPath returns the path part of Previous, with the application root and any query string removed.

func (*UrlGenerator) Query

func (g *UrlGenerator) Query(path string, query map[string]any, extra []any, secure *bool) string

Query returns the absolute URL for a path with the given query parameters merged onto any the path already carried.

func (*UrlGenerator) ResolveMissingNamedRoutesUsing

func (g *UrlGenerator) ResolveMissingNamedRoutesUsing(resolver func(name string, parameters map[string]any, absolute bool) (string, error)) *UrlGenerator

ResolveMissingNamedRoutesUsing sets the callback that gets a chance to answer a name this table does not know -- a route registered by another service, in front of the same domain.

func (*UrlGenerator) Route

func (g *UrlGenerator) Route(name string, parameters map[string]any, absolute bool) (string, error)

Route returns the URL of a named route.

func (*UrlGenerator) Secure

func (g *UrlGenerator) Secure(path string, parameters ...any) string

Secure returns the absolute https URL for a path.

func (*UrlGenerator) SecureAsset

func (g *UrlGenerator) SecureAsset(path string) string

SecureAsset returns the https URL of an application asset.

func (*UrlGenerator) SetKeyResolver

func (g *UrlGenerator) SetKeyResolver(keyResolver func() []string) *UrlGenerator

SetKeyResolver sets the callback that resolves the signing keys. It returns every key a signature may have been made with, newest first: signing uses the first, verifying tries them all, which is what makes a key rotation something other than an outage.

func (*UrlGenerator) SetRequest

func (g *UrlGenerator) SetRequest(request *http.Request)

SetRequest replaces the request, clears the cached scheme and root, and carries the route defaults over to the new route URL generator.

func (*UrlGenerator) SetRootControllerNamespace

func (g *UrlGenerator) SetRootControllerNamespace(rootNamespace string) *UrlGenerator

SetRootControllerNamespace sets the root controller namespace formatAction qualifies an action name with.

func (*UrlGenerator) SetRoutes

func (g *UrlGenerator) SetRoutes(routes *Routes) *UrlGenerator

SetRoutes replaces the route table URLs are generated against.

func (*UrlGenerator) SetSessionResolver

func (g *UrlGenerator) SetSessionResolver(sessionResolver func() SessionStore) *UrlGenerator

SetSessionResolver sets the callback that resolves the session store.

func (*UrlGenerator) SignatureHasNotExpired

func (g *UrlGenerator) SignatureHasNotExpired(request *http.Request) bool

SignatureHasNotExpired reports whether the request's signature has not expired. A link with no expiry never runs out; one whose expiry is in the past has.

func (*UrlGenerator) SignedRoute

func (g *UrlGenerator) SignedRoute(name string, parameters map[string]any, expiration time.Time, absolute bool) (string, error)

SignedRoute returns the URL of a named route carrying an HMAC over the address it was issued for.

The signature covers the whole address, expiry included, so appending anything to a signed link invalidates it -- a route behind middleware.ValidateSignature cannot be handed a value its author did not sign. A zero expiration means the link does not run out.

It returns an error for reserved parameter names, an unknown route, or no signing key.

func (*UrlGenerator) TemporarySignedRoute

func (g *UrlGenerator) TemporarySignedRoute(name string, expiration time.Time, parameters map[string]any, absolute bool) (string, error)

TemporarySignedRoute is SignedRoute with the expiration and parameters reordered.

func (*UrlGenerator) To

func (g *UrlGenerator) To(path string, extra []any, secure *bool) string

To returns the absolute URL for a path.

To("invoices/42", nil, nil)  // "https://example.com/invoices/42"

A path that is already a valid URL is returned unchanged. Anything in extra is appended as further path segments.

func (*UrlGenerator) ToRoute

func (g *UrlGenerator) ToRoute(route *Route, parameters map[string]any, absolute bool) (string, error)

ToRoute builds the URL of a route already in hand.

func (*UrlGenerator) UseAssetOrigin

func (g *UrlGenerator) UseAssetOrigin(root string)

UseAssetOrigin sets the origin every generated asset URL is built from -- a CDN host, typically.

func (*UrlGenerator) UseOrigin

func (g *UrlGenerator) UseOrigin(root string)

UseOrigin sets the origin every generated URL is built from.

func (*UrlGenerator) WithKeyResolver

func (g *UrlGenerator) WithKeyResolver(keyResolver func() []string) *UrlGenerator

WithKeyResolver returns a copy of the generator signing with a different key, which is how a link is issued for a tenant whose key is not the application's.

type UrlRoutable

type UrlRoutable interface {
	// GetRouteKey returns the value that stands for this record in a URL.
	GetRouteKey() string
	// GetRouteKeyName returns the column that value lives in, "id" unless
	// the route binds by another.
	GetRouteKeyName() string
	// ResolveRouteBinding resolves value into a record. An empty field means
	// GetRouteKeyName. It returns nil when nothing matched, which is a 404
	// and not an error.
	ResolveRouteBinding(ctx context.Context, g auth.Grant, value, field string) (UrlRoutable, error)
	// ResolveChildRouteBinding is the same lookup, restricted to the children
	// of this record, which is what a scoped nested resource resolves
	// through.
	ResolveChildRouteBinding(ctx context.Context, g auth.Grant, childType, value, field string) (UrlRoutable, error)
}

UrlRoutable is what a record implements to be reachable from a URL: it knows the value that stands for it in a path, the column that value lives in, and how to find itself again from that value.

The tenant comes from the Grant, never from the URL

ResolveRouteBinding receives an auth.Grant and MUST filter by auth.Tenant(g). This is the exact place a multi-tenant application leaks: /invoices/9 arrives, the id is read off the path, a row is loaded by that id alone, and the reader is handed another customer's invoice. No policy was violated -- none was consulted. The implementation is one WHERE clause:

func (Invoice) ResolveRouteBinding(ctx context.Context, g auth.Grant, value, field string) (routing.UrlRoutable, error) {
	if field == "" {
		field = Invoice{}.GetRouteKeyName()
	}
	return invoices.FindBy(ctx, g, field, value) // scoped by auth.Tenant(g)
}

The Grant is a parameter and not something read out of the context, because a parameter cannot be forgotten: a resolver written without it does not compile.

type ViewRenderer

type ViewRenderer interface {
	// Render writes the rendered view to w. The status is the one a View
	// route answers with, 200 by default, and headers are the caller's to
	// add (the CSRF header, for instance).
	Render(w io.Writer, name string, data any, status int, headers http.Header) error
}

ViewRenderer renders a named view with data. It is the minimal contract Router.View needs, and it is here rather than importing hesape/view because that import would make routing depend on the view layer -- and the view layer depends on routing for URL generation, which is a cycle. The concrete is hesape/view's Factory, wired by the kernel through SetViewRenderer.

The contract is what a redirect-to-view route touches: make a view by name, hand it data, and let it write the response. How the view renders is the view layer's concern.

Directories

Path Synopsis
Package console is empty, and stays that way: console commands live in the project's own binary, through the switch in bootstrap/console.go, rather than being registered here.
Package console is empty, and stays that way: console commands live in the project's own binary, through the switch in bootstrap/console.go, rather than being registered here.
Package contracts is empty, and stays that way: an interface lives in the package that consumes it, not in one of its own.
Package contracts is empty, and stays that way: an interface lives in the package that consumes it, not in one of its own.
Package controllers holds the contract a controller uses to declare its own middleware: HasMiddleware, and the MiddlewareDef pairs it returns.
Package controllers holds the contract a controller uses to declare its own middleware: HasMiddleware, and the MiddlewareDef pairs it returns.
Package exceptions holds the error types a route, a signed URL or a streamed response can fail with: an invalid signature, a missing rate limiter, a route URL that is missing required parameters, a backed-enum parameter that matched no case, and a streamed response that failed after the headers were already sent.
Package exceptions holds the error types a route, a signed URL or a streamed response can fail with: an invalid signature, a missing rate limiter, a route URL that is missing required parameters, a backed-enum parameter that matched no case, and a streamed response that failed after the headers were already sent.
Package matching holds the four checks that decide whether a route answers a request: its host, its method, its scheme and its path.
Package matching holds the four checks that decide whether a route answers a request: its host, its method, its scheme and its path.
Package middleware holds the two middlewares that decide whether a request reaches a route at all.
Package middleware holds the two middlewares that decide whether a request reaches a route at all.

Jump to

Keyboard shortcuts

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