ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Server.Route

Description

A route: one record saying everything there is to say about one URL the proxy serves.

A Route carries its method condition, its path template (literal segments and named captures that parse themselves), what to do when it matches, and its documentation. An ecosystem's routing table is then simply a list of these values (Ecluse.Core.Registry.Npm.Route is npm's), and routerOf folds that list into the mount's router: first match wins, no match is the deny-by-default 404.

There is no route sum. A classified-route type would have to be matched again to decide what to do about it, and again to document it, and each of those matches is somewhere the three can fall out of step. Here the pattern, the action, and the documentation are the same value, so they cannot disagree, and the manifest renders RouteSpec projections of the very records the router runs.

What stays a named function

The engine owns the structure: literal matching, capture arity, ordering, exact consumption. It does not infer an ecosystem's semantics. A Capture carries its own segment parser and a Route its own builder, so the security-critical leaf logic (the component-safety gate, an ecosystem's scoped-name decoding, a version parse, the cross-capture path-confusion check) stays in named, reviewed, separately-tested functions that the record references, rather than being regenerated from a generic template.

Synopsis

A route

data Route v Source #

One route, whole: how it matches, what it does, and what it means.

Generic over the ecosystem's capture-value type v, which is the only thing about a route that is not shared (npm's captures yield a parsed package or an artifact name; another registry's would yield its own).

Constructors

Route 

Fields

  • routeName :: RouteName

    This route's name, unique within its ecosystem ("packument"). It is the handle a test asserts on when it checks which route a request took, and the manifest qualifies it by ecosystem to form OpenAPI's operationId (which must be unique across the whole document, so only the manifest, which sees every mount at once, can guarantee it).

  • routeMethod :: MethodMatch

    The method condition a request must satisfy to match.

  • routeSegs :: [PatternSeg v]

    The mount-relative path template: literal segments and named captures, in order.

  • routeBuild :: Method -> [v] -> Maybe (ResponseAction response)

    What serving this route amounts to, given the request method and the captured values (one per SegCap, in template order).

    Nothing denies: the route does not claim this request after all, and matching falls through to the next route (and, failing all of them, to the 404). That is where a cross-capture check lives, e.g. an artifact file name that must parse for the package captured earlier: a name addressing some other package's artifact is refused rather than fabricated into a coordinate.

    The Method is passed because a HEAD is a bodiless variation of its GET rather than a distinct route: it matches the same pattern, and the builder selects the head-mode handler.

  • routeSummary :: Text

    A one-line summary (the OpenAPI operation summary).

  • routeDescription :: Text

    The fuller prose description of what the route does.

  • routeRequest :: Maybe RequestSpec

    The request body a write route accepts; Nothing for a read.

  • routeContract :: ResponseContract response

    The response contract whose indexed value the builder's action can produce. Runtime dispatch renders that value to WAI while the manifest renders the same contract's response documents, so the action and documentation cannot be paired with different response sets.

newtype RouteName Source #

A route's name within its ecosystem ("packument", "tarball"). Not qualified: the route already lives in its ecosystem's table, and the manifest adds the namespace when it needs a globally unique identifier.

Constructors

RouteName 

Fields

data PatternSeg v Source #

One segment of a path template: a fixed segment matched verbatim, or a named capture that consumes one or more leading segments and yields a value.

Constructors

SegLit Text 
SegCap (Capture v) 

data Capture v Source #

A named path capture: how it parses (the security-critical leaf) and how it documents. capConsume may consume more than one segment (an ecosystem whose identifier spans a decoded '/' needs this) and returns the unconsumed tail, so captures thread left to right; Nothing fails the match, and the request falls through to the next route or to the deny-by-default catch-all.

Constructors

Capture 

Fields

  • capName :: Text

    The capture name, as it appears in the template ({package}).

  • capDescription :: Text

    A one-line, human-facing description for the documentation.

  • capConsume :: [Text] -> Maybe (v, [Text])

    Consume the leading segments this capture claims, yielding its value and the tail.

data MethodMatch Source #

The method condition on a route: the read methods (GET and HEAD), or the one client write (PUT).

Any other method matches no route and therefore denies (deny by default): the front door answers only the methods it was taught, so a DELETE or POST over a package path is a 404 rather than being read as a package request. This also keeps the documented method honest: the manifest says GET for a read, and only a GET (or its bodiless HEAD) is served.

Kept as a small closed vocabulary rather than a bare predicate so the manifest can still name the documented method.

Constructors

MethodPut

The write method (PUT).

MethodRead

The read methods (GET and HEAD).

Instances

Instances details
Show MethodMatch Source # 
Instance details

Defined in Ecluse.Core.Server.Route

Eq MethodMatch Source # 
Instance details

Defined in Ecluse.Core.Server.Route

Routing a request

routerOf :: RouteAction -> [Route v] -> MountRouter Source #

Fold an ecosystem's route table into its mount's router: the first route that claims the request decides what is done with it, and a request no route claims is the deny-by-default 404 in the mount's own error surface.

Deny-by-default is structural here: routerOf has no other way to answer. There is no catch-all branch to forget. The 404 Answer a mount supplies for a path no route claims is its deny-by-default surface (npm's {"error": "not found"}).

matchRoute :: [Route v] -> Method -> [Text] -> Maybe (Route v, RouteAction) Source #

The route that claims a request, and the action it names: the first whose method condition holds, whose segments are consumed exactly, and whose builder accepts the captures. Nothing when none does.

Exported beside routerOf because it is what makes a routing table testable with no server: feed it a method and segments and assert which route won (by its routeName), or that none did. The action itself is a closure and is exercised through the serve path.