├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── all_docs.exs
├── config
└── config.exs
├── lib
├── http_router.ex
└── http_router
│ └── util.ex
├── mix.exs
├── mix.lock
└── test
├── http_router
└── util_test.exs
├── http_router_test.exs
└── test_helper.exs
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build
2 | /deps
3 | erl_crash.dump
4 | *.ez
5 | docs/
6 | doc/
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir:
3 | - 1.2
4 | - 1.3
5 | - 1.4
6 | script:
7 | - mix test
8 | - MIX_ENV=test mix credo --strict
9 | after_success:
10 | - MIX_ENV=test mix coveralls.travis
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Shane Logsdon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HttpRouter
2 | [](https://travis-ci.org/sugar-framework/elixir-http-router)
3 | [](https://coveralls.io/r/sugar-framework/elixir-http-router)
4 | [](https://hex.pm/packages/http_router)
5 | [](http://inch-ci.org/github/sugar-framework/elixir-http-router)
6 |
7 | > HTTP Router with various macros to assist in
8 | > developing your application and organizing
9 | > your code
10 |
11 | ## Installation
12 |
13 | Add the following line to your dependency list
14 | in your `mix.exs` file, and run `mix deps.get`:
15 |
16 | ```elixir
17 | {:http_router, "~> 0.0.1"}
18 | ```
19 |
20 | Also, be sure to add `:http_router` to the list
21 | of applications on which your web application
22 | depends (the default looks something like
23 | `applications: [:logger]`) in your `mix.exs`
24 | file.
25 |
26 | Be sure to have [Plug][plug] in your dependency
27 | list as well as this is essentially a
28 | reimagination of the `Plug.Router` module, and
29 | as such, it still make use of a large portion
30 | of the Plug library.
31 |
32 | ## Usage
33 |
34 | To get the benefits that this package has to
35 | offer, it is necessary to use the `HttpRouter`
36 | module in one of your modules that you wish to
37 | act as the router for your web application.
38 | Similar to `Plug.Router`, we can start with a
39 | simple module:
40 |
41 | ```elixir
42 | defmodule MyRouter do
43 | use HttpRouter
44 | end
45 | ```
46 |
47 | That was easy, huh? Now, this module still needs a
48 | few calls to the below macros, but this depends on
49 | how your application needs to be structured.
50 |
51 | ### The Macros
52 |
53 | Check out this convoluted example:
54 |
55 | ```elixir
56 | defmodule MyRouter do
57 | use HttpRouter
58 |
59 | # Define one of the versions of the API
60 | # with a simple version number "1"
61 | # or following semver "1.0.0"
62 | # or date of release "2014-09-06"
63 | version "1" do
64 | # Define your routes here
65 | get "/", Handlers.V1.Pages, :index
66 | get "/pages", Handlers.V1.Pages, :create
67 | post "/pages", Handlers.V1.Pages, :create
68 | put "/pages/:page_id" when id == 1,
69 | Handlers.V1.Pages, :update_only_one
70 | get "/pages/:page_id", Handlers.V1.Pages, :show
71 |
72 | # Auto-create a full set of routes for resources
73 | #
74 | resource :users, Handlers.V1.User, arg: :user_id
75 | #
76 | # Generates:
77 | #
78 | # get "/users", Handlers.V1.User, :index
79 | # post "/users", Handlers.V1.User, :create
80 | # get "/users/:user_id", Handlers.V1.User, :show
81 | # put "/users/:user_id", Handlers.V1.User, :update
82 | # patch "/users/:user_id", Handlers.V1.User, :patch
83 | # delete "/users/:user_id", Handlers.V1.User, :delete
84 | #
85 | # options "/users", "HEAD,GET,POST"
86 | # options "/users/:_user_id", "HEAD,GET,PUT,PATCH,DELETE"
87 | end
88 |
89 | # An updated version of the AP
90 | version "2" do
91 | get "/", Handlers.V2.Pages, :index
92 | post "/pages", Handlers.V2.Pages, :create
93 | get "/pages/:page_id", Handlers.V2.Pages, :show
94 | put "/pages/:page_id", Handlers.V2.Pages, :update
95 |
96 | raw :trace, "/trace", Handlers.V2.Tracer, :trace
97 |
98 | resource :users, Handlers.V2.User
99 | resource :groups, Handlers.V2.Group
100 | end
101 | end
102 | ```
103 |
104 | `get/3`, `post/3`, `put/3`, `patch/3`, `delete/3`,
105 | `options/2`, and `any/3` are already built-in as
106 | described. `resource` exists but will need
107 | modifications to create everything as noted.
108 |
109 | `raw/4` allows for using custom HTTP methods, allowing
110 | your application to be HTTP spec compliant.
111 |
112 | `version/2` will need to be created outright. Will
113 | allow requests to contained endpoints when version
114 | exists in either `Accepts` header or URL (which ever
115 | is defined in app config).
116 |
117 | Extra routes will need to be added for `*.json`,
118 | `*.xml`, etc. requests for optionally specifying
119 | desired content type without the use of the
120 | `Accepts` header. These should match
121 | parsing/rendering abilities of your application.
122 |
123 | ## Configuration
124 |
125 | TBD.
126 |
127 | ## License
128 |
129 | HttpRouter is released under the MIT License.
130 |
131 | See [LICENSE](license) for details.
132 |
133 | [plug]: https://github.com/elixir-lang/plug
134 | [license]: https://github.com/sugar-framework/elixir-http-router/blob/master/LICENSE
135 |
--------------------------------------------------------------------------------
/all_docs.exs:
--------------------------------------------------------------------------------
1 | defmodule OfflineDocs do
2 | def parse_dep({dep, constraint}) when constraint |> is_binary, do: parse_dep({dep, constraint, []})
3 | def parse_dep({dep, options}) when options |> is_list, do: parse_dep({dep, "", []})
4 | def parse_dep({dep, _contraint, options}) do
5 | if options[:only] == nil or options[:only] == Mix.env do
6 | [dep: dep]
7 | else
8 | nil
9 | end
10 | end
11 |
12 | def add_options(dependency) do
13 | dep = dependency[:dep]
14 | opts = default_opts
15 | |> Keyword.update!(:output, &(&1 <> "#{dep}"))
16 | |> Keyword.update!(:source_root, &(&1 <> "#{dep}"))
17 | |> Keyword.update!(:source_beam, &(&1 <> "#{Mix.env}/lib/#{dep}/ebin"))
18 | dependency
19 | |> Keyword.put(:opts, opts)
20 | end
21 |
22 | def generate_docs(dep) do
23 | ExDoc.generate_docs("#{dep[:dep]}", "", dep[:opts])
24 | end
25 |
26 | def generate_index(deps) do
27 | links = deps
28 | |> Enum.map(fn dep ->
29 | "#{dep[:dep]}
"
30 | end)
31 | |> Enum.join("")
32 | File.write! "docs/index.html", [
33 | "",
34 | links,
35 | ""
36 | ]
37 | deps
38 | end
39 |
40 | def default_opts do
41 | [output: "docs/",
42 | source_root: "deps/",
43 | source_beam: "_build/",
44 | homepage_url: "",
45 | main: "overview"]
46 | end
47 | end
48 |
49 | Mix.Project.config
50 | |> Keyword.get(:deps)
51 | |> Enum.map(&OfflineDocs.parse_dep/1)
52 | |> Enum.filter(&(&1 != nil))
53 | |> Enum.map(&OfflineDocs.add_options/1)
54 | |> OfflineDocs.generate_index
55 | |> Enum.map(&OfflineDocs.generate_docs/1)
56 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for third-
9 | # party users, it should be done in your mix.exs file.
10 |
11 | # Sample configuration:
12 | #
13 | # config :logger, :console,
14 | # level: :info,
15 | # format: "$date $time [$level] $metadata$message\n",
16 | # metadata: [:user_id]
17 |
18 | # It is also possible to import configuration files, relative to this
19 | # directory. For example, you can emulate configuration per environment
20 | # by uncommenting the line below and defining dev.exs, test.exs and such.
21 | # Configuration from the imported file will override the ones defined
22 | # here (which is why it is important to import them last).
23 | #
24 | # import_config "#{Mix.env}.exs"
25 |
--------------------------------------------------------------------------------
/lib/http_router.ex:
--------------------------------------------------------------------------------
1 | defmodule HttpRouter do
2 | @moduledoc """
3 | `HttpRouter` defines an alternate format for `Plug.Router`
4 | routing. Supports all HTTP methods that `Plug.Router` supports.
5 |
6 | Routes are defined with the form:
7 |
8 | method route [guard], handler, action
9 |
10 | `method` is `get`, `post`, `put`, `patch`, or `delete`, each
11 | responsible for a single HTTP method. `method` can also be `any`, which will
12 | match on all HTTP methods. `options` is yet another option for `method`, but
13 | when using `options`, only a route path and the methods that route path
14 | supports are needed. `handler` is any valid Elixir module name, and
15 | `action` is any valid public function defined in the `handler` module.
16 |
17 | `get/3`, `post/3`, `put/3`, `patch/3`, `delete/3`, `options/2`, and `any/3`
18 | are already built-in as described. `resource/2` exists but will need
19 | modifications to create everything as noted.
20 |
21 | `raw/4` allows for using custom HTTP methods, allowing your application to be
22 | HTTP spec compliant.
23 |
24 | `version/2` allows requests to contained endpoints when version exists in
25 | either `Accept` header or URL (which ever is defined in the app config).
26 |
27 | Extra routes will be added for `*.json`, `*.xml`, etc. requests for optionally
28 | specifying desired content type without the use of the `Accept` header. These
29 | match parsing/rendering abilities of HttpRouter.
30 |
31 | ## Example
32 |
33 | defmodule Router do
34 | use HttpRouter
35 |
36 | # Define one of the versions of the API
37 | # with a simple version number "1"
38 | # or following semver "1.0.0"
39 | # or date of release "2014-09-06"
40 | version "1" do
41 | # Define your routes here
42 | get "/", Handlers.V1.Pages, :index
43 | get "/pages", Handlers.V1.Pages, :create
44 | post "/pages", Handlers.V1.Pages, :create
45 | put "/pages/:page_id" when id == 1,
46 | Handlers.V1.Pages, :update_only_one
47 | get "/pages/:page_id", Handlers.V1.Pages, :show
48 |
49 | # Auto-create a full set of routes for resources
50 | #
51 | resource :users, Handlers.V1.User, arg: :user_id
52 | #
53 | # Generates:
54 | #
55 | # get "/users", Handlers.V1.User, :index
56 | # post "/users", Handlers.V1.User, :create
57 | # get "/users/:user_id", Handlers.V1.User, :show
58 | # put "/users/:user_id", Handlers.V1.User, :update
59 | # patch "/users/:user_id", Handlers.V1.User, :patch
60 | # delete "/users/:user_id", Handlers.V1.User, :delete
61 | #
62 | # options "/users", "HEAD,GET,POST"
63 | # options "/users/:_user_id", "HEAD,GET,PUT,PATCH,DELETE"
64 | end
65 |
66 | # An updated version of the AP
67 | version "2" do
68 | get "/", Handlers.V2.Pages, :index
69 | post "/pages", Handlers.V2.Pages, :create
70 | get "/pages/:page_id", Handlers.V2.Pages, :show
71 | put "/pages/:page_id", Handlers.V2.Pages, :update
72 |
73 | raw :trace, "/trace", Handlers.V2.Tracer, :trace
74 |
75 | resource :users, Handlers.V2.User
76 | resource :groups, Handlers.V2.Group
77 | end
78 | end
79 | """
80 |
81 | alias Plug.Conn
82 | alias Plug.Builder
83 |
84 | import HttpRouter.Util
85 |
86 | @http_methods [:get, :post, :put, :patch, :delete, :any]
87 |
88 | @default_options [
89 | add_match_details_to_private: true,
90 | allow_copy_req_content_type: true,
91 | allow_head: true,
92 | allow_method_override: true,
93 | default_content_type: "text/html; charset=utf-8",
94 | json_decoder: Poison,
95 | parsers: [:json, :urlencoded, :multipart]
96 | ]
97 |
98 | ## Macros
99 |
100 | @doc false
101 | defmacro __using__(opts) do
102 | opts = @default_options |> Keyword.merge(opts)
103 | quote do
104 | import HttpRouter
105 | import Plug.Builder, only: [plug: 1, plug: 2]
106 | @before_compile HttpRouter
107 | @behaviour Plug
108 | Module.register_attribute(__MODULE__, :plugs, accumulate: true)
109 | Module.register_attribute(__MODULE__, :version, accumulate: false)
110 | Module.put_attribute(__MODULE__, :options, unquote(opts))
111 |
112 | # Plugs we want early in the stack
113 | popts = [parsers: unquote(opts[:parsers])]
114 | parsers_opts =
115 | if :json in popts[:parsers] do
116 | popts
117 | |> Keyword.put(:json_decoder, unquote(opts[:json_decoder]))
118 | else
119 | popts
120 | end
121 |
122 | plug Plug.Parsers, parsers_opts
123 | end
124 | end
125 |
126 | defp maybe_push_plug(defaults, true, plug) do
127 | [{plug, [], true} | defaults]
128 | end
129 | defp maybe_push_plug(defaults, false, _) do
130 | defaults
131 | end
132 |
133 | @doc false
134 | defmacro __before_compile__(env) do
135 | options = Module.get_attribute(env.module, :options)
136 | # Plugs we want predefined but aren't necessary to be before
137 | # user-defined plugs
138 | defaults =
139 | [{:match, [], true},
140 | {:dispatch, [], true}]
141 | |> maybe_push_plug(
142 | options[:allow_copy_req_content_type] == true,
143 | :copy_req_content_type
144 | )
145 | |> maybe_push_plug(
146 | options[:allow_method_override] == true,
147 | Plug.MethodOverride
148 | )
149 | |> maybe_push_plug(options[:allow_head] == true, Plug.Head)
150 |
151 | plugs = Enum.reverse(defaults) ++
152 | Module.get_attribute(env.module, :plugs)
153 | {conn, body} = env |> Builder.compile(plugs, [])
154 |
155 | quote do
156 | unquote(def_init())
157 | unquote(def_call(conn, body))
158 | unquote(def_copy_req_content_type(options))
159 | unquote(def_match())
160 | unquote(def_dispatch())
161 | end
162 | end
163 |
164 | defp def_init do
165 | quote do
166 | @spec init(Keyword.t) :: Keyword.t
167 | def init(opts) do
168 | opts
169 | end
170 | end
171 | end
172 |
173 | defp def_call(conn, body) do
174 | quote do
175 | @spec call(Conn.t, Keyword.t) :: Conn.t
176 | def call(conn, opts) do
177 | do_call(conn, opts)
178 | end
179 |
180 | defp do_call(unquote(conn), _), do: unquote(body)
181 | end
182 | end
183 |
184 | defp def_copy_req_content_type(options) do
185 | quote do
186 | if unquote(options[:allow_copy_req_content_type]) == true do
187 | @spec copy_req_content_type(Conn.t, Keyword.t) :: Conn.t
188 | def copy_req_content_type(conn, _opts) do
189 | default = unquote(options[:default_content_type])
190 | content_type = case Conn.get_req_header conn, "content-type" do
191 | [content_type] -> content_type
192 | _ -> default
193 | end
194 | conn |> Conn.put_resp_header("content-type", content_type)
195 | end
196 | end
197 | end
198 | end
199 |
200 | defp def_match do
201 | quote do
202 | @spec match(Conn.t, Keyword.t) :: Conn.t
203 | def match(conn, _opts) do
204 | plug_route = __MODULE__.do_match(conn.method, conn.path_info)
205 | Conn.put_private(conn, :plug_route, plug_route)
206 | end
207 |
208 | # Our default match so `Plug` doesn't fall on
209 | # its face when accessing an undefined route.
210 | @spec do_match(Conn.t, Keyword.t) :: Conn.t
211 | def do_match(_,_) do
212 | fn conn ->
213 | conn |> Conn.send_resp(404, "")
214 | end
215 | end
216 | end
217 | end
218 |
219 | defp def_dispatch do
220 | quote do
221 | @spec dispatch(Conn.t, Keyword.t) :: Conn.t
222 | def dispatch(%Conn{assigns: assigns} = conn, _opts) do
223 | Map.get(conn.private, :plug_route).(conn)
224 | end
225 | end
226 | end
227 |
228 | for verb <- @http_methods do
229 | @doc """
230 | Macro for defining `#{verb |> to_string |> String.upcase}` routes.
231 |
232 | ## Arguments
233 |
234 | * `route` - `String|List`
235 | * `handler` - `Atom`
236 | * `action` - `Atom`
237 | """
238 | @spec unquote(verb)(binary | list, atom, atom) :: Macro.t
239 | defmacro unquote(verb)(route, handler, action) do
240 | build_match unquote(verb), route, handler, action, __CALLER__
241 | end
242 | end
243 |
244 | @doc """
245 | Macro for defining `OPTIONS` routes.
246 |
247 | ## Arguments
248 |
249 | * `route` - `String|List`
250 | * `allows` - `String`
251 | """
252 | @spec options(binary | list, binary) :: Macro.t
253 | defmacro options(route, allows) do
254 | build_match :options, route, allows, __CALLER__
255 | end
256 |
257 | @doc """
258 | Macro for defining routes for custom HTTP methods.
259 |
260 | ## Arguments
261 |
262 | * `method` - `Atom`
263 | * `route` - `String|List`
264 | * `handler` - `Atom`
265 | * `action` - `Atom`
266 | """
267 | @spec raw(atom, binary | list, atom, atom) :: Macro.t
268 | defmacro raw(method, route, handler, action) do
269 | build_match method, route, handler, action, __CALLER__
270 | end
271 |
272 | @doc """
273 | Creates RESTful resource endpoints for a route/handler
274 | combination.
275 |
276 | ## Example
277 |
278 | resource :users, Handlers.User
279 |
280 | expands to
281 |
282 | get, "/users", Handlers.User, :index
283 | post, "/users", Handlers.User, :create
284 | get, "/users/:id", Handlers.User, :show
285 | put, "/users/:id", Handlers.User, :update
286 | patch, "/users/:id", Handlers.User, :patch
287 | delete, "/users/:id", Handlers.User, :delete
288 |
289 | options, "/users", "HEAD,GET,POST"
290 | options, "/users/:_id", "HEAD,GET,PUT,PATCH,DELETE"
291 | """
292 | @spec resource(atom, atom, Keyword.t) :: Macro.t
293 | defmacro resource(resource, handler, opts \\ []) do
294 | arg = Keyword.get opts, :arg, :id
295 | allowed = Keyword.get opts, :only, [:index, :create, :show,
296 | :update, :patch, :delete]
297 | # mainly used by `version/2`
298 | ppath = Keyword.get opts, :prepend_path, nil
299 | prepend_path =
300 | if ppath do
301 | "/" <> ppath <> "/"
302 | else
303 | ppath
304 | end
305 |
306 | routes = get_resource_routes(prepend_path, resource, arg)
307 | options_routes = get_resource_options_routes(prepend_path, resource, arg)
308 |
309 | build_resource_matches(routes, allowed, handler, __CALLER__)
310 | ++ build_resource_options_matches(options_routes, allowed, __CALLER__)
311 | end
312 |
313 | @doc """
314 | Macro for defining a version for a set of routes.
315 |
316 | ## Arguments
317 |
318 | * `version` - `String`
319 | """
320 | @spec version(binary, any) :: Macro.t
321 | defmacro version(version, do: body) do
322 | body = update_body_with_version body, version
323 | quote do
324 | unquote(body)
325 | end
326 | end
327 |
328 | ## Private API
329 |
330 | defp build_resource_matches(routes, allowed, handler, caller) do
331 | for {method, path, action} <- routes |> filter(allowed) do
332 | build_match method, path, handler, action, caller
333 | end
334 | end
335 |
336 | defp build_resource_options_matches(routes, allowed, caller) do
337 | for {path, methods} <- routes do
338 | allows = methods
339 | |> filter(allowed)
340 | |> Enum.map(fn {_, m} ->
341 | normalize_method(m)
342 | end)
343 | |> Enum.join(",")
344 | build_match :options, path, "HEAD,#{allows}", caller
345 | end
346 | end
347 |
348 | defp get_resource_routes(prepend_path, resource, arg) do
349 | base_path = "#{prepend_path}#{resource}"
350 | arg_path = "#{base_path}/:#{arg}"
351 |
352 | [{:get, base_path, :index},
353 | {:post, base_path, :create},
354 | {:get, arg_path, :show},
355 | {:put, arg_path, :update},
356 | {:patch, arg_path, :patch},
357 | {:delete, arg_path, :delete}]
358 | end
359 |
360 | defp get_resource_options_routes(prepend_path, resource, arg) do
361 | [{"/#{ignore_args prepend_path}#{resource}",
362 | [index: :get, create: :post]},
363 | {"/#{ignore_args prepend_path}#{resource}/:_#{arg}",
364 | [show: :get, update: :put,
365 | patch: :patch, delete: :delete]}]
366 | end
367 |
368 | defp ignore_args(nil), do: ""
369 | defp ignore_args(str) do
370 | str
371 | |> String.to_char_list
372 | |> do_ignore_args
373 | |> to_string
374 | end
375 |
376 | defp do_ignore_args([]), do: []
377 | defp do_ignore_args([?:|t]), do: [?:,?_] ++ do_ignore_args(t)
378 | defp do_ignore_args([h|t]), do: [h] ++ do_ignore_args(t)
379 |
380 | defp update_body_with_version({:__block__, [], calls}, version) do
381 | {:__block__, [], calls |> Enum.map(&prepend_version(&1, "/" <> version))}
382 | end
383 | defp update_body_with_version(item, version) when is_tuple(item) do
384 | {:__block__, [], [item] |> Enum.map(&prepend_version(&1, "/" <> version))}
385 | end
386 |
387 | defp prepend_version({method, line, args}, version) do
388 | new_args = case method do
389 | :options ->
390 | [path, allows] = args
391 | [version <> path, allows]
392 | :raw ->
393 | [verb, path, handler, action] = args
394 | [verb, version <> path, handler, action]
395 | :resource ->
396 | prepend_resource_version(args, version)
397 | _ ->
398 | [path, handler, action] = args
399 | [version <> path, handler, action]
400 | end
401 | {method, line, new_args}
402 | end
403 |
404 | defp prepend_resource_version(args, version) do
405 | case args do
406 | [res, handler] ->
407 | [res, handler, [prepend_path: version]]
408 | [res, handler, opts] ->
409 | opts = Keyword.update opts, :prepend_path, version, &("#{version}/#{&1}")
410 | [res, handler, opts]
411 | end
412 | end
413 |
414 | # Builds a `do_match/2` function body for a given route.
415 | defp build_match(:options, route, allows, caller) do
416 | body = quote do
417 | conn
418 | |> Conn.resp(200, "")
419 | |> Conn.put_resp_header("allow", unquote(allows))
420 | |> Conn.send_resp
421 | end
422 |
423 | do_build_match :options, route, body, caller
424 | end
425 | defp build_match(method, route, handler, action, caller) do
426 | body = build_body handler, action, caller
427 | # body_json = build_body handler, action, caller, :json
428 | # body_xml = build_body handler, action, caller, :xml
429 |
430 | [#do_build_match(method, route <> ".json", body_json, caller),
431 | #do_build_match(method, route <> ".xml", body_xml, caller),
432 | do_build_match(method, route, body, caller)]
433 | end
434 |
435 | defp do_build_match(verb, route, body, caller) do
436 | {method, guards, _vars, match} = prep_match verb, route, caller
437 | match_method = if verb == :any, do: quote(do: _), else: method
438 |
439 | quote do
440 | def do_match(unquote(match_method), unquote(match)) when unquote(guards) do
441 | fn conn ->
442 | unquote(body)
443 | end
444 | end
445 | end
446 | end
447 |
448 | defp build_body(handler, action, caller), do: build_body(handler, action, caller, :skip)
449 | defp build_body(handler, action, _caller, add_header) do
450 | header = case add_header do
451 | # :json -> [{"accept", "application/json"}]
452 | # :xml -> [{"accept", "application/xml"}]
453 | _ -> []
454 | end
455 |
456 | quote do
457 | opts = [action: unquote(action), args: binding()]
458 | private = conn.private
459 | |> Map.put(:controller, unquote(handler))
460 | |> Map.put(:handler, unquote(handler))
461 | |> Map.put(:action, unquote(action))
462 |
463 | %{conn | req_headers: unquote(header) ++ conn.req_headers,
464 | private: private}
465 | |> unquote(handler).call(unquote(handler).init(opts))
466 | end
467 | end
468 |
469 | defp filter(list, allowed) do
470 | Enum.filter list, &do_filter(&1, allowed)
471 | end
472 |
473 | defp do_filter({_, _, action}, allowed) do
474 | action in allowed
475 | end
476 | defp do_filter({action, _}, allowed) do
477 | action in allowed
478 | end
479 |
480 | ## Grabbed from `Plug.Router`
481 |
482 | defp prep_match(method, route, caller) do
483 | {method, guard} = method |> List.wrap |> convert_methods
484 | {path, guards} = extract_path_and_guards(route, guard)
485 | {vars, match} = path |> Macro.expand(caller) |> build_spec
486 | {method, guards, vars, match}
487 | end
488 |
489 | # Convert the verbs given with :via into a variable
490 | # and guard set that can be added to the dispatch clause.
491 | defp convert_methods([]) do
492 | {quote(do: _), true}
493 | end
494 | defp convert_methods([method]) do
495 | {normalize_method(method), true}
496 | end
497 |
498 | # Extract the path and guards from the path.
499 | defp extract_path_and_guards({:when, _, [path, guards]}, true) do
500 | {path, guards}
501 | end
502 | defp extract_path_and_guards({:when, _, [path, guards]}, extra_guard) do
503 | {path, {:and, [], [guards, extra_guard]}}
504 | end
505 | defp extract_path_and_guards(path, extra_guard) do
506 | {path, extra_guard}
507 | end
508 | end
509 |
--------------------------------------------------------------------------------
/lib/http_router/util.ex:
--------------------------------------------------------------------------------
1 | defmodule HttpRouter.Util do
2 | @moduledoc false
3 |
4 | defmodule InvalidSpecError do
5 | @moduledoc false
6 | defexception message: "invalid route specification"
7 | end
8 |
9 | @spec normalize_method(binary) :: binary
10 | def normalize_method(method) do
11 | method |> to_string |> String.upcase
12 | end
13 |
14 | @spec split(binary) :: [binary]
15 | def split(bin) do
16 | for segment <- String.split(bin, "/"), segment != "", do: segment
17 | end
18 |
19 | @spec build_spec(binary, any) :: {list, list}
20 | def build_spec(spec, context \\ nil)
21 | def build_spec(spec, context) when is_binary(spec) do
22 | build_spec split(spec), context, [], []
23 | end
24 | def build_spec(spec, _context) do
25 | {[], spec}
26 | end
27 | defp build_spec([h|t], context, vars, acc) do
28 | handle_segment_match segment_match(h, "", context), t, context, vars, acc
29 | end
30 | defp build_spec([], _context, vars, acc) do
31 | {vars |> Enum.uniq |> Enum.reverse, Enum.reverse(acc)}
32 | end
33 |
34 | defp handle_segment_match({:literal, literal}, t, context, vars, acc) do
35 | build_spec t, context, vars, [literal|acc]
36 | end
37 | defp handle_segment_match({:identifier, identifier, expr}, t, context, vars, acc) do
38 | build_spec t, context, [identifier|vars], [expr|acc]
39 | end
40 | defp handle_segment_match({:glob, identifier, expr}, t, context, vars, acc) do
41 | if t != [] do
42 | raise InvalidSpecError, message: "cannot have a *glob followed by other segments"
43 | end
44 |
45 | case acc do
46 | [hs|ts] ->
47 | acc = [{:|, [], [hs, expr]} | ts]
48 | build_spec([], context, [identifier|vars], acc)
49 | _ ->
50 | {vars, expr} = build_spec([], context, [identifier|vars], [expr])
51 | {vars, hd(expr)}
52 | end
53 | end
54 |
55 | defp segment_match(":" <> argument, buffer, context) do
56 | identifier = binary_to_identifier(":", argument)
57 | expr = quote_if_buffer identifier, buffer, context, fn var ->
58 | quote do: unquote(buffer) <> unquote(var)
59 | end
60 | {:identifier, identifier, expr}
61 | end
62 | defp segment_match("*" <> argument, buffer, context) do
63 | underscore = {:_, [], context}
64 | identifier = binary_to_identifier("*", argument)
65 | expr = quote_if_buffer identifier, buffer, context, fn var ->
66 | quote do:
67 | [unquote(buffer) <> unquote(underscore)|unquote(underscore)]
68 | = unquote(var)
69 | end
70 | {:glob, identifier, expr}
71 | end
72 | defp segment_match(<>, buffer, context) do
73 | segment_match t, buffer <> <>, context
74 | end
75 | defp segment_match(<<>>, buffer, _context) do
76 | {:literal, buffer}
77 | end
78 |
79 | defp quote_if_buffer(identifier, "", context, _fun) do
80 | {identifier, [], context}
81 | end
82 | defp quote_if_buffer(identifier, _buffer, context, fun) do
83 | fun.({identifier, [], context})
84 | end
85 |
86 | defp binary_to_identifier(prefix, <> = binary)
87 | when letter in ?a..?z
88 | or letter == ?_ do
89 | if binary =~ ~r/^\w+$/ do
90 | String.to_atom(binary)
91 | else
92 | raise InvalidSpecError,
93 | message: "#{prefix}identifier in routes must be made of letters, numbers and underscore"
94 | end
95 | end
96 | defp binary_to_identifier(prefix, _) do
97 | raise InvalidSpecError, message: "#{prefix} in routes must be followed by lowercase letters"
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule HttpRouter.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [ app: :http_router,
6 | version: "0.10.0",
7 | elixir: "~> 1.0",
8 | deps: deps(),
9 | name: "HttpRouter",
10 | package: package(),
11 | description: description(),
12 | docs: [ main: "HttpRouter" ],
13 | test_coverage: [ tool: ExCoveralls ] ]
14 | end
15 |
16 | def application do
17 | [ applications: [ :logger, :poison, :cowboy, :plug,
18 | :xml_builder] ]
19 | end
20 |
21 | defp deps do
22 | [
23 | {:cowboy, "~> 1.0"},
24 | {:plug, "~> 1.0"},
25 | {:poison, "~> 3.0"},
26 | {:xml_builder, "~> 2.3"},
27 | {:earmark, "~> 1.0", only: :dev},
28 | {:ex_doc, "~> 0.14", only: :dev},
29 | {:credo, "~> 0.5", only: [:dev, :test]},
30 | {:excoveralls, "~> 0.5", only: :test},
31 | {:dialyze, "~> 0.2", only: :test}
32 | ]
33 | end
34 |
35 | defp description do
36 | """
37 | HTTP Router with various macros to assist in
38 | developing your application and organizing
39 | your code
40 | """
41 | end
42 |
43 | defp package do
44 | %{ maintainers: [ "Shane Logsdon" ],
45 | licenses: [ "MIT" ],
46 | links: %{ "GitHub" => "https://github.com/sugar-framework/elixir-http-router" } }
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
3 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "f4763bbe08233eceed6f24bc4fcc8d71c17cfeafa6439157c57349aa1bb4f17c"},
4 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm", "db622da03aa039e6366ab953e31186cc8190d32905e33788a1acb22744e6abd2"},
5 | "credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "539596b6774069260d5938aa73042a2f5157e1c0215aa35f5a53d83889546d14"},
6 | "dialyze": {:hex, :dialyze, "0.2.1", "9fb71767f96649020d769db7cbd7290059daff23707d6e851e206b1fdfa92f9d", [:mix], [], "hexpm", "f485181fa53229356621261a384963cb47511cccf1454e82ca4fde53274fcd48"},
7 | "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"},
8 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
9 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
10 | "excoveralls": {:hex, :excoveralls, "0.18.2", "86efd87a0676a3198ff50b8c77620ea2f445e7d414afa9ec6c4ba84c9f8bdcc2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230262c418f0de64077626a498bd4fdf1126d5c2559bb0e6b43deac3005225a4"},
11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
12 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
13 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
14 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
15 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
17 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
18 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
19 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
20 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm", "6e56493a862433fccc3aca3025c946d6720d8eedf6e3e6fb911952a7071c357f"},
21 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
22 | "xml_builder": {:hex, :xml_builder, "2.3.0", "69d214c6ad41ae1300b36acff4367551cdfd9dc1b860affc16e103c6b1589053", [:mix], [], "hexpm", "972ec33346a225cd5acd14ab23d4e79042bd37cb904e07e24cd06992dde1a0ed"},
23 | }
24 |
--------------------------------------------------------------------------------
/test/http_router/util_test.exs:
--------------------------------------------------------------------------------
1 | defmodule HttpRouter.UtilTest do
2 | use ExUnit.Case, async: true
3 | import HttpRouter.Util
4 |
5 | test "split/1" do
6 | assert split("bin/sh") === ["bin", "sh"]
7 | assert split("bin/:sh") === ["bin", ":sh"]
8 | assert split("bin/*sh") === ["bin", "*sh"]
9 | assert split("/bin/sh") === ["bin", "sh"]
10 | assert split("//bin/sh/") === ["bin", "sh"]
11 | end
12 |
13 | test "build_spec/2 proper" do
14 | assert build_spec("bin/:sh", nil) === {[:sh], ["bin", {:sh, [], nil}]}
15 | assert build_spec("bin/*sh", nil) === {[:sh], [{:|, [], ["bin", {:sh, [], nil}]}]}
16 | assert build_spec("*sh", nil) === {[:sh], {:sh, [], nil}}
17 | end
18 |
19 | test "build_spec/2 illegal chars" do
20 | message = "identifier in routes must be made of letters, numbers and underscore"
21 | assert_raise HttpRouter.Util.InvalidSpecError, ":" <> message, fn ->
22 | build_spec("bin/:sh-", nil)
23 | end
24 |
25 | assert_raise HttpRouter.Util.InvalidSpecError, "*" <> message, fn ->
26 | build_spec("bin/*sh-", nil)
27 | end
28 | end
29 |
30 | test "build_spec/2 bad name" do
31 | message = "in routes must be followed by lowercase letters"
32 | assert_raise HttpRouter.Util.InvalidSpecError, ": " <> message, fn ->
33 | build_spec("bin/:-sh", nil)
34 | end
35 |
36 | assert_raise HttpRouter.Util.InvalidSpecError, "* " <> message, fn ->
37 | build_spec("bin/*-sh", nil)
38 | end
39 | end
40 |
41 | test "build_spec/2 glob with segments after capture" do
42 | message = "cannot have a *glob followed by other segments"
43 | assert_raise HttpRouter.Util.InvalidSpecError, message, fn ->
44 | build_spec("bin/*sh/json", nil)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/http_router_test.exs:
--------------------------------------------------------------------------------
1 | defmodule HttpRouterTest do
2 | use ExUnit.Case, async: true
3 | use Plug.Test
4 | use HttpRouter
5 |
6 | test "get/3" do
7 | conn = conn(:get, "/2/get")
8 | |> HttpRouterTest.Router.call([])
9 |
10 | assert conn.state === :sent
11 | assert conn.status === 200
12 | end
13 |
14 | test "post/3" do
15 | conn = conn(:post, "/2/post")
16 | |> HttpRouterTest.Router.call([])
17 |
18 | assert conn.state === :sent
19 | assert conn.status === 200
20 | end
21 |
22 | test "put/3" do
23 | conn = conn(:put, "/2/put")
24 | |> HttpRouterTest.Router.call([])
25 |
26 | assert conn.state === :sent
27 | assert conn.status === 200
28 | end
29 |
30 | test "patch/3" do
31 | conn = conn(:patch, "/2/patch")
32 | |> HttpRouterTest.Router.call([])
33 |
34 | assert conn.state === :sent
35 | assert conn.status === 200
36 | end
37 |
38 | test "delete/3" do
39 | conn = conn(:delete, "/2/delete")
40 | |> HttpRouterTest.Router.call([])
41 |
42 | assert conn.state === :sent
43 | assert conn.status === 200
44 | end
45 |
46 | test "options/3" do
47 | conn = conn(:options, "/2/options")
48 | |> HttpRouterTest.Router.call([])
49 |
50 | assert conn.state === :sent
51 | assert conn.status === 200
52 | end
53 |
54 | test "any/3 any" do
55 | conn = conn(:any, "/2/any")
56 | |> HttpRouterTest.Router.call([])
57 |
58 | assert conn.state === :sent
59 | assert conn.status === 200
60 | end
61 |
62 | test "any/3 get" do
63 | conn = conn(:get, "/2/any")
64 | |> HttpRouterTest.Router.call([])
65 |
66 | assert conn.state === :sent
67 | assert conn.status === 200
68 | end
69 |
70 | test "raw/4 trace" do
71 | conn = conn(:trace, "/2/trace")
72 | |> HttpRouterTest.Router.call([])
73 |
74 | assert conn.state === :sent
75 | assert conn.status === 200
76 | end
77 |
78 | test "resource/2" do
79 | assert true === true
80 | end
81 |
82 | test "resource/2 index" do
83 | conn = conn(:get, "/2/users")
84 | |> HttpRouterTest.Router.call([])
85 |
86 | assert conn.state === :sent
87 | assert conn.status === 200
88 | end
89 |
90 | test "resource/2 create" do
91 | conn = conn(:post, "/2/users")
92 | |> HttpRouterTest.Router.call([])
93 |
94 | assert conn.state === :sent
95 | assert conn.status === 200
96 | end
97 |
98 | test "resource/2 show" do
99 | conn = conn(:get, "/2/users/1")
100 | |> HttpRouterTest.Router.call([])
101 |
102 | assert conn.state === :sent
103 | assert conn.status === 200
104 | end
105 |
106 | test "resource/2 update" do
107 | conn = conn(:put, "/2/users/1")
108 | |> HttpRouterTest.Router.call([])
109 |
110 | assert conn.state === :sent
111 | assert conn.status === 200
112 | end
113 |
114 | test "resource/2 patch" do
115 | conn = conn(:patch, "/2/users/1")
116 | |> HttpRouterTest.Router.call([])
117 |
118 | assert conn.state === :sent
119 | assert conn.status === 200
120 | end
121 |
122 | test "resource/2 delete" do
123 | conn = conn(:delete, "/2/users/1")
124 | |> HttpRouterTest.Router.call([])
125 |
126 | assert conn.state === :sent
127 | assert conn.status === 200
128 | end
129 |
130 | test "filter plug is run" do
131 | conn = conn(:get, "/2/get")
132 | |> HttpRouterTest.Router.call([])
133 |
134 | assert conn.state === :sent
135 | assert conn.status === 200
136 | assert get_resp_header(conn, "content-type") === ["text/html; charset=utf-8"]
137 | end
138 |
139 | test "resource/2 index with prepended path" do
140 | conn = conn(:get, "/2/users/2/comments")
141 | |> HttpRouterTest.Router.call([])
142 |
143 | assert conn.state === :sent
144 | assert conn.status === 200
145 | end
146 |
147 | # test "get/3 with translate ext to accepts header" do
148 | # conn = conn(:get, "/2/get.json")
149 | # |> HttpRouterTest.Router.call([])
150 |
151 | # assert conn.state === :sent
152 | # assert conn.status === 200
153 | # assert get_req_header(conn, "accept") === ["application/json"]
154 | # end
155 |
156 | defmodule Foo do
157 | def init(opts), do: opts
158 | def call(conn, opts) do
159 | apply __MODULE__, opts[:action], [conn, conn.params]
160 | end
161 | def get(conn, _args) do
162 | conn
163 | |> Map.put(:resp_body, "")
164 | |> Map.put(:status, 200)
165 | |> Map.put(:state, :set)
166 | |> Plug.Conn.send_resp
167 | end
168 | def post(conn, _args) do
169 | conn
170 | |> Map.put(:resp_body, "")
171 | |> Map.put(:status, 200)
172 | |> Map.put(:state, :set)
173 | |> Plug.Conn.send_resp
174 | end
175 | def put(conn, _args) do
176 | conn
177 | |> Map.put(:resp_body, "")
178 | |> Map.put(:status, 200)
179 | |> Map.put(:state, :set)
180 | |> Plug.Conn.send_resp
181 | end
182 | def patch(conn, _args) do
183 | conn
184 | |> Map.put(:resp_body, "")
185 | |> Map.put(:status, 200)
186 | |> Map.put(:state, :set)
187 | |> Plug.Conn.send_resp
188 | end
189 | def delete(conn, _args) do
190 | conn
191 | |> Map.put(:resp_body, "")
192 | |> Map.put(:status, 200)
193 | |> Map.put(:state, :set)
194 | |> Plug.Conn.send_resp
195 | end
196 | def options(conn, _args) do
197 | conn
198 | |> Map.put(:resp_body, "")
199 | |> Map.put(:status, 200)
200 | |> Map.put(:state, :set)
201 | |> Plug.Conn.send_resp
202 | end
203 | def any(conn, _args) do
204 | conn
205 | |> Map.put(:resp_body, "")
206 | |> Map.put(:status, 200)
207 | |> Map.put(:state, :set)
208 | |> Plug.Conn.send_resp
209 | end
210 | def trace(conn, _args) do
211 | conn
212 | |> Map.put(:resp_body, "")
213 | |> Map.put(:status, 200)
214 | |> Map.put(:state, :set)
215 | |> Plug.Conn.send_resp
216 | end
217 | end
218 |
219 | defmodule Bar do
220 | def init(opts), do: opts
221 | def call(conn, opts) do
222 | apply __MODULE__, opts[:action], [conn, conn.params]
223 | end
224 | def index(conn, _args) do
225 | conn
226 | |> Map.put(:resp_body, "")
227 | |> Map.put(:status, 200)
228 | |> Map.put(:state, :set)
229 | |> Plug.Conn.send_resp
230 | end
231 | def create(conn, _args) do
232 | conn
233 | |> Map.put(:resp_body, "")
234 | |> Map.put(:status, 200)
235 | |> Map.put(:state, :set)
236 | |> Plug.Conn.send_resp
237 | end
238 | def show(conn, _args) do
239 | conn
240 | |> Map.put(:resp_body, "")
241 | |> Map.put(:status, 200)
242 | |> Map.put(:state, :set)
243 | |> Plug.Conn.send_resp
244 | end
245 | def update(conn, _args) do
246 | conn
247 | |> Map.put(:resp_body, "")
248 | |> Map.put(:status, 200)
249 | |> Map.put(:state, :set)
250 | |> Plug.Conn.send_resp
251 | end
252 | def patch(conn, _args) do
253 | conn
254 | |> Map.put(:resp_body, "")
255 | |> Map.put(:status, 200)
256 | |> Map.put(:state, :set)
257 | |> Plug.Conn.send_resp
258 | end
259 | def delete(conn, _args) do
260 | conn
261 | |> Map.put(:resp_body, "")
262 | |> Map.put(:status, 200)
263 | |> Map.put(:state, :set)
264 | |> Plug.Conn.send_resp
265 | end
266 | end
267 |
268 | defmodule Baz do
269 | def init(opts), do: opts
270 | def call(conn, opts) do
271 | apply __MODULE__, opts[:action], [conn, conn.params]
272 | end
273 | def index(conn, _args) do
274 | conn
275 | |> Map.put(:resp_body, "")
276 | |> Map.put(:status, 200)
277 | |> Map.put(:state, :set)
278 | |> Plug.Conn.send_resp
279 | end
280 | end
281 |
282 | defmodule Router do
283 | use HttpRouter
284 |
285 | plug :set_utf8_json
286 |
287 | version "1" do
288 | get "/get", Foo, :get
289 | end
290 |
291 | version "2" do
292 | get "/get", Foo, :get
293 | get "/get/:id", Foo, :get
294 | post "/post", Foo, :post
295 | put "/put", Foo, :put
296 | patch "/patch", Foo, :patch
297 | delete "/delete", Foo, :delete
298 | options "/options", "HEAD,GET"
299 | any "/any", Foo, :any
300 | raw :trace, "/trace", Foo, :trace
301 |
302 | resource :users, Bar
303 | resource :comments, Baz, prepend_path: "/users/:user_id",
304 | only: [:index]
305 | end
306 |
307 | def set_utf8_json(%Plug.Conn{state: state} = conn, _) when state in [:unset, :set] do
308 | conn |> put_resp_header("content-type", "application/json; charset=utf-8")
309 | end
310 | def set_utf8_json(conn, _), do: conn
311 | end
312 | end
313 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------