//
// Copyright (c) 2011, Andy Frank
// Licensed under the MIT License
//
// History:
// 14 May 2011 Andy Frank Creation
//
using web
**************************************************************************
** Router
**************************************************************************
**
** Router handles routing URIs to method handlers.
**
const class Router
{
** Constructor.
new make(|This| f)
{
f(this)
this.routes = Route.sort(routes)
}
** RouteGroup configuration.
const RouteGroup[] groups := [,]
** Route configuration.
const Route[] routes := [,]
** Match a request to Route. If no matches are found, returns
** 'null'. The first route that matches is chosen. Routes
** from `groups` are matched before `routes`. Literal routes
** are matched before parameterized routes.
RouteMatch? match(Uri uri, Str method)
{
for (i:=0; i<groups.size; i++)
{
g := groups[i]
m := _match(g.meta, g.routes, uri, method)
if (m != null) return m
}
return _match(null, routes, uri, method)
}
private RouteMatch? _match([Str:Obj]? meta, Route[] list, Uri uri, Str method)
{
for (i:=0; i<list.size; i++)
{
r := list[i]
m := r.match(uri, method)
if (m != null) return RouteMatch(meta, r, m)
}
return null
}
}
**************************************************************************
** RouteGroup
**************************************************************************
**
** RouteGroup models a set of Routes with optional meta-data.
** If any Routes are matched in a RouteGroup, the meta-data
** will be stored and available in:
**
** Str:Obj meta := req.stash["draft.route.meta"]
**
const class RouteGroup
{
** It-block ctor.
new make(|This| f)
{
f(this)
this.routes = Route.sort(routes)
}
** Meta-data for this group.
const Str:Obj meta := [:]
** Routes for this group.
const Route[] routes
}
**************************************************************************
** Route
**************************************************************************
**
** Route models how a URI pattern gets routed to a method handler.
** Example patterns:
**
** Pattern Uri Args
** -------------- ------------ ----------
** "/" `/` [:]
** "/foo/{bar}" `/foo/12` ["bar":"12"]
** "/foo/*" `/foo/x/y/z` [:]
** "/foo/{bar}/*" `/foo/x/y/z` ["bar":"x"]
**
const class Route
{
** Constructor.
new make(Str pattern, Str method, Method handler)
{
this.pattern = pattern
this.method = method
this.handler = handler
try
{
this.tokens = pattern == "/"
? RouteToken#.emptyList
: pattern[1..-1].split('/').map |v| { RouteToken(v) }
varIndex := tokens.findIndex |t| { t.type == RouteToken.vararg }
if (varIndex != null && varIndex != tokens.size-1) throw Err()
this.isLiteral = tokens.all |t| { t.type == RouteToken.literal }
}
catch (Err err) throw ArgErr("Invalid pattern $pattern.toCode", err)
}
** URI pattern for this route.
const Str pattern
// TODO FIXIT: confusing b/w HTTP method and Method Handler
** HTTP method used for this route.
const Str method
** Method handler for this route. If this method is an instance
** method, a new intance of the parent type is created before
** invoking the method.
const Method handler
** Match this route against the request arguments. If route can
** be be matched, return the pattern arguments, or return 'null'
** for no match.
[Str:Str]? match(Uri uri, Str method)
{
// if methods not equal, no match
if (method != this.method) return null
// if size unequal, we know there is no match
path := uri.path
if (tokens.last?.type == RouteToken.vararg)
{
if (path.size < tokens.size) return null
}
else if (tokens.size != path.size) return null
// iterate tokens looking for matches
map := Str:Str[:]
for (i:=0; i<path.size; i++)
{
p := path[i]
t := tokens[i]
switch (t.type)
{
case RouteToken.literal: if (t.val != p) return null
case RouteToken.arg: map[t.val] = p
case RouteToken.vararg: break
}
}
return map
}
** Sort a list of routes by bubbling literals to top, and
** maintaining existing order for remaining routes.
internal static Route[] sort(Route[] routes)
{
lits := routes.findAll |r| { r.isLiteral }
if (lits.isEmpty) return routes
copy := routes.dup.rw
lits.eachr |r| { copy.moveTo(r, 0) }
return copy.toImmutable
}
** 'toStr' is `pattern`.
override Str toStr() { pattern }
** Is the route all literal tokens (no args or patterns)?
internal const Bool isLiteral
** Parsed tokens.
private const RouteToken[] tokens
}
**************************************************************************
** RouteToken
**************************************************************************
**
** RouteToken models each path token in a URI pattern.
**
internal const class RouteToken
{
** Constructor.
new make(Str val)
{
if (val[0] == '*')
{
this.val = val
this.type = vararg
}
else if (val[0] == '{' && val[-1] == '}')
{
this.val = val[1..-2]
this.type = arg
}
else
{
this.val = val
this.type = literal
}
}
** Token type.
const Int type
** Token value.
const Str val
** Str value is "$type:$val".
override Str toStr() { "$type:$val" }
** Type id for a literal token.
static const Int literal := 0
** Type id for an argument token.
static const Int arg := 1
** Type id for vararg token.
static const Int vararg := 2
}
**************************************************************************
** RouteMatch
**************************************************************************
**
** RouteMatch models a matched Route instance.
**
const class RouteMatch
{
** Constructor
new make([Str:Obj]? meta, Route route, Str:Str args)
{
this.meta = meta
this.route = route
this.args = args
}
** Optional meta-data for match.
const [Str:Obj]? meta
** Matched route instance.
const Route route
** Arguments for matched Route.
const Str:Str args
}