# Hanami::Router Rack compatible, lightweight and fast HTTP Router for Ruby and [Hanami](http://hanamirb.org). ## Status [![Gem Version](https://badge.fury.io/rb/hanami-router.svg)](https://badge.fury.io/rb/hanami-router) [![Build Status](https://ci.hanamirb.org/api/badges/hanami/router/status.svg)](https://ci.hanamirb.org/hanami/router) [![CircleCI](https://circleci.com/gh/hanami/router/tree/master.svg?style=svg)](https://circleci.com/gh/hanami/router/tree/master) [![Test Coverage](https://codecov.io/gh/hanami/router/branch/master/graph/badge.svg)](https://codecov.io/gh/hanami/router) [![Depfu](https://badges.depfu.com/badges/5f6b8e8fa3b0d082539f0b0f84d55960/overview.svg)](https://depfu.com/github/hanami/router?project=Bundler) [![Inline Docs](http://inch-ci.org/github/hanami/router.svg)](http://inch-ci.org/github/hanami/router) ## Contact * 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 ## Rubies __Hanami::Router__ supports Ruby (MRI) 2.3+, JRuby 9.1.5.0+ ## Installation Add this line to your application's Gemfile: ```ruby gem 'hanami-router' ``` And then execute: ```shell $ bundle ``` Or install it yourself as: ```shell $ gem install hanami-router ``` ## Getting Started ```ruby require 'hanami/router' app = Hanami::Router.new do get '/', to: ->(env) { [200, {}, ['Welcome to Hanami::Router!']] } end Rack::Server.start app: app, Port: 2300 ``` ## Usage __Hanami::Router__ is designed to work as a standalone framework or within a context of a [Hanami](http://hanamirb.org) application. For the standalone usage, it supports neat features: ### A Beautiful DSL: ```ruby 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 ``` ### Fixed string matching: ```ruby router = Hanami::Router.new router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] } ``` ### String matching with variables: ```ruby router = Hanami::Router.new router.get '/flowers/:id', to: ->(env) { [200, {}, ["Hello from Flower no. #{ env['router.params'][:id] }!"]] } ``` ### Variables Constraints: ```ruby router = Hanami::Router.new router.get '/flowers/:id', id: /\d+/, to: ->(env) { [200, {}, [":id must be a number!"]] } ``` ### String matching with globbing: ```ruby router = Hanami::Router.new router.get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] } ``` ### String matching with optional tokens: ```ruby router = Hanami::Router.new router.get '/hanami(.:format)' to: ->(env) { [200, {}, ["You've requested #{ env['router.params'][:format] }!"]] } ``` ### Support for the most common HTTP methods: ```ruby 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 ``` ### Root: ```ruby router = Hanami::Router.new router.root to: ->(env) { [200, {}, ['Hello from Hanami!']] } ``` ### Redirect: ```ruby router = Hanami::Router.new router.get '/redirect_destination', to: ->(env) { [200, {}, ['Redirect destination!']] } router.redirect '/legacy', to: '/redirect_destination' ``` ### Named routes: ```ruby 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" ``` ### Namespaced routes: ```ruby 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" ``` ### Mount Rack applications: ```ruby 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 ``` 1. `RackOne` is used as it is (class), because it respond to `.call` 2. `RackTwo` is initialized, because it respond to `#call` 3. `RackThree` is used as it is (object), because it respond to `#call` 4. That Proc is used as it is, because it respond to `#call` 5. That string is resolved as `Dashboard::Index` ([Hanami::Controller](https://github.com/hanami/controller) integration) ### Duck typed endpoints: Everything that responds to `#call` is invoked as it is: ```ruby 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: ```ruby 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: ```ruby 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 ``` ### Implicit Not Found (404): ```ruby router = Hanami::Router.new router.call(Rack::MockRequest.env_for('/unknown')).status # => 404 ``` ### Controllers: `Hanami::Router` has a special convention for controllers naming. It allows to declare an action as an endpoint, with a special syntax: `#`. ```ruby Hanami::Router.new do get '/', to: 'welcome#index' end ``` In the example above, the router will look for the `Welcome::Index` action. #### Namespaces 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`. ```ruby 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. ### RESTful Resource: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby router = Hanami::Router.new router.resource 'profile', controller: 'identity' router.path(:profile) # => /profile # Will route to Identity::Show ``` #### Nested Resources We can nest resource(s): ```ruby 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 ``` ### RESTful Resources: ```ruby 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
```ruby 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: ```ruby 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: ```ruby 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: ```ruby router = Hanami::Router.new router.resources 'blossoms', controller: 'flowers' router.path(:blossom, id: 23) # => /blossoms/23 # Will route to Flowers::Show ``` #### Nested Resources We can nest resource(s): ```ruby 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 ``` ### Body Parsers 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: ```ruby 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. #### JSON Parsing ```ruby require 'hanami/router' require 'hanami/middleware/body_parser' app = Hanami::Router.new do patch '/books/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] } end use Hanami::Middleware::BodyParser, :json run app ``` ```shell 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: ```ruby Hanami::Middleware::BodyParser::BodyParsingError ``` ##### `multi_json` If you want to use a different JSON backend, include `multi_json` in your `Gemfile`. #### Custom Parsers ```ruby require 'hanami/router' require 'hanami/middleware/body_parser' # See Hanami::Routing::Parsing::Parser class XmlParser < Hanami::Routing::Parsing::Parser def mime_types ['application/xml', 'text/xml'] end # Parse body and return a Hash def parse(body) # parse xml rescue SomeXmlParsingError => e raise Hanami::Middleware::BodyParser::BodyParsingError.new(e) end end app = Hanami::Router.new do patch '/authors/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] } end use Hanami::Middleware::BodyParser, XmlParser run app ``` ```shell curl http://localhost:2300/authors/1 \ -H "Content-Type: application/xml" \ -H "Accept: application/xml" \ -d 'LG' \ -X PATCH # => [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]] ``` ## Testing ```ruby 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 ``` ## Versioning __Hanami::Router__ uses [Semantic Versioning 2.0.0](http://semver.org) ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ## Acknowledgements Thanks to Joshua Hull ([@joshbuddy](https://github.com/joshbuddy)) for his [http_router](http://rubygems.org/gems/http_router). ## Copyright Copyright © 2014-2017 Luca Guidi – Released under MIT License This project was formerly known as Lotus (`lotus-router`).