Lotus::Router
Rack compatible, lightweight and fast HTTP Router for Ruby and Lotus.
Status
![Inline docs](http://inch-ci.org/github/lotus/router.png)
Contact
Rubies
Lotus::Router supports Ruby (MRI) 2+, JRuby 9k+
Installation
Add this line to your application's Gemfile:
gem 'lotus-router'
And then execute:
$ bundle
Or install it yourself as:
$ gem install lotus-router
Getting Started
require 'lotus/router'
app = Lotus::Router.new do
get '/', to: ->(env) { [200, {}, ['Welcome to Lotus::Router!']] }
end
Rack::Server.start app: app, Port: 2300
Usage
Lotus::Router is designed to work as a standalone framework or within a
context of a Lotus application.
For the standalone usage, it supports neat features:
A Beautiful DSL:
Lotus::Router.new do
get '/', to: ->(env) { [200, {}, ['Hi!']] }
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:
router = Lotus::Router.new
router.get '/lotus', to: ->(env) { [200, {}, ['Hello from Lotus!']] }
String matching with variables:
router = Lotus::Router.new
router.get '/flowers/:id', to: ->(env) { [200, {}, ["Hello from Flower no. #{ env['router.params'][:id] }!"]] }
Variables Constraints:
router = Lotus::Router.new
router.get '/flowers/:id', id: /\d+/, to: ->(env) { [200, {}, [":id must be a number!"]] }
String matching with globbing:
router = Lotus::Router.new
router.get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] }
String matching with optional tokens:
router = Lotus::Router.new
router.get '/lotus(.:format)' to: ->(env) { [200, {}, ["You've requested #{ env['router.params'][:format] }!"]] }
Support for the most common HTTP methods:
router = Lotus::Router.new
endpoint = ->(env) { [200, {}, ['Hello from Lotus!']] }
router.get '/lotus', to: endpoint
router.post '/lotus', to: endpoint
router.put '/lotus', to: endpoint
router.patch '/lotus', to: endpoint
router.delete '/lotus', to: endpoint
router.trace '/lotus', to: endpoint
Redirect:
router = Lotus::Router.new
router.get '/redirect_destination', to: ->(env) { [200, {}, ['Redirect destination!']] }
router.redirect '/legacy', to: '/redirect_destination'
Named routes:
router = Lotus::Router.new(scheme: 'https', host: 'lotusrb.org')
router.get '/lotus', to: ->(env) { [200, {}, ['Hello from Lotus!']] }, as: :lotus
router.path(:lotus)
router.url(:lotus)
Namespaced routes:
router = Lotus::Router.new
router.namespace 'animals' do
namespace 'mammals' do
get '/cats', to: ->(env) { [200, {}, ['Meow!']] }, as: :cats
end
end
router.path(:animals_mammals_cats)
Mount Rack applications:
Lotus::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
(Lotus::Controller integration)
Duck typed endpoints:
Everything that responds to #call
is invoked as it is:
router = Lotus::Router.new
router.get '/lotus', to: ->(env) { [200, {}, ['Hello from Lotus!']] }
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 = Lotus::Router.new
router.get '/lotus', to: 'rack_app'
It also supports Controller + Action syntax:
module Flowers
class Index
def call(env)
end
end
end
router = Lotus::Router.new
router.get '/flowers', to: 'flowers#index'
Implicit Not Found (404):
router = Lotus::Router.new
router.call(Rack::MockRequest.env_for('/unknown')).status
Controllers:
Lotus::Router
has a special convention for controllers naming.
It allows to declare an action as an endpoint, with a special syntax: <controller>#<action>
.
Lotus::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, Lotus::Router
can accept a :namespace
option, which defines the
Ruby namespace where to look for actions.
For instance, given a Lotus full stack application called Bookshelf
, the
controllers are available under Bookshelf::Controllers
.
Lotus::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:
router = Lotus::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 = Lotus::Router.new
router.resource 'identity', only: [:edit, :update]
router.resource 'identity', except: [:show, :new, :create, :destroy]
If you need extra endpoints:
router = Lotus::Router.new
router.resource 'identity' do
member do
get 'avatar'
end
collection do
get 'authorizations'
end
end
router.path(:avatar_identity)
router.path(:authorizations_identity)
Configure controller:
router = Lotus::Router.new
router.resource 'profile', controller: 'identity'
router.path(:profile)
Nested Resources
We can nest resource(s):
router = Lotus::Router.new
router.resource :identity do
resource :avatar
resources :api_keys
end
router.path(:identity_avatar)
router.path(:new_identity_avatar)
router.path(:edit_identity_avatar)
router.path(:identity_api_keys)
router.path(:identity_api_key)
router.path(:new_identity_api_key)
router.path(:edit_identity_api_key)
RESTful Resources:
router = Lotus::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)
router.path(:flower, id: 23)
router.path(:edit_flower, id: 23)
If you don't need all the default endpoints, just do:
router = Lotus::Router.new
router.resources 'flowers', only: [:new, :create, :show]
router.resources 'flowers', except: [:index, :edit, :update, :destroy]
If you need extra endpoints:
router = Lotus::Router.new
router.resources 'flowers' do
member do
get 'toggle'
end
collection do
get 'search'
end
end
router.path(:toggle_flower, id: 23)
router.path(:search_flowers)
Configure controller:
router = Lotus::Router.new
router.resources 'blossoms', controller: 'flowers'
router.path(:blossom, id: 23)
Nested Resources
We can nest resource(s):
router = Lotus::Router.new
router.resources :users do
resource :avatar
resources :favorites
end
router.path(:user_avatar, user_id: 1)
router.path(:new_user_avatar, user_id: 1)
router.path(:edit_user_avatar, user_id: 1)
router.path(:user_favorites, user_id: 1)
router.path(:user_favorite, user_id: 1, id: 2)
router.path(:new_user_favorites, user_id: 1)
router.path(:edit_user_favorites, user_id: 1, id: 2)
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:
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
require 'lotus/router'
endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
router = Lotus::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:
Lotus::Routing::Parsing::BodyParsingError
Custom Parsers
require 'lotus/router'
class XmlParser
def mime_types
['application/xml', 'text/xml']
end
def parse(body)
rescue SomeXmlParsingError => e
raise Lotus::Routing::Parsing::BodyParsingError.new(e)
end
end
endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
router = Lotus::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\"}"]]
Testing
require 'lotus/router'
router = Lotus::Router.new do
get '/books/:id', to: 'books#show', as: :book
end
route = router.recognize('/books/23')
route.verb
route.action
route.params
route.routable?
route = router.recognize(:book, id: 23)
route.verb
route.action
route.params
route.routable?
route = router.recognize('/books/23', method: :post)
route.verb
route.routable?
Versioning
Lotus::Router uses Semantic Versioning 2.0.0
Contributing
- 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
Acknowledgements
Thanks to Joshua Hull (@joshbuddy) for his
http_router.
Copyright
Copyright © 2014-2016 Luca Guidi – Released under MIT License