Hanami::Router

Rack compatible, lightweight and fast HTTP Router for Ruby and Hanami.

Status

Gem Version Build Status Coverage Code Climate Dependencies Inline docs

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 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 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: <controller>#<action>.

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) # => /identity/api_keys/:id router.path(:new_identity_api_key) # => /identity/api_keys/new router.path(:edit_identity_api_key) # => /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’

endpoint = ->(env) { [200, {},[env[‘router.params’].inspect]] }

router = Hanami::Router.new(parsers: [:json]) do patch ‘/books/:id’, to: endpoint end ```

```shell curl http://localhost:2300/books/1 \ -H “Content-Type: application/json” \ -H “Accept: application/json” \ -d ‘“published”:”true”’ \ -X PATCH

=> [200, {}, [””]]

```

If the json can’t be parsed an exception is raised:

ruby Hanami::Routing::Parsing::BodyParsingError

multi_json

If you want to use a different JSON backend, include multi_json in your Gemfile.

Custom Parsers

```ruby 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 ```

```shell curl http://localhost:2300/authors/1 \ -H “Content-Type: application/xml” \ -H “Accept: application/xml” \ -d ‘LG’ \ -X PATCH

=> [200, {}, [””]]

```

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

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) for his http_router.

Copyright © 2014-2016 Luca Guidi – Released under MIT License

This project was formerly known as Lotus (lotus-router).