Skip to content

Latest commit

 

History

History
695 lines (468 loc) · 13.9 KB

Routing.md

File metadata and controls

695 lines (468 loc) · 13.9 KB

Base URL

By default, each class will serve the path built from its underscored name.

Ex.: Forum will serve "/forum", LatestNews will serve "/latest_news" etc.

This can be changed by setting base URL via map.

Example: - Book app should serve "/books"

class Book < E
  map '/books'

  # ...
end

[ contents ↑ ]

Canonicals

Lets say you need your News app to serve both "/news" and "/headlines" base URLs.
It is easily done by using map with multiple params.
First param will be treated as base URL, any other consequent params - as canonical ones.

Example: - News should serve both "/news" and "/headlines" paths.

class News < E
  map :news, :headlines

  def index
    # ...
  end
end

To find out either current URL is a canonical URL use canonical?
It will return nil for base URLs and a string for canonial ones.

Example:

class App < E
  map '/', '/cms'

  def page

    # on /page         canonical? == nil
    # on /cms/page     canonical? == "/page"
  end

  # ...
end

[ contents ↑ ]

Actions

Defining Espresso actions is as simple as defining Ruby methods,
cause Espresso actions actually are pure Ruby methods.

Example: - Defining 2 actions - :index and :edit

class App < E
  map '/'

  def index
    # ...
  end

  def edit
    # ...
  end
end

# Now `App` will now serve:
#  -   /      # backed by `:index` action
#  -   /edit  # backed by `:edit`  action

[ contents ↑ ]

Action Mapping

Usually actions should also contain non-alphanumeric chars.
Most common - hyphens, dots and slashes.

To address this, Espresso uses a map to translate action names into HTTP paths.

The default map looks like this:

"__"    => "/"
"___"   => "-"
"____"  => "."

Example:

def users__online  # 2 underscores
  # ...
end
# will serve users/online

def latest___news  # 3 underscores
  # ...
end
# will serve latest-news

def read____html   # 4 underscores
  # ...
end
# will serve read.html

You can define your own rules by using path_rule at class level.

Example: - Convert bang methods into .html suffixed paths

class App < E
  map '/'

  path_rule "!", ".html"

  def news!
    # ...
  end
end

# :news! action will serve /news.html path

Example: - Convert methods ending in "_j" into .json suffixed paths

class App < E
  map '/'

  path_rule /_j$/, ".json"

  def news_j
    # ...
  end
end
# :news_j action will serve /news.json path

[ contents ↑ ]

Action Aliases

Though path rules are useful enough, you can bypass them and set routes directly.

Example: make bar method to serve /bar, /some/url and /some/another/url

def bar
  # ...
end

alias_action 'some/url', :bar
alias_action 'some/another/url', :bar

Example: make foo method to serve /foo and /some/url via any request method

def foo
  # ...
end
alias_action 'some/url', :foo

Example: get_foo method will serve /foo and /some/url only via GET request method

def get_foo
  # ...
end
alias_action 'some/url', :get_foo

Also standard Ruby alias can be used:

class App < E
  map '/'

  def news
      # ...
  end
  alias news____html news
  alias headlines__recent____html news
end

Now news action will serve any of:

  • /news
  • /news.html
  • /headlines/recent.html

NOTE: Private and protected methods usually are not publicly available via HTTP.
However, if you add an action alias to such a method, it becomes public via its alias.
To alias a private/protected method and keep it private,
use standard ruby alias or alias_method rather than alias_action.

[ contents ↑ ]

Shared Actions

Sometimes you need some actions to perform on multiple controllers.

To avoid repetitive operations, just put that actions into a module and import them.

Yes, import, not include, cause include are used to share helpers and included methods wont be treated as actions.

module MySharedActions
  def foo
    # ...
  end

  def bar :with, :some, args: 'etc.'
    # ...
  end
end

class App < E
  map '/'
  import MySharedActions

  # ...
end

App will now respond to both /foo and /bar URL's

Please note that import will include all methods, however only public ones will be treated as actions. That's it, protected/private methods are not treated as actions.

Also, import wont import any setups, just actions.

See Remote Setup if you need to share setups between controllers.

[ contents ↑ ]

Parametrization

Espresso will split URL by slashes and feed obtained array to the Ruby method that backing current action.

Let's suppose we have an action like this:

class App < E
  map '/'

  def read type, status
    # ...
  end
end

If we do a request like this - "/read/news/latest", it will be decomposed as follow:

  • action - read
  • params - news/latest

Now Espresso will split params and call action:

read "news", "latest"

Current example will work just well, cause read receives as many arguments as expected.

Now let's suppose we do an request like: "/read/news"

This wont work, cause read receives 1 argument instead of 2 expected.

read "news"

And "/read/news/articles/latest" wont work either, cause read receives too many arguments.

read "news", "articles", "latest"

However, as we know, Ruby is powerful enough.

And Espresso uses this power in full.

So, when we need read method to accept 1 or 2 args, we simply give the last param a default value:

class App < E
  map '/'

  def read type, status = 'latest'
    # ...
  end
end

Now read action will serve "/read/news" as well as "/read/news/latest", "/read/news/archived", "/read/news/anything!"

Also we can make "/read/news/articles/latest" to work.

class App < E
    map '/'

    def read *types, status
        # ...
    end
end

That's it! Now when calling "/read/news/articles/latest", types will be an array like ["news", "articles"] and status will be equal to "latest".

In a word, if Ruby method works with given params, HTTP action will work too.
Otherwise, HTTP action will return "404 NotFound" error.

[ contents ↑ ]

Format

format allow to manipulate routing by instructing actions to respond to various extensions.

Also it is aimed to automatically set Content-Type header based on used extension.

Example:

class App < E
  map '/'
  format '.xml'

  def article
    # ...
  end
end

In the example above, article action will respond to both "/article" and "/article.xml" URLs.

format accepts any number of extensions.

The second meaning of format is to automatically set Content-Type header.

Content type are extracted from Rack::Mime::MIME_TYPES map.
Ex: format '.txt' will return the content type extracted via Rack::Mime::MIME_TYPES.fetch('.txt')

Worth to note that format will act on all actions.

To set format(s) only for specific actions, use format_for.

Example: - only pages action will respond to URLs ending in .html and .xml

class App < E
  map '/'

  format_for :pages, '.xml', '.html'

  def pages
    # ...
  end

  def news
    # ...
  end

  # ...
end

Now App will respond to any of "/pages", "/pages.html", "/pages.xml" and "/news" but not "/news.html" nor "/news.xml", cause format was set for pages action only.

It is also possible to disable format for specific actions by using disable_format_for:

class App < E
  map '/'

  format '.xml' # this will enable .xml format for all actions
  
  disable_format_for :news, :pages # disabling format for :pages and :news actions

  # ...
end

Worth to note that Espresso will get rid of extension passed with last param, so you get clean params without manually remove format.
Meant that when "/news/100.html" requested, you get "100" param inside news action, rather than "100.html"

Example:

class App < E
  format '.xml'

  def read item = nil
    # on /read             item == nil
    # on /read.xml         item == nil
    # on /read.xml/book    404 NotFound
    # on /read/book        item == "book"
    # on /read/book.xml    item == "book"
    # on /read/100.xml     item == "100"
    # on /read/blah.xml    item == "blah"
    # on /read/blah.json   item == "blah.json"
  end
end
/read.xml will return XML Content-Type /read/book.xml will return XML Content-Type too /read/100.xml will return XML Content-Type as well /read/anything-here.xml will return XML Content-Type either /read instead will return default Content-Type /read/book will return default Content-Type too

[ contents ↑ ]

RESTful Actions

By default, verbless actions will respond to any request method.

Example: - index action responding to any request method

class App < E

  def index
  end
end

To make an action to respond only to a specific request method, simply prepend desired request method verb to action name.

Example:

class App < E

  def post_news  # will serve POST /news
    # ...
  end

  def put_news   # will serve PUT /news
    # ...
  end

  # etc.
end

IMPORTANT: verbified actions has priority over verbless ones, regardless definition order!
That's it, if you have news and post_news actions, on POST requests post_news will be executed:

class App < E

  def post_news  # will serve POST requests
    # ...
  end

  def news  # will serve any requests except POST ones
    # ...
  end

end

[ contents ↑ ]

Hosts

By default Espresso will respond only to requests originating on the host application is running on.

To make it listen on multiple hosts, pass originating hosts via controller's map method or via mount method when controllers are mounted.

Let's suppose your application are listening on site.com.

Example: make App controller to listen also on site.org:

class App < E
  map '/', host: 'site.org'
  # or just
  map host: 'site.org'

  # ...
end

Example: make App controller to listen also on site.org and site.net:

class App < E
  map '/', hosts: ['site.org', 'site.net']
  # or just
  map hosts: ['site.org', 'site.net']

  # ...
end

Example: make Forum slice to listen on site.org beside default site.com:

module Forum
  class Posts < E
    # ...
  end
  class Users < E
    # ...
  end
end

E.new do
  mount Forum, host: 'site.org'
  run
end

Example: make Forum slice to listen on site.org and site.net beside default site.com:

E.new do
  mount Forum, hosts: ['site.org', 'site.net']
  run
end

Hosts can also be specified at app level and will apply to all controllers:

E.new do
  map '/', host: 'site.org'
  # or
  map host: 'site.org'
  # or
  map hosts: ['site.org', 'site.net']

  mount Forum
  mount Blog

  run
end

[ contents ↑ ]

Rewriter

Espresso uses a really flexible rewrite engine that allow to redirect the browser to new address as well as pass control to next matching route or to an arbitrary controller(without redirect) or just send a custom response to browser(without redirect as well).

A rewrite rule consist of a regular expression and a block that receives matches via arguments.

redirect and permanent_redirect will redirect browser to new address with 302 and 301 codes respectively.

Example:

app = E.new do

  rewrite /\A\/(.*)\.php\Z/ do |title|
    redirect Cms.route(:index, title)
  end

  # ...
end

pass will pass control to an arbitrary controller, without redirect.

Example:

class Articles < E

  def read title
    # ...
  end
end

class Pages < E
  
  def archive title
    # ...
  end
end

app = E.new do

  # pass old pages to archive action
  rewrite /\A\/(.*)\.php\Z/ do |title|
    pass Pages, :archive, title
  end

  # pages ending in html are in fact articles, so passing control to Articles controller
  rewrite /\A\/(.*)\.html\Z/ do |title|
    pass Articles, :read, title
  end

end

Important: if pass called without arguments it will pass control to next matching rule/route:

class Pages < E

  rewrite /\A\/+(.*)/ do |path| # matching pretty anything
    if matched = Redirects.where(source: path).first
      redirect matched.target
    end
    pass # no redirects matched, moving to next matching route
  end

  # actions
end

env['espresso.gateways'] will display the list of tried routes before matched one.

halt will send response to browser and stop any code execution, without redirect.

It accepts from 0 to 3 arguments.
If argument is a hash, it is added to headers.
If argument is a Integer, it is treated as Status-Code.
Any other arguments are treated as body.

If a single argument given and it is an Array, it is treated as a bare Rack response and instantly sent to browser.

Example:

app = E.new do

  rewrite /\A\/archived\/(.*)\.html\Z/ do |title|

    unless page = Model::Page.first(:url => title)
      halt 404, 'page not found'
    end

    halt page.content, 'Last-Modified' => page.last_modified.to_rfc2822
  end
end

[ contents ↑ ]