├── .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 | [![Build Status](https://img.shields.io/travis/sugar-framework/elixir-http-router.svg?style=flat)](https://travis-ci.org/sugar-framework/elixir-http-router) 3 | [![Coverage Status](https://img.shields.io/coveralls/sugar-framework/elixir-http-router.svg?style=flat)](https://coveralls.io/r/sugar-framework/elixir-http-router) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/http_router.svg?style=flat)](https://hex.pm/packages/http_router) 5 | [![Inline docs](http://inch-ci.org/github/sugar-framework/elixir-http-router.svg?branch=master)](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 | --------------------------------------------------------------------------------