Project

signpost

0.0
No commit activity in last 3 years
No release in over 3 years
Standalone router for rack and nothing else
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

= 0.0.2
 Project Readme

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