From 2d736313fa4a05f074f7f16a6fa46b6e471761ad Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 11 Dec 2014 21:33:24 -0500 Subject: [PATCH] lib/nitcorn: introduces vararg_routes module This will allow the creation of parameterized routes like `/users/:id`. Signed-off-by: Alexandre Terrasa --- lib/nitcorn/vararg_routes.nit | 228 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 lib/nitcorn/vararg_routes.nit diff --git a/lib/nitcorn/vararg_routes.nit b/lib/nitcorn/vararg_routes.nit new file mode 100644 index 0000000..9099284 --- /dev/null +++ b/lib/nitcorn/vararg_routes.nit @@ -0,0 +1,228 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014 Alexandre Terrasa +# +# 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 -- 1.7.9.5