** Matches HTTP Requests to response objects.
**
** URL matching is case-insensitive and trailing slashes that denote index or directory directory
** pages are ignored.
**
**
**
** Response Objects
** ================
** A 'Route' may return *any* response object, be it `Text`, `HttpStatus`, 'File', or any other.
** It simply returns whatever is passed into the ctor.
**
** Example, this matches the URL '/greet' and returns the string 'Hello Mum!'
**
** syntax: fantom
** Route(`/greet`, Text.fromPlain("Hello Mum!"))
**
** And this redirects any request for '/home' to '/greet'
**
** syntax: fantom
** Route(`/home`, HttpRedirect.movedTemporarily(`/greet`))
**
** You can use glob expressions in your URL, so:
**
** syntax: fantom
** Route(`/greet/*`, ...)
**
**
**
** Response Methods
** ================
** Routes may return `MethodCall` instances that call a Fantom method.
** To use, pass in the method as the response object.
** On a successful match, the 'Route' will convert the method into a 'MethodCall' object.
**
** syntax: fantom
** Route(`/greet`, MyPage#hello)
**
** Method matching can also map URL path segments to method parameters and is a 2 stage process:
**
** Stage 1 - URL Matching
** ----------------------
** Wildcards are used to capture string sections from the request URL to be used as method arguments.
**
** Wildcard syntax is:
** - '/*' captures a path segment
** - '/**' captures all remaining path segments
**
** Examples:
**
** URL glob captures
** ------------ --- ---------- -- -------------
** /user/ --> /user/* => default(*)
** /user/42 --> /user/* => "42"
** /user/42/ --> /user/* => "42"
** /user/42/dee --> /user/* => no match
**
** /user/ --> /user/** => default(*)
** /user/42 --> /user/** => "42"
** /user/42/ --> /user/** => "42"
** /user/42/dee --> /user/** => "42/dee"
**
** '(*)' If the corresponding method argument has a default value, it is taken, otherwise no match.
**
** Assuming you you have an entity object, such as 'User', with an ID field; you can contribute a
** 'ValueEncoder' that inflates (or otherwise reads from a database) 'User' objects from a string
** version of the ID. Then your methods can declare 'User' as a parameter and BedSheet will
** convert the captured strings to User objects for you!
**
**
**
** Method Invocation
** -----------------
** Handler methods may be non-static.
** They they belong to an IoC service then the service is obtained from the IoC registry.
** Otherwise the containing class is [autobuilt]`afIoc::Scope.build`.
** If the class is 'const', the instance is cached for future use.
**
const class Route {
internal const Uri _urlGlob
internal const Str _httpMethod
internal const Obj _response
** Creates a Route that matches on the given URL glob pattern.
** 'urlGlob' must start with a slash "/". Example:
**
** syntax: fantom
** Route(`/index/**`)
**
** 'httpMethod' may specify multiple HTTP method separated by a space.
**
** syntax: fantom
** Route(`/index/**`, MyClass#myMethod, "GET HEAD")
**
new make(Uri url, Obj response, Str httpMethod := "GET") {
if (url.pathOnly != url)
throw ArgErr("Route `$url` must only contain a path. e.g. `/foo/bar`")
if (url.isPathRel)
throw ArgErr("Route `$url` must start with a slash. e.g. `/foo/bar`")
this._urlGlob = url
this._httpMethod = httpMethod
this._response = response is Method ? RouteMethod(response) : response
}
** A hint at what this route matches on. Used for debugging and in 404 / 500 error pages.
virtual Str matchHint() {
_httpMethod.justl(4) + " ${_urlGlob}"
}
** A hint at what response this route returns. Used for debugging and in 404 / 500 error pages.
virtual Str responseHint() {
_response.toStr
}
** Creates additional Routes that match default method arguments
@NoDoc // I thought I'd need this for Pillow - I don't
internal Route[]? _defRoutes(Method method) {
path := _urlGlob.path
numWildcards := 0
for (i := 0; i < path.size; ++i) {
if (path[i] == "*" || path[i] == "**")
numWildcards++
}
if (numWildcards > method.params.size)
throw ArgErr(msg_uriWillNeverMatchMethod(_urlGlob, method))
numMinArgs := 0
for (i := 0; i < method.params.size; ++i) {
if (method.params[i].hasDefault == false)
numMinArgs++
}
if (numWildcards < numMinArgs)
throw ArgErr(msg_uriWillNeverMatchMethod(_urlGlob, method))
if (numMinArgs == method.params.size)
return null
routes := Route[,]
numWild := 0
for (i := 0; i < path.size; ++i) {
if (path[i] == "*" || path[i] == "**") {
if (numWild >= numMinArgs) {
url := ``
for (x := 0; x < i; ++x) {
url = url.plusSlash.plusName(path[x])
}
routes.add(Route(url, _response, _httpMethod))
}
numWild++
}
}
return routes
}
override Str toStr() {
"${matchHint} : ${responseHint}"
}
private static Str msg_uriWillNeverMatchMethod(Uri url, Method method) {
"Route URL `${url}` will never match method ${method.parent.qname} " + method.signature.replace("sys::", "")
}
}