lib/nitcorn: introduces vararg_routes module
authorAlexandre Terrasa <alexandre@moz-code.org>
Fri, 12 Dec 2014 02:33:24 +0000 (21:33 -0500)
committerAlexandre Terrasa <alexandre@moz-code.org>
Fri, 12 Dec 2014 22:41:37 +0000 (17:41 -0500)
This will allow the creation of parameterized routes like `/users/:id`.

Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

lib/nitcorn/vararg_routes.nit [new file with mode: 0644]

diff --git a/lib/nitcorn/vararg_routes.nit b/lib/nitcorn/vararg_routes.nit
new file mode 100644 (file)
index 0000000..9099284
--- /dev/null
@@ -0,0 +1,228 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Routes with uri parameters.
+#
+# Using `vararg_routes`, a `Route` can contain variable parts
+# that will be matched against a `HttpRequest` path.
+#
+# Variable parts of path can be specified using the `:` prefix.
+#
+# ## Route matching
+#
+# Route can match variables expression.
+#
+# ~~~
+# # We need an Action to try routes.
+# class DummyAction super Action end
+# var action = new DummyAction
+#
+# var route = new Route("/users/:id", action)
+# assert not route.match("/users")
+# assert route.match("/users/1234")
+# assert route.match("/users/") # empty id
+# ~~~
+#
+# Route without uri parameters still behave like before.
+#
+# ~~~
+# route = new Route("/users", action)
+# assert route.match("/users")
+# assert route.match("/users/1234")
+# assert not route.match("/issues/1234")
+# ~~~
+#
+# ## Route priority
+#
+# Priority depends on the order the routes were added to the `Routes` dispatcher.
+#
+# ~~~
+# var host = new VirtualHost("")
+# var routes = new Routes(host)
+#
+# routes.add new Route("/:a/:b/:c", action)
+# routes.add new Route("/users/:id", action)
+# routes.add new Route("/:foo", action)
+#
+# assert routes["/a/b/c"].path == "/:a/:b/:c"
+# assert routes["/a/b/c/d"].path == "/:a/:b/:c"
+# assert routes["/users/1234/foo"].path == "/:a/:b/:c"
+#
+# assert routes["/users/"].path == "/users/:id"
+# assert routes["users/"].path == "/users/:id"
+# assert routes["/users/1234"].path == "/users/:id"
+#
+# assert routes["/users"].path == "/:foo"
+# assert routes["/"].path == "/:foo"
+# assert routes[""].path == "/:foo"
+# ~~~
+#
+# ## Accessing uri parameter and values
+#
+# Parameters can be accessed by parsing the uri.
+#
+# ~~~
+# route = new Route("/users/:id", action)
+# var params = route.parse_params("/users/1234")
+# assert params.has_key("id")
+# assert not params.has_key("foo")
+# assert params["id"] == "1234"
+# ~~~
+#
+# Or from the `HttpRequest`.
+#
+# ~~~
+# route = new Route("/users/:id", action)
+# var req = new HttpRequest
+# req.uri_params = route.parse_params("/users/1234")
+# assert req.params == ["id"]
+# assert req.param("id") == "1234"
+# assert req.param("foo") == null
+# ~~~
+#
+# Note that normally, all this work is done by nitcorn.
+# Params can then be accessed in the `HttpRequest` given to `Action::answer`.
+module vararg_routes
+
+import server_config
+import http_request
+
+# A route to an `Action` according to a `path`
+redef class Route
+
+       redef init do
+               super
+               parse_pattern(path)
+       end
+
+       # Cut `path` into `UriParts`.
+       private fun parse_pattern(path: nullable String) do
+               if path == null then return
+               path = standardize_path(path)
+               var parts = path.split("/")
+               for part in parts do
+                       if not part.is_empty and part.first == ':' then
+                               # is an uri param
+                               var name = part.substring(1, part.length)
+                               var param = new UriParam(name)
+                               pattern_parts.add param
+                       else
+                               # is a standard string
+                               pattern_parts.add new UriString(part)
+                       end
+               end
+       end
+
+       # `UriPart` forming `self` pattern.
+       private var pattern_parts = new Array[UriPart]
+
+       # Does `self` matches `uri`?
+       fun match(uri: nullable String): Bool do
+               if pattern_parts.is_empty then return true
+               if uri == null then return false
+               uri = standardize_path(uri)
+               var parts = uri.split("/")
+               for i in [0 .. pattern_parts.length[ do
+                       if i >= parts.length then return false
+                       var ppart = pattern_parts[i]
+                       var part = parts[i]
+                       if not ppart.match(part) then return false
+               end
+               return true
+       end
+
+       # Extract parameter values from `uri`.
+       fun parse_params(uri: nullable String): Map[String, String] do
+               var res = new HashMap[String, String]
+               if pattern_parts.is_empty then return res
+               if uri == null then return res
+               uri = standardize_path(uri)
+               var parts = uri.split("/")
+               for i in [0 .. pattern_parts.length[ do
+                       if i >= parts.length then return res
+                       var ppart = pattern_parts[i]
+                       var part = parts[i]
+                       if not ppart.match(part) then return res
+                       if ppart isa UriParam then
+                               res[ppart.name] = part
+                       end
+               end
+               return res
+       end
+
+       # Remove first occurence of `/`.
+       private fun standardize_path(path: String): String do
+               if not path.is_empty and path.first == '/' then
+                       return path.substring(1, path.length)
+               end
+               return path
+       end
+end
+
+# A String that compose an URI.
+#
+# In practice, UriPart can be parameters or static strings.
+private interface UriPart
+       # Does `self` matches a part of the uri?
+       fun match(uri_part: String): Bool is abstract
+end
+
+# An uri parameter string like `:id`.
+private class UriParam
+       super UriPart
+
+       # Param `name` in the route uri.
+       var name: String
+
+       # Parameters match everything.
+       redef fun match(part) do return true
+end
+
+# A static uri string like `users`.
+private class UriString
+       super UriPart
+
+       # Uri part string.
+       var string: String
+
+       # Empty strings match everything otherwise matching is based on string equality.
+       redef fun match(part) do return string.is_empty or string == part
+end
+
+redef class Routes
+       # Use `Route::match` instead of `==`.
+       redef fun [](key) do
+               for route in routes do
+                       if route.match(key) then return route
+               end
+               return null
+       end
+end
+
+redef class HttpRequest
+
+       # Parameters found in uri associated to their values.
+       var uri_params: Map[String, String] = new HashMap[String, String] is public writable
+
+       # Get the value for parameter `name` or `null`.
+       fun param(name: String): nullable String do
+               if not uri_params.has_key(name) then return null
+               return uri_params[name]
+       end
+
+       # List all uri parameters matched by this request.
+       fun params: Array[String] do return uri_params.keys.to_a
+end