Skip to content

Instantly share code, notes, and snippets.

@andreaseger
Last active January 30, 2017 22:23
Show Gist options
  • Save andreaseger/3484c5c6f5fca3c8ea9462cc33c76646 to your computer and use it in GitHub Desktop.
Save andreaseger/3484c5c6f5fca3c8ea9462cc33c76646 to your computer and use it in GitHub Desktop.
defmodule DynamicRoutingTest do
use ExUnit.Case
doctest DynamicRouting
test "match" do
:routes = :ets.new(:routes, [:named_table, :bag, :public])
method = :get
service = :articles
[["v1", "users"],
["v2", "users"],
["v1", "users", :_],
["v2", "users", :_],
["v2", "users", :_, "posts"],
["v1", "users", :_, "posts"],
["v1", "users", :_, "posts", :_],
["v2", "users", :_, "posts", :_]
] |> Enum.each(fn(r) ->
:ets.insert(:routes, {service, method, Enum.count(r), r, %{}})
end)
path_info = ["v2", "users", "123", "posts", "grgr"]
assert match(service, method, path_info) == {:ok, ["v2", "users", :_, "posts", :_], %{}}
path_info = ["v1", "users", "123", "posts", "grgr"]
assert match(service, method, path_info) == {:ok, ["v1", "users", :_, "posts", :_], %{}}
path_info = ["v1", "users", "123", "posts"]
assert match(service, method, path_info) == {:ok, ["v1", "users", :_, "posts"], %{}}
assert match(:photos, method, path_info) == {:error, :no_route}
end
end
def match(service, method, path_info) do
path_length = Enum.count(path_info)
with {:ok, routes} <- fetch_routes(service, method, path_length),
{route, config} <- Enum.find(routes, fn({r,_}) -> match_route(r, path_info) end)
do
{:ok, route, config}
else
_ -> {:error, :no_route}
end
def fetch_routes(service, method, path_length) do
case :ets.match_object(:routes, {service, method, path_length, :_, :_}) do
[] -> {:error, :no_routes}
var -> {:ok, Enum.map(var, fn({_,_,_,r,c}) -> {r,c} end)}
end
end
def match_route([],[]), do: true
def match_route([:_|route], [_|path_info]), do: match_route(route, path_info)
def match_route([h|route], [h|path_info]), do: match_route(route, path_info)
def match_route([_h|_route], [_h2|_path_info]), do: false
end
defmodule DynamicRouting.Utils do
@doc """
Generates a representation that will only match routes
according to the given `spec`.
If a non-binary spec is given, it is assumed to be
custom match arguments and they are simply returned.
## Examples
iex> DynamicRouter.Utils.build_path_match("/foo/:id")
["foo", :_]
"""
def build_path_match(spec \\ nil) when is_binary(spec) do
build_path_match split(spec), []
end
@doc """
Splits the given path into several segments.
It ignores both leading and trailing slashes in the path.
## Examples
iex> DynamicRouter.Utils.split("/foo/bar")
["foo", "bar"]
iex> DynamicRouter.Utils.split("/:id/*")
[":id", "*"]
iex> DynamicRouter.Utils.split("/foo//*_bar")
["foo", "*_bar"]
"""
def split(bin) do
for segment <- String.split(bin, "/"), segment != "", do: segment
end
# Loops each segment checking for matches.
defp build_path_match([h|t], acc) do
handle_segment_match segment_match(h, ""), t, acc
end
defp build_path_match([], acc) do
Enum.reverse(acc)
end
# Handle each segment match. They can either be a
# :literal ("foo"), an :identifier (":bar") or a :glob ("*path")
defp handle_segment_match({:literal, literal}, t, acc) do
build_path_match t, [literal|acc]
end
defp handle_segment_match({:identifier, expr}, t, acc) do
build_path_match t, [expr|acc]
end
defp handle_segment_match({:glob, _expr}, t, _acc) when t != [] do
raise Plug.Router.InvalidSpecError,
message: "cannot have a *glob followed by other segments"
end
defp handle_segment_match({:glob, expr}, _t, [hs|ts]) do
acc = [{:|, [], [hs, expr]} | ts]
build_path_match([], acc)
end
defp handle_segment_match({:glob, expr}, _t, _) do
expr = build_path_match([], [expr])
hd(expr)
end
# In a given segment, checks if there is a match.
defp segment_match(":" <> _argument, _buffer) do
{:identifier, :_}
end
defp segment_match("*" <> _argument, _buffer) do
{:glob, :_}
end
defp segment_match(<<h, t::binary>>, buffer) do
segment_match t, buffer <> <<h>>
end
defp segment_match(<<>>, buffer) do
{:literal, buffer}
end
end
@andreaseger
Copy link
Author

utils are copied and simplified from https://github.com/elixir-lang/plug/blob/master/lib/plug/router/utils.ex

Idea is to process all known routes with Utils.build_path_match and save that "nested" in ETS as shown in the test, the full config is attached to each route_spec in ets. Ideally with the combination of service+method+path_length there should not be many routes left to search.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment