Signpost
Standalone router for rack
Release 0.1.0
is a technical preview. Feel free to create issues for bugs, suggestions or feature requests.
Basic usage
builder = Signpost::Builder.new do
root.to('home')
get('/users').to('users#index')
get('/users/:id').to('users#index')
post('/users').to('users#create')
end
App = builder.build
run App
Routes
Methods
Signpost provides API for the common HTTP methods
Signpost::Builder.new do
get('/somewhere').to('some#action')
post('/somewhere').to('some#action')
put('/somewhere').to('some#action')
patch('/somewhere').to('some#action')
options('/somewhere').to('some#action')
delete('/somewhere').to('some#action')
end
also it has a special methods: root
and match
. First one will add GET /
route named root
. Second will match all or specified methods.
Signpost::Builder.new do
root.to('home#index')
match('/blog').to('blogs')
match('/users').to('users').via(:get, :post)
end
Please note that root
will add route to the top of stack:
Signpost::Builder.new do
get('/').to(controller: 'One', action: 'home')
root.to(controller: 'Two', action: 'home')
end
will resolve controller Two
for root path
For namespaces root
method will add namespaced root named route:
Signpost::Builder.new do
root.to(Home)
namespace :admin do
root.to('admin#home')
end
end
will generate routes with root
and admin_root
names. For named routes usage see Named Routes section.
Patterns
Signpost pattern matching powered by awesome mustermann gem.
By default it uses sinatra-style patterns, but you can easily change the style by :style
option:
# in Gemfile
gem 'mustermann-flask'
# then
builder = Signpost::Builder.new(style: :flask) do
post('/users/<int:id>').to('users#create')
end
See the full list of supported styles in mustermann's readme
If you need another pattern: mustermann provides an API for custom patterns example. Do not forget to share your pattern with the world ;)
Endpoints
Basically, any rack-compatible (responds to call and returns rack result) ruby object can be an endpoint.
It may be just lambda:
get('/').to(->(env) { [200, {}, ['Hello world!']] })
# which is the same as
get('/').to do |env|
[200, {}, ['Hello world!']]
end
# also you can omit #to
get('/') do |env|
[200, {}, ['Hello world!']]
end
or any class which responds to #call
get('/').to(Home)
get('/').to('Home') # Will try to resolve constant after
Endpoint format
In case of string like admin/users#index
resolver will try to get Admin::Users::Index
class.
If it doesn't exists, Admin::Users
will be expected as an endpoint, which will dispatch corresponding action itself.
class Admin::Users
def self.call(env)
new.call(env['router.params']['action'])
end
def index
[200, {}, []]
end
end
# index action will be called this way:
get('/admin/users').to('admin/users#index')
By default, resolver looks for exact controller name. You can change naming pattern by :controller_format
option:
builder = Signpost::Builder.new(controller_format: '%{name}Controller') do
root.to('pages#index') # PagesController
end
also, you can use plural_name
and singular_name
builder = Signpost::Builder.new(controller_format: '%{plural_name}Controller')
builder.root.to('page#index') # PagesController
builder = Signpost::Builder.new(controller_format: 'Controllers::%{singular_name}')
builder.root.to('pages#index') # Controllers::Page
Hash is also a valid format for declaring endpoint:
builder.get('/users').to(controller: Users, action: 'index')
builder.get('/users').to(controller: 'Users', action: 'index')
# for single-class actions
builder.get('/users').to(controller: Users, action: Index)
builder.get('/users').to(controller: Users::Index)
builder.get('/users').to(Users::Index)
Constraints
Path constraints
get('/users/:id').to('users#show').capture(/\d+/)
get('/users/:id').to('users#show').capture(:digit)
Available POSIX character classes are: :alnum
, :alpha
, :blank
, :cntrl
, :digit
, :graph
, :lower
, :print
, :punct
, :space
, :upper
, :xdigit
, :word
and :ascii
If you need more:
get('/unicorns/:id_or_name').to('unicorns#show').capture([/\d+/, :word])
get('/unicorns/:type/:id').to('unicorns#show').capture(id: /\d+/, type: :word)
get('/images/:id.:ext').to('images').capture(id: /\d+/, ext: ['png', 'jpg'])
Exclude constraints
delete('/users/:name').to('users#destroy').except('/users/admin')
get('/pages/*slug/edit').to('pages#edit').except('/pages/system/*/edit')
Logical constraints
get('/stats').to(Dashboard).constraint(->(env) { env['RACK_ENV'] == 'development' })
get('/admin').to('admin#index').constraint(IpRestrictor.new) # Objects with #call method allowed too
get('/stats').to(Dashboard).constraints(
->(env) { env['RACK_ENV'] == 'development' },
->(env) { env['admin'] }
)
Named routes
Named routes can be used in path helpers:
builder = Signpost::Builder.new do
root.to('Home')
get('/users/:id').to('users#show').as(:show_users)
namespace :users do
post('/types').to('types#create').as(:create, :type)
end
end
router = builder.build
router.expand(:root) # '/'
router.expand(:show_users, id: 2) # /users/2
router.expand(:create_users_type) # /users/types
Nested routes
Routes can be grouped into subroute for better readability and performance
Signpost::Builder.new do
within('/users') do
root.to(controller: 'users', action: 'index')
patch(':id').to(controller: 'users', action: 'update') # /users/:id
get('inventory').to('users#inventory') # /users/inventory
get('/inventory').to('users#inventory') # the same, leading slash will be ignored
within('/types') do
post('/').to('users/types#create') # /users/types
patch(':id').to('users/types#update') # /users/2
end
end
end
note, that within
does not introduce class or name namespace. So root
inside within
block will not add any named route.
Also, for sinatra-style, only trailing slash will match (this is the subject to fix).
Namespaces
Namespace is basically just within
which adds class and named route namespace:
namespace :admin do
root.to('dashboard') # :admin_root name, /admin path and Admin::Dashboard controller
namespace :types do
get('edit').to(action: 'edit').as(:edit) # :admin_types_edit name, Admin::Types controller and /admin/types/edit path
get(':id/properties').to(action: 'show').as(:show, :properties) # :show_admin_types_properties name
end
end
Resources
Will be introduced in 0.2.0
Redirects
Simple redirects:
Signpost::Builder.new do
redirect('/horses').to('/unicorns').permanent
redirect('/ponies').to('/unicorns')
redirect('/zebras').to('/unicorns').with_status(307)
end
by default, redirect uses 303
code. It can be changed in options
Pattern to pattern redirects are also allowed:
redirect('/birds/:id').to('/dragons/:id')
redirect('/goats/:id').to('/unicorns/{id}s')
If source has more parameters than target, additional values will be ignored. To change this, you can use :append
directive:
redirect('/goats/:type/:id').to('/unicorns/:id', :append)
or set option :default_redirect_additional_values
to :append
. All additional values will be applied as query string params: /goats/angora/2
will be redirected to /unicorns/2?type=angora
.
If target route have name, it's easy to reuse the pattern:
get('/unicorns/:id').to('unicorns#show').as(:show_unicorn)
redirect('/zebras/:id').to(:show_unicorn)
For more complex redirects you can use block:
redirect('/horses/:id') do
"/horses/#{params['id']}/#{env['REQUEST_METHOD']}"
end
redirect('/ponies/:type') do
expand(:show_unicorn, params['type'].downcase)
end
Options
Name | Default | Values | Description |
---|---|---|---|
:style |
:sinatra |
See mustermann's readme | URL pattern style |
:controller_format |
'%{name}' |
Format for controller name. See Endpoint format section | |
:params_key |
'router.params' |
Key used for rack environment to conduct matched values, controller name and action name | |
:rack_params |
false |
true, false | To be compatible with Rack::Request params, router params can be merged into rack.request.query_hash . This option will turn on this behaviour |
:default_redirect_status |
303 |
Any 3xx code |
HTTP Status wich will be used by redirect when not specified |
For :default_redirect_additional_values
see Redirects