PageRenderTime 7ms CodeModel.GetById 1ms app.highlight 2ms RepoModel.GetById 1ms app.codeStats 0ms

/src/fan/Router.fan

https://bitbucket.org/afrankvt/draft/
Unknown | 240 lines | 200 code | 40 blank | 0 comment | 0 complexity | 0f16b236ed36debbb7c3f06fd79dcabc MD5 | raw file
  1//
  2// Copyright (c) 2011, Andy Frank
  3// Licensed under the MIT License
  4//
  5// History:
  6//   14 May 2011  Andy Frank  Creation
  7//
  8
  9using web
 10
 11**************************************************************************
 12** Router
 13**************************************************************************
 14
 15**
 16** Router handles routing URIs to method handlers.
 17**
 18const class Router
 19{
 20  ** Constructor.
 21  new make(|This| f) { f(this) }
 22
 23  ** RouteGroup configuration.
 24  const RouteGroup[] groups := [,]
 25
 26  ** Route configuration.
 27  const Route[] routes := [,]
 28
 29  ** Match a request to Route. If no matches are found, returns
 30  ** 'null'.  The first route that matches is chosen.  Routes
 31  ** from `groups` are matched before `routes`.
 32  RouteMatch? match(Uri uri, Str method)
 33  {
 34    for (i:=0; i<groups.size; i++)
 35    {
 36      g := groups[i]
 37      m := _match(g.meta, g.routes, uri, method)
 38      if (m != null) return m
 39    }
 40
 41    return _match(null, routes, uri, method)
 42  }
 43
 44  private RouteMatch? _match([Str:Obj]? meta, Route[] list, Uri uri, Str method)
 45  {
 46    for (i:=0; i<list.size; i++)
 47    {
 48      r := list[i]
 49      m := r.match(uri, method)
 50      if (m != null) return RouteMatch(meta, r, m)
 51    }
 52    return null
 53  }
 54}
 55
 56**************************************************************************
 57** RouteGroup
 58**************************************************************************
 59
 60**
 61** RouteGroup models a set of Routes with optional meta-data.
 62** If any Routes are matched in a RouteGroup, the meta-data
 63** will be stored and available in:
 64**
 65**   Str:Obj meta := req.stash["draft.route.meta"]
 66**
 67const class RouteGroup
 68{
 69  ** It-block ctor.
 70  new make(|This| f) { f(this) }
 71
 72  ** Meta-data for this group.
 73  const Str:Obj meta := [:]
 74
 75  ** Routes for this group.
 76  const Route[] routes
 77}
 78
 79**************************************************************************
 80** Route
 81**************************************************************************
 82
 83**
 84** Route models how a URI pattern gets routed to a method handler.
 85** Example patterns:
 86**
 87**   Pattern         Uri           Args
 88**   --------------  ------------  ----------
 89**   "/"             `/`           [:]
 90**   "/foo/{bar}"    `/foo/12`     ["bar":"12"]
 91**   "/foo/*"        `/foo/x/y/z`  [:]
 92**   "/foo/{bar}/*"  `/foo/x/y/z`  ["bar":"x"]
 93**
 94const class Route
 95{
 96  ** Constructor.
 97  new make(Str pattern, Str method, Method handler)
 98  {
 99    this.pattern = pattern
100    this.method  = method
101    this.handler = handler
102
103    try
104    {
105      this.tokens = pattern == "/"
106        ? RouteToken#.emptyList
107        : pattern[1..-1].split('/').map |v| { RouteToken(v) }
108
109      varIndex := tokens.findIndex |t| { t.type == RouteToken.vararg }
110      if (varIndex != null && varIndex != tokens.size-1) throw Err()
111
112    }
113    catch (Err err) throw ArgErr("Invalid pattern $pattern.toCode", err)
114  }
115
116  ** URI pattern for this route.
117  const Str pattern
118
119// TODO FIXIT: confusing b/w HTTP method and Method Handler
120  ** HTTP method used for this route.
121  const Str method
122
123  ** Method handler for this route.  If this method is an instance
124  ** method, a new intance of the parent type is created before
125  ** invoking the method.
126  const Method handler
127
128  ** Match this route against the request arguments.  If route can
129  ** be be matched, return the pattern arguments, or return 'null'
130  ** for no match.
131  [Str:Str]? match(Uri uri, Str method)
132  {
133    // if methods not equal, no match
134    if (method != this.method) return null
135
136    // if size unequal, we know there is no match
137    path := uri.path
138    if (tokens.last?.type == RouteToken.vararg)
139    {
140      if (path.size < tokens.size) return null
141    }
142    else if (tokens.size != path.size) return null
143
144    // iterate tokens looking for matches
145    map := Str:Str[:]
146    for (i:=0; i<path.size; i++)
147    {
148      p := path[i]
149      t := tokens[i]
150      switch (t.type)
151      {
152        case RouteToken.literal: if (t.val != p) return null
153        case RouteToken.arg:     map[t.val] = p
154        case RouteToken.vararg:  break
155      }
156    }
157
158    return map
159  }
160
161  ** Parsed tokens.
162  private const RouteToken[] tokens
163}
164
165**************************************************************************
166** RouteToken
167**************************************************************************
168
169**
170** RouteToken models each path token in a URI pattern.
171**
172internal const class RouteToken
173{
174  ** Constructor.
175  new make(Str val)
176  {
177    if (val[0] == '*')
178    {
179      this.val = val
180      this.type = vararg
181    }
182    else if (val[0] == '{' && val[-1] == '}')
183    {
184      this.val  = val[1..-2]
185      this.type = arg
186    }
187    else
188    {
189      this.val  = val
190      this.type = literal
191    }
192  }
193
194  ** Token type.
195  const Int type
196
197  ** Token value.
198  const Str val
199
200  ** Str value is "$type:$val".
201  override Str toStr() { "$type:$val" }
202
203  ** Type id for a literal token.
204  static const Int literal := 0
205
206  ** Type id for an argument token.
207  static const Int arg := 1
208
209  ** Type id for vararg token.
210  static const Int vararg := 2
211}
212
213**************************************************************************
214** RouteMatch
215**************************************************************************
216
217**
218** RouteMatch models a matched Route instance.
219**
220const class RouteMatch
221{
222  ** Constructor
223  new make([Str:Obj]? meta, Route route, Str:Str args)
224  {
225    this.meta = meta
226    this.route = route
227    this.args  = args
228  }
229
230  ** Optional meta-data for match.
231  const [Str:Obj]? meta
232
233  ** Matched route instance.
234  const Route route
235
236  ** Arguments for matched Route.
237  const Str:Str args
238}
239
240