Last active
January 30, 2017 22:23
-
-
Save andreaseger/3484c5c6f5fca3c8ea9462cc33c76646 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.