Rack compatible, lightweight and fast HTTP Router for Ruby and Hanami.
- Home page: http://hanamirb.org
- Mailing List: http://hanamirb.org/mailing-list
- API Doc: http://rdoc.info/gems/hanami-router
- Bugs/Issues: https://github.com/hanami/router/issues
- Support: http://stackoverflow.com/questions/tagged/hanami
- Chat: http://chat.hanamirb.org
Hanami::Router supports Ruby (MRI) 2.3+, JRuby 9.1.5.0+
Add this line to your application's Gemfile:
gem 'hanami-router'
And then execute:
$ bundle
Or install it yourself as:
$ gem install hanami-router
require 'hanami/router'
app = Hanami::Router.new do
get '/', to: ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
end
Rack::Server.start app: app, Port: 2300
Hanami::Router is designed to work as a standalone framework or within a context of a Hanami application.
For the standalone usage, it supports neat features:
Hanami::Router.new do
root to: ->(env) { [200, {}, ['Hello']] }
get '/lambda', to: ->(env) { [200, {}, ['World']] }
get '/dashboard', to: Dashboard::Index
get '/rack-app', to: RackApp.new
get '/flowers', to: 'flowers#index'
get '/flowers/:id', to: 'flowers#show'
redirect '/legacy', to: '/'
mount Api::App, at: '/api'
namespace 'admin' do
get '/users', to: Users::Index
end
resource 'identity' do
member do
get '/avatar'
end
collection do
get '/api_keys'
end
end
resources 'robots' do
member do
patch '/activate'
end
collection do
get '/search'
end
end
end
router = Hanami::Router.new
router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
router = Hanami::Router.new
router.get '/flowers/:id', to: ->(env) { [200, {}, ["Hello from Flower no. #{ env['router.params'][:id] }!"]] }
router = Hanami::Router.new
router.get '/flowers/:id', id: /\d+/, to: ->(env) { [200, {}, [":id must be a number!"]] }
router = Hanami::Router.new
router.get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] }
router = Hanami::Router.new
router.get '/hanami(.:format)' to: ->(env) { [200, {}, ["You've requested #{ env['router.params'][:format] }!"]] }
router = Hanami::Router.new
endpoint = ->(env) { [200, {}, ['Hello from Hanami!']] }
router.get '/hanami', to: endpoint
router.post '/hanami', to: endpoint
router.put '/hanami', to: endpoint
router.patch '/hanami', to: endpoint
router.delete '/hanami', to: endpoint
router.trace '/hanami', to: endpoint
router = Hanami::Router.new
router.root to: ->(env) { [200, {}, ['Hello from Hanami!']] }
router = Hanami::Router.new
router.get '/redirect_destination', to: ->(env) { [200, {}, ['Redirect destination!']] }
router.redirect '/legacy', to: '/redirect_destination'
router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }, as: :hanami
router.path(:hanami) # => "/hanami"
router.url(:hanami) # => "https://hanamirb.org/hanami"
router = Hanami::Router.new
router.namespace 'animals' do
namespace 'mammals' do
get '/cats', to: ->(env) { [200, {}, ['Meow!']] }, as: :cats
end
end
# and it generates:
router.path(:animals_mammals_cats) # => "/animals/mammals/cats"
Hanami::Router.new do
mount RackOne, at: '/rack1'
mount RackTwo, at: '/rack2'
mount RackThree.new, at: '/rack3'
mount ->(env) {[200, {}, ['Rack Four']]}, at: '/rack4'
mount 'dashboard#index', at: '/dashboard'
end
RackOne
is used as it is (class), because it respond to.call
RackTwo
is initialized, because it respond to#call
RackThree
is used as it is (object), because it respond to#call
- That Proc is used as it is, because it respond to
#call
- That string is resolved as
Dashboard::Index
(Hanami::Controller integration)
Everything that responds to #call
is invoked as it is:
router = Hanami::Router.new
router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
router.get '/middleware', to: Middleware
router.get '/rack-app', to: RackApp.new
router.get '/method', to: ActionControllerSubclass.action(:new)
If it's a string, it tries to instantiate a class from it:
class RackApp
def call(env)
# ...
end
end
router = Hanami::Router.new
router.get '/hanami', to: 'rack_app' # it will map to RackApp.new
It also supports Controller + Action syntax:
module Flowers
class Index
def call(env)
# ...
end
end
end
router = Hanami::Router.new
router.get '/flowers', to: 'flowers#index' # it will map to Flowers::Index.new
router = Hanami::Router.new
router.call(Rack::MockRequest.env_for('/unknown')).status # => 404
Hanami::Router
has a special convention for controllers naming.
It allows to declare an action as an endpoint, with a special syntax: <controller>#<action>
.
Hanami::Router.new do
get '/', to: 'welcome#index'
end
In the example above, the router will look for the Welcome::Index
action.
In applications where for maintainability or technical reasons, this convention
can't work, Hanami::Router
can accept a :namespace
option, which defines the
Ruby namespace where to look for actions.
For instance, given a Hanami full stack application called Bookshelf
, the
controllers are available under Bookshelf::Controllers
.
Hanami::Router.new(namespace: Bookshelf::Controllers) do
get '/', to: 'welcome#index'
end
In the example above, the router will look for the Bookshelf::Controllers::Welcome::Index
action.
router = Hanami::Router.new
router.resource 'identity'
It will map:
Verb | Path | Action | Name | Named Route |
---|---|---|---|---|
GET | /identity | Identity::Show | :show | :identity |
GET | /identity/new | Identity::New | :new | :new_identity |
POST | /identity | Identity::Create | :create | :identity |
GET | /identity/edit | Identity::Edit | :edit | :edit_identity |
PATCH | /identity | Identity::Update | :update | :identity |
DELETE | /identity | Identity::Destroy | :destroy | :identity |
If you don't need all the default endpoints, just do:
router = Hanami::Router.new
router.resource 'identity', only: [:edit, :update]
#### which is equivalent to:
router.resource 'identity', except: [:show, :new, :create, :destroy]
If you need extra endpoints:
router = Hanami::Router.new
router.resource 'identity' do
member do
get 'avatar' # maps to Identity::Avatar
end
collection do
get 'authorizations' # maps to Identity::Authorizations
end
end
router.path(:avatar_identity) # => /identity/avatar
router.path(:authorizations_identity) # => /identity/authorizations
Configure controller:
router = Hanami::Router.new
router.resource 'profile', controller: 'identity'
router.path(:profile) # => /profile # Will route to Identity::Show
We can nest resource(s):
router = Hanami::Router.new
router.resource :identity do
resource :avatar
resources :api_keys
end
router.path(:identity_avatar) # => /identity/avatar
router.path(:new_identity_avatar) # => /identity/avatar/new
router.path(:edit_identity_avatar) # => /identity/avatar/new
router.path(:identity_api_keys) # => /identity/api_keys
router.path(:identity_api_key, id: 1) # => /identity/api_keys/:id
router.path(:new_identity_api_key) # => /identity/api_keys/new
router.path(:edit_identity_api_key, id: 1) # => /identity/api_keys/:id/edit
router = Hanami::Router.new
router.resources 'flowers'
It will map:
Verb | Path | Action | Name | Named Route |
---|---|---|---|---|
GET | /flowers | Flowers::Index | :index | :flowers |
GET | /flowers/:id | Flowers::Show | :show | :flower |
GET | /flowers/new | Flowers::New | :new | :new_flower |
POST | /flowers | Flowers::Create | :create | :flowers |
GET | /flowers/:id/edit | Flowers::Edit | :edit | :edit_flower |
PATCH | /flowers/:id | Flowers::Update | :update | :flower |
DELETE | /flowers/:id | Flowers::Destroy | :destroy | :flower |
router.path(:flowers) # => /flowers
router.path(:flower, id: 23) # => /flowers/23
router.path(:edit_flower, id: 23) # => /flowers/23/edit
If you don't need all the default endpoints, just do:
router = Hanami::Router.new
router.resources 'flowers', only: [:new, :create, :show]
#### which is equivalent to:
router.resources 'flowers', except: [:index, :edit, :update, :destroy]
If you need extra endpoints:
router = Hanami::Router.new
router.resources 'flowers' do
member do
get 'toggle' # maps to Flowers::Toggle
end
collection do
get 'search' # maps to Flowers::Search
end
end
router.path(:toggle_flower, id: 23) # => /flowers/23/toggle
router.path(:search_flowers) # => /flowers/search
Configure controller:
router = Hanami::Router.new
router.resources 'blossoms', controller: 'flowers'
router.path(:blossom, id: 23) # => /blossoms/23 # Will route to Flowers::Show
We can nest resource(s):
router = Hanami::Router.new
router.resources :users do
resource :avatar
resources :favorites
end
router.path(:user_avatar, user_id: 1) # => /users/1/avatar
router.path(:new_user_avatar, user_id: 1) # => /users/1/avatar/new
router.path(:edit_user_avatar, user_id: 1) # => /users/1/avatar/edit
router.path(:user_favorites, user_id: 1) # => /users/1/favorites
router.path(:user_favorite, user_id: 1, id: 2) # => /users/1/favorites/2
router.path(:new_user_favorites, user_id: 1) # => /users/1/favorites/new
router.path(:edit_user_favorites, user_id: 1, id: 2) # => /users/1/favorites/2/edit
Rack ignores request bodies unless they come from a form submission. If we have a JSON endpoint, the payload isn't available in the params hash:
Rack::Request.new(env).params # => {}
This feature enables body parsing for specific MIME Types. It comes with a built-in JSON parser and allows to pass custom parsers.
require 'hanami/router'
endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
router = Hanami::Router.new(parsers: [:json]) do
patch '/books/:id', to: endpoint
end
curl http://localhost:2300/books/1 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"published":"true"}' \
-X PATCH
# => [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
If the json can't be parsed an exception is raised:
Hanami::Routing::Parsing::BodyParsingError
If you want to use a different JSON backend, include multi_json
in your Gemfile
.
require 'hanami/router'
# See Hanami::Routing::Parsing::Parser
class XmlParser
def mime_types
['application/xml', 'text/xml']
end
# Parse body and return a Hash
def parse(body)
# parse xml
rescue SomeXmlParsingError => e
raise Hanami::Routing::Parsing::BodyParsingError.new(e)
end
end
endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
router = Hanami::Router.new(parsers: [XmlParser.new]) do
patch '/authors/:id', to: endpoint
end
curl http://localhost:2300/authors/1 \
-H "Content-Type: application/xml" \
-H "Accept: application/xml" \
-d '<name>LG</name>' \
-X PATCH
# => [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]
require 'hanami/router'
router = Hanami::Router.new do
get '/books/:id', to: 'books#show', as: :book
end
route = router.recognize('/books/23')
route.verb # "GET"
route.action # => "books#show"
route.params # => {:id=>"23"}
route.routable? # => true
route = router.recognize(:book, id: 23)
route.verb # "GET"
route.action # => "books#show"
route.params # => {:id=>"23"}
route.routable? # => true
route = router.recognize('/books/23', method: :post)
route.verb # "POST"
route.routable? # => false
Hanami::Router uses Semantic Versioning 2.0.0
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Thanks to Joshua Hull (@joshbuddy) for his http_router.
Copyright © 2014-2017 Luca Guidi – Released under MIT License
This project was formerly known as Lotus (lotus-router
).