Skip to content

Commit 8e4b4d8

Browse files
authored
Merge pull request #93 from Gigitsu/feature/ParametricRoute
Parametric route
2 parents edf2d9b + 3aab4db commit 8e4b4d8

File tree

3 files changed

+93
-9
lines changed

3 files changed

+93
-9
lines changed

lib/bypass/instance.ex

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Bypass.Instance do
22
use GenServer, restart: :transient
33

44
import Bypass.Utils
5+
import Plug.Router.Utils, only: [build_path_match: 1]
56

67
def start_link(opts \\ []) do
78
GenServer.start_link(__MODULE__, [opts])
@@ -139,6 +140,7 @@ defmodule Bypass.Instance do
139140
route,
140141
new_route(
141142
fun,
143+
path,
142144
case expect do
143145
:expect -> :once_or_more
144146
:expect_once -> :once
@@ -285,18 +287,47 @@ defmodule Bypass.Instance do
285287
end
286288

287289
defp route_info(method, path, %{expectations: expectations} = _state) do
288-
route =
289-
case Map.get(expectations, {method, path}, :no_expectations) do
290-
:no_expectations ->
291-
{:any, :any}
290+
segments = build_path_match(path) |> elem(1)
292291

293-
_ ->
294-
{method, path}
295-
end
292+
route =
293+
expectations
294+
|> Enum.reduce_while(
295+
{:any, :any, %{}},
296+
fn
297+
{{^method, path_pattern}, %{path_parts: path_parts}}, acc ->
298+
case match_route(segments, path_parts) do
299+
{true, params} -> {:halt, {method, path_pattern, params}}
300+
{false, _} -> {:cont, acc}
301+
end
302+
303+
_, acc ->
304+
{:cont, acc}
305+
end
306+
)
296307

297308
{route, Map.get(expectations, route)}
298309
end
299310

311+
defp match_route(path, route) when length(path) == length(route) do
312+
path
313+
|> Enum.zip(route)
314+
|> Enum.reduce_while(
315+
{true, %{}},
316+
fn
317+
{value, {param, _, _}}, {_, params} ->
318+
{:cont, {true, Map.put(params, Atom.to_string(param), value)}}
319+
320+
{segment, segment}, acc ->
321+
{:cont, acc}
322+
323+
_, _ ->
324+
{:halt, {false, nil}}
325+
end
326+
)
327+
end
328+
329+
defp match_route(_, _), do: {false, nil}
330+
300331
defp do_up(port, ref) do
301332
plug_opts = [self()]
302333
{:ok, socket} = :ranch_tcp.listen(so_reuseport() ++ [ip: listen_ip(), port: port])
@@ -353,16 +384,23 @@ defmodule Bypass.Instance do
353384
|> length
354385
end
355386

356-
defp new_route(fun, expected) do
387+
defp new_route(fun, path_parts, expected) when is_list(path_parts) do
357388
%{
358389
fun: fun,
359390
expected: expected,
391+
path_parts: path_parts,
360392
retained_plugs: %{},
361393
results: [],
362394
request_count: 0
363395
}
364396
end
365397

398+
defp new_route(fun, :any, expected),
399+
do: new_route(fun, [], expected)
400+
401+
defp new_route(fun, path, expected),
402+
do: new_route(fun, build_path_match(path) |> elem(1), expected)
403+
366404
defp cowboy_opts(port, ref, socket) do
367405
case Application.spec(:plug_cowboy, :vsn) do
368406
'1.' ++ _ -> [ref: ref, acceptors: 5, port: port, socket: socket]

lib/bypass/plug.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ defmodule Bypass.Plug do
22
def init([pid]), do: pid
33

44
def call(%{method: method, request_path: request_path} = conn, pid) do
5-
route = Bypass.Instance.call(pid, {:get_route, method, request_path})
5+
{method, path, path_params} = Bypass.Instance.call(pid, {:get_route, method, request_path})
6+
route = {method, path}
67
ref = make_ref()
78

9+
conn = Plug.Conn.fetch_query_params(%{conn | params: path_params})
10+
811
case Bypass.Instance.call(pid, {:get_expect_fun, route}) do
912
fun when is_function(fun, 1) ->
1013
retain_current_plug(pid, route, ref)

test/bypass_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,49 @@ defmodule BypassTest do
362362
end)
363363
end
364364

365+
test "Bypass.stub/4 does not raise if request with parameters is made" do
366+
:stub |> specific_route_with_params
367+
end
368+
369+
test "Bypass.expect/4 can be used to define a specific route with parameters" do
370+
:expect |> specific_route_with_params
371+
end
372+
373+
test "Bypass.expect_once/4 can be used to define a specific route with parameters" do
374+
:expect_once |> specific_route_with_params
375+
end
376+
377+
defp specific_route_with_params(expect_fun) do
378+
bypass = Bypass.open()
379+
method = "POST"
380+
pattern = "/this/:resource/get/:id"
381+
path = "/this/my_resource/get/1234"
382+
383+
# one of Bypass.expect or Bypass.expect_once
384+
apply(Bypass, expect_fun, [
385+
bypass,
386+
method,
387+
pattern,
388+
fn conn ->
389+
assert conn.method == method
390+
assert conn.request_path == path
391+
392+
assert conn.params == %{
393+
"resource" => "my_resource",
394+
"id" => "1234",
395+
"q_param_1" => "a",
396+
"q_param_2" => "b"
397+
}
398+
399+
Plug.Conn.send_resp(conn, 200, "")
400+
end
401+
])
402+
403+
capture_log(fn ->
404+
assert {:ok, 200, ""} = request(bypass.port, path <> "?q_param_1=a&q_param_2=b")
405+
end)
406+
end
407+
365408
test "All routes to a Bypass.expect/4 call must be called" do
366409
:expect |> all_routes_must_be_called
367410
end

0 commit comments

Comments
 (0)