lib/hanami/router.rb in hanami-router-0.0.0 vs lib/hanami/router.rb in hanami-router-0.6.0
- old
+ new
@@ -1,7 +1,1164 @@
-require "hanami/router/version"
+require 'rack/request'
+require 'hanami/routing/http_router'
+require 'hanami/routing/namespace'
+require 'hanami/routing/resource'
+require 'hanami/routing/resources'
+require 'hanami/routing/error'
module Hanami
- module Router
- # Your code goes here...
+ # Rack compatible, lightweight and fast HTTP Router.
+ #
+ # @since 0.1.0
+ #
+ # @example It offers an intuitive DSL, that supports most of the HTTP verbs:
+ # require 'hanami/router'
+ #
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
+ # router = Hanami::Router.new do
+ # get '/', to: endpoint # => get and head requests
+ # post '/', to: endpoint
+ # put '/', to: endpoint
+ # patch '/', to: endpoint
+ # delete '/', to: endpoint
+ # options '/', to: endpoint
+ # trace '/', to: endpoint
+ # end
+ #
+ #
+ #
+ # @example Specify an endpoint with `:to` (Rack compatible object)
+ # require 'hanami/router'
+ #
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
+ # router = Hanami::Router.new do
+ # get '/', to: endpoint
+ # end
+ #
+ # # :to is mandatory for the default resolver (`Hanami::Routing::EndpointResolver.new`),
+ # # This behavior can be changed by passing a custom resolver to `Hanami::Router#initialize`
+ #
+ #
+ #
+ # @example Specify an endpoint with `:to` (controller and action string)
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/', to: 'articles#show' # => Articles::Show
+ # end
+ #
+ # # This is a builtin feature for a Hanami::Controller convention.
+ #
+ #
+ #
+ # @example Specify a named route with `:as`
+ # require 'hanami/router'
+ #
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org') do
+ # get '/', to: endpoint, as: :root
+ # end
+ #
+ # router.path(:root) # => '/'
+ # router.url(:root) # => 'https://hanamirb.org/'
+ #
+ # # This isn't mandatory for the default route class (`Hanami::Routing::Route`),
+ # # This behavior can be changed by passing a custom route to `Hanami::Router#initialize`
+ #
+ # @example Mount an application
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # mount Api::App, at: '/api'
+ # end
+ #
+ # # All the requests starting with "/api" will be forwarded to Api::App
+ class Router
+ # This error is raised when <tt>#call</tt> is invoked on a non-routable
+ # recognized route.
+ #
+ # @since 0.5.0
+ #
+ # @see Hanami::Router#recognize
+ # @see Hanami::Routing::RecognizedRoute
+ # @see Hanami::Routing::RecognizedRoute#call
+ # @see Hanami::Routing::RecognizedRoute#routable?
+ class NotRoutableEndpointError < Hanami::Routing::Error
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
+ PATH_INFO = 'PATH_INFO'.freeze
+
+ def initialize(env)
+ super %(Cannot find routable endpoint for #{ env[REQUEST_METHOD] } "#{ env[PATH_INFO] }")
+ end
+ end
+
+ # Returns the given block as it is.
+ #
+ # When Hanami::Router is used as a standalone gem and the routes are defined
+ # into a configuration file, some systems could raise an exception.
+ #
+ # Imagine the following file into a Ruby on Rails application:
+ #
+ # get '/', to: 'api#index'
+ #
+ # Because Ruby on Rails in production mode use to eager load code and the
+ # routes file uses top level method calls, it crashes the application.
+ #
+ # If we wrap these routes with <tt>Hanami::Router.define</tt>, the block
+ # doesn't get yielded but just returned to the caller as it is.
+ #
+ # Usually the receiver of this block is <tt>Hanami::Router#initialize</tt>,
+ # which finally evaluates the block.
+ #
+ # @param blk [Proc] a set of route definitions
+ #
+ # @return [Proc] the given block
+ #
+ # @since 0.5.0
+ #
+ # @example
+ # # apps/web/config/routes.rb
+ # Hanami::Router.define do
+ # get '/', to: 'home#index'
+ # end
+ def self.define(&blk)
+ blk
+ end
+
+ # Initialize the router.
+ #
+ # @param options [Hash] the options to initialize the router
+ #
+ # @option options [String] :scheme The HTTP scheme (defaults to `"http"`)
+ # @option options [String] :host The URL host (defaults to `"localhost"`)
+ # @option options [String] :port The URL port (defaults to `"80"`)
+ # @option options [Object, #resolve, #find, #action_separator] :resolver
+ # the route resolver (defaults to `Hanami::Routing::EndpointResolver.new`)
+ # @option options [Object, #generate] :route the route class
+ # (defaults to `Hanami::Routing::Route`)
+ # @option options [String] :action_separator the separator between controller
+ # and action name (eg. 'dashboard#show', where '#' is the :action_separator)
+ # @option options [Array<Symbol,String,Object #mime_types, parse>] :parsers
+ # the body parsers for mime types
+ #
+ # @param blk [Proc] the optional block to define the routes
+ #
+ # @return [Hanami::Router] self
+ #
+ # @since 0.1.0
+ #
+ # @example Basic example
+ # require 'hanami/router'
+ #
+ # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
+ #
+ # router = Hanami::Router.new
+ # router.get '/', to: endpoint
+ #
+ # # or
+ #
+ # router = Hanami::Router.new do
+ # get '/', to: endpoint
+ # end
+ #
+ # @example Body parsers
+ # require 'json'
+ # require 'hanami/router'
+ #
+ # # It parses JSON body and makes the attributes available to the params
+ #
+ # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
+ #
+ # router = Hanami::Router.new(parsers: [:json]) do
+ # patch '/books/:id', to: endpoint
+ # end
+ #
+ # # From the shell
+ #
+ # curl http://localhost:2300/books/1 \
+ # -H "Content-Type: application/json" \
+ # -H "Accept: application/json" \
+ # -d '{"published":"true"}' \
+ # -X PATCH
+ #
+ # # It returns
+ #
+ # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
+ #
+ # @example Custom body parser
+ # require 'hanami/router'
+ #
+ # class XmlParser
+ # def mime_types
+ # ['application/xml', 'text/xml']
+ # end
+ #
+ # # Parse body and return a Hash
+ # def parse(body)
+ # # ...
+ # end
+ # end
+ #
+ # # It parses XML body and makes the attributes available to the params
+ #
+ # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
+ #
+ # router = Hanami::Router.new(parsers: [XmlParser.new]) do
+ # patch '/authors/:id', to: endpoint
+ # end
+ #
+ # # From the shell
+ #
+ # curl http://localhost:2300/authors/1 \
+ # -H "Content-Type: application/xml" \
+ # -H "Accept: application/xml" \
+ # -d '<name>LG</name>' \
+ # -X PATCH
+ #
+ # # It returns
+ #
+ # [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]
+ def initialize(options = {}, &blk)
+ @router = Routing::HttpRouter.new(options)
+ define(&blk)
+ end
+
+ # Returns self
+ #
+ # This is a duck-typing trick for compatibility with `Hanami::Application`.
+ # It's used by `Hanami::Routing::RoutesInspector` to inspect both apps and
+ # routers.
+ #
+ # @return [self]
+ #
+ # @since 0.2.0
+ # @api private
+ def routes
+ self
+ end
+
+ # To support defining routes in the `define` wrapper.
+ #
+ # @param blk [Proc] the block to define the routes
+ #
+ # @return [Hanami::Routing::Route]
+ #
+ # @since 0.2.0
+ #
+ # @example In Hanami framework
+ # class Application < Hanami::Application
+ # configure do
+ # routes 'config/routes'
+ # end
+ # end
+ #
+ # # In `config/routes`
+ #
+ # define do
+ # get # ...
+ # end
+ def define(&blk)
+ instance_eval(&blk) if block_given?
+ end
+
+ # Check if there are defined routes
+ #
+ # @return [TrueClass,FalseClass] the result of the check
+ #
+ # @since 0.2.0
+ # @api private
+ #
+ # @example
+ #
+ # router = Hanami::Router.new
+ # router.defined? # => false
+ #
+ # router = Hanami::Router.new { get '/', to: ->(env) { } }
+ # router.defined? # => true
+ def defined?
+ @router.routes.any?
+ end
+
+ # Defines a route that accepts a GET request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @since 0.1.0
+ #
+ # @example Fixed matching string
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
+ #
+ # @example String matching with variables
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.get '/flowers/:id',
+ # to: ->(env) {
+ # [
+ # 200,
+ # {},
+ # ["Hello from Flower no. #{ env['router.params'][:id] }!"]
+ # ]
+ # }
+ #
+ # @example Variables Constraints
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.get '/flowers/:id',
+ # id: /\d+/,
+ # to: ->(env) { [200, {}, [":id must be a number!"]] }
+ #
+ # @example String matching with globbling
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.get '/*',
+ # to: ->(env) {
+ # [
+ # 200,
+ # {},
+ # ["This is catch all: #{ env['router.params'].inspect }!"]
+ # ]
+ # }
+ #
+ # @example String matching with optional tokens
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.get '/hanami(.:format)',
+ # to: ->(env) {
+ # [200, {}, ["You've requested #{ env['router.params'][:format] }!"]]
+ # }
+ #
+ # @example Named routes
+ # require 'hanami/router'
+ #
+ # 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"
+ #
+ # @example Duck typed endpoints (Rack compatible objects)
+ # require 'hanami/router'
+ #
+ # 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)
+ #
+ # # Everything that responds to #call is invoked as it is
+ #
+ # @example Duck typed endpoints (strings)
+ # require 'hanami/router'
+ #
+ # class RackApp
+ # def call(env)
+ # # ...
+ # end
+ # end
+ #
+ # router = Hanami::Router.new
+ # router.get '/hanami', to: 'rack_app' # it will map to RackApp.new
+ #
+ # @example Duck typed endpoints (string: controller + action)
+ # require 'hanami/router'
+ #
+ # 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, which is the
+ # # Hanami::Controller convention.
+ def get(path, options = {}, &blk)
+ @router.get(path, options, &blk)
+ end
+
+ # Defines a route that accepts a POST request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @see Hanami::Router#get
+ #
+ # @since 0.1.0
+ def post(path, options = {}, &blk)
+ @router.post(path, options, &blk)
+ end
+
+ # Defines a route that accepts a PUT request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @see Hanami::Router#get
+ #
+ # @since 0.1.0
+ def put(path, options = {}, &blk)
+ @router.put(path, options, &blk)
+ end
+
+ # Defines a route that accepts a PATCH request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @see Hanami::Router#get
+ #
+ # @since 0.1.0
+ def patch(path, options = {}, &blk)
+ @router.patch(path, options, &blk)
+ end
+
+ # Defines a route that accepts a DELETE request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @see Hanami::Router#get
+ #
+ # @since 0.1.0
+ def delete(path, options = {}, &blk)
+ @router.delete(path, options, &blk)
+ end
+
+ # Defines a route that accepts a TRACE request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @see Hanami::Router#get
+ #
+ # @since 0.1.0
+ def trace(path, options = {}, &blk)
+ @router.trace(path, options, &blk)
+ end
+
+ # Defines a route that accepts a OPTIONS request for the given path.
+ #
+ # @param path [String] the relative URL to be matched
+ #
+ # @param options [Hash] the options to customize the route
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
+ #
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
+ #
+ # @return [Hanami::Routing::Route] this may vary according to the :route
+ # option passed to the constructor
+ #
+ # @see Hanami::Router#get
+ #
+ # @since 0.1.0
+ def options(path, options = {}, &blk)
+ @router.options(path, options, &blk)
+ end
+
+ # Defines an HTTP redirect
+ #
+ # @param path [String] the path that needs to be redirected
+ # @param options [Hash] the options to customize the redirect behavior
+ # @option options [Fixnum] the HTTP status to return (defaults to `301`)
+ #
+ # @return [Hanami::Routing::Route] the generated route.
+ # This may vary according to the `:route` option passed to the initializer
+ #
+ # @since 0.1.0
+ #
+ # @see Hanami::Router
+ #
+ # @example
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # redirect '/legacy', to: '/new_endpoint'
+ # redirect '/legacy2', to: '/new_endpoint2', code: 302
+ # end
+ #
+ # @example
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.redirect '/legacy', to: '/new_endpoint'
+ def redirect(path, options = {}, &endpoint)
+ get(path).redirect @router.find(options), options[:code] || 301
+ end
+
+ # Defines a Ruby block: all the routes defined within it will be namespaced
+ # with the given relative path.
+ #
+ # Namespaces blocks can be nested multiple times.
+ #
+ # @param namespace [String] the relative path where the nested routes will
+ # be mounted
+ # @param blk [Proc] the block that defines the resources
+ #
+ # @return [Hanami::Routing::Namespace] the generated namespace.
+ #
+ # @since 0.1.0
+ #
+ # @see Hanami::Router
+ #
+ # @example Basic example
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # namespace 'trees' do
+ # get '/sequoia', to: endpoint # => '/trees/sequoia'
+ # end
+ # end
+ #
+ # @example Nested namespaces
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # namespace 'animals' do
+ # namespace 'mammals' do
+ # get '/cats', to: endpoint # => '/animals/mammals/cats'
+ # end
+ # end
+ # end
+ #
+ # @example
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new
+ # router.namespace 'trees' do
+ # get '/sequoia', to: endpoint # => '/trees/sequoia'
+ # end
+ def namespace(namespace, &blk)
+ Routing::Namespace.new(self, namespace, &blk)
+ end
+
+ # Defines a set of named routes for a single RESTful resource.
+ # It has a built-in integration for Hanami::Controller.
+ #
+ # @param name [String] the name of the resource
+ # @param options [Hash] a set of options to customize the routes
+ # @option options [Array<Symbol>] :only a subset of the default routes
+ # that we want to generate
+ # @option options [Array<Symbol>] :except prevent the given routes to be
+ # generated
+ # @param blk [Proc] a block of code to generate additional routes
+ #
+ # @return [Hanami::Routing::Resource]
+ #
+ # @since 0.1.0
+ #
+ # @see Hanami::Routing::Resource
+ # @see Hanami::Routing::Resource::Action
+ # @see Hanami::Routing::Resource::Options
+ #
+ # @example Default usage
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resource 'identity'
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+----------------+-------------------+----------+----------------+
+ # # | 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 |
+ # # +--------+----------------+-------------------+----------+----------------+
+ #
+ #
+ #
+ # @example Limit the generated routes with :only
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resource 'identity', only: [:show, :new, :create]
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+----------------+------------------+----------+----------------+
+ # # | 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 |
+ # # +--------+----------------+------------------+----------+----------------+
+ #
+ #
+ #
+ # @example Limit the generated routes with :except
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resource 'identity', except: [:edit, :update, :destroy]
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+----------------+------------------+----------+----------------+
+ # # | 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 |
+ # # +--------+----------------+------------------+----------+----------------+
+ #
+ #
+ #
+ # @example Additional single routes
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resource 'identity', only: [] do
+ # member do
+ # patch 'activate'
+ # end
+ # end
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+--------------------+--------------------+------+--------------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +--------+--------------------+--------------------+------+--------------------+
+ # # | PATCH | /identity/activate | Identity::Activate | | :activate_identity |
+ # # +--------+--------------------+--------------------+------+--------------------+
+ #
+ #
+ #
+ # @example Additional collection routes
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resource 'identity', only: [] do
+ # collection do
+ # get 'keys'
+ # end
+ # end
+ # end
+ #
+ # # It generates:
+ # #
+ # # +------+----------------+----------------+------+----------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +------+----------------+----------------+------+----------------+
+ # # | GET | /identity/keys | Identity::Keys | | :keys_identity |
+ # # +------+----------------+----------------+------+----------------+
+ def resource(name, options = {}, &blk)
+ Routing::Resource.new(self, name, options.merge(separator: @router.action_separator), &blk)
+ end
+
+ # Defines a set of named routes for a plural RESTful resource.
+ # It has a built-in integration for Hanami::Controller.
+ #
+ # @param name [String] the name of the resource
+ # @param options [Hash] a set of options to customize the routes
+ # @option options [Array<Symbol>] :only a subset of the default routes
+ # that we want to generate
+ # @option options [Array<Symbol>] :except prevent the given routes to be
+ # generated
+ # @param blk [Proc] a block of code to generate additional routes
+ #
+ # @return [Hanami::Routing::Resources]
+ #
+ # @since 0.1.0
+ #
+ # @see Hanami::Routing::Resources
+ # @see Hanami::Routing::Resources::Action
+ # @see Hanami::Routing::Resource::Options
+ #
+ # @example Default usage
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resources 'articles'
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+--------------------+-------------------+----------+----------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +--------+--------------------+-------------------+----------+----------------+
+ # # | GET | /articles | Articles::Index | :index | :articles |
+ # # | GET | /articles/:id | Articles::Show | :show | :articles |
+ # # | GET | /articles/new | Articles::New | :new | :new_articles |
+ # # | POST | /articles | Articles::Create | :create | :articles |
+ # # | GET | /articles/:id/edit | Articles::Edit | :edit | :edit_articles |
+ # # | PATCH | /articles/:id | Articles::Update | :update | :articles |
+ # # | DELETE | /articles/:id | Articles::Destroy | :destroy | :articles |
+ # # +--------+--------------------+-------------------+----------+----------------+
+ #
+ #
+ #
+ # @example Limit the generated routes with :only
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resources 'articles', only: [:index]
+ # end
+ #
+ # # It generates:
+ # #
+ # # +------+-----------+-----------------+--------+-------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +------+-----------+-----------------+--------+-------------+
+ # # | GET | /articles | Articles::Index | :index | :articles |
+ # # +------+-----------+-----------------+--------+-------------+
+ #
+ #
+ #
+ # @example Limit the generated routes with :except
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resources 'articles', except: [:edit, :update]
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+--------------------+-------------------+----------+----------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +--------+--------------------+-------------------+----------+----------------+
+ # # | GET | /articles | Articles::Index | :index | :articles |
+ # # | GET | /articles/:id | Articles::Show | :show | :articles |
+ # # | GET | /articles/new | Articles::New | :new | :new_articles |
+ # # | POST | /articles | Articles::Create | :create | :articles |
+ # # | DELETE | /articles/:id | Articles::Destroy | :destroy | :articles |
+ # # +--------+--------------------+-------------------+----------+----------------+
+ #
+ #
+ #
+ # @example Additional single routes
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resources 'articles', only: [] do
+ # member do
+ # patch 'publish'
+ # end
+ # end
+ # end
+ #
+ # # It generates:
+ # #
+ # # +--------+-----------------------+-------------------+------+-------------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +--------+-----------------------+-------------------+------+-------------------+
+ # # | PATCH | /articles/:id/publish | Articles::Publish | | :publish_articles |
+ # # +--------+-----------------------+-------------------+------+-------------------+
+ #
+ #
+ #
+ # @example Additional collection routes
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # resources 'articles', only: [] do
+ # collection do
+ # get 'search'
+ # end
+ # end
+ # end
+ #
+ # # It generates:
+ # #
+ # # +------+------------------+------------------+------+------------------+
+ # # | Verb | Path | Action | Name | Named Route |
+ # # +------+------------------+------------------+------+------------------+
+ # # | GET | /articles/search | Articles::Search | | :search_articles |
+ # # +------+------------------+------------------+------+------------------+
+ def resources(name, options = {}, &blk)
+ Routing::Resources.new(self, name, options.merge(separator: @router.action_separator), &blk)
+ end
+
+ # Mount a Rack application at the specified path.
+ # All the requests starting with the specified path, will be forwarded to
+ # the given application.
+ #
+ # All the other methods (eg #get) support callable objects, but they
+ # restrict the range of the acceptable HTTP verb. Mounting an application
+ # with #mount doesn't apply this kind of restriction at the router level,
+ # but let the application to decide.
+ #
+ # @param app [#call] a class or an object that responds to #call
+ # @param options [Hash] the options to customize the mount
+ # @option options [:at] the relative path where to mount the app
+ #
+ # @since 0.1.1
+ #
+ # @example Basic usage
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # mount Api::App.new, at: '/api'
+ # end
+ #
+ # # Requests:
+ # #
+ # # GET /api # => 200
+ # # GET /api/articles # => 200
+ # # POST /api/articles # => 200
+ # # GET /api/unknown # => 404
+ #
+ # @example Difference between #get and #mount
+ # require 'hanami/router'
+ #
+ # Hanami::Router.new do
+ # get '/rack1', to: RackOne.new
+ # mount RackTwo.new, at: '/rack2'
+ # end
+ #
+ # # Requests:
+ # #
+ # # # /rack1 will only accept GET
+ # # GET /rack1 # => 200 (RackOne.new)
+ # # POST /rack1 # => 405
+ # #
+ # # # /rack2 accepts all the verbs and delegate the decision to RackTwo
+ # # GET /rack2 # => 200 (RackTwo.new)
+ # # POST /rack2 # => 200 (RackTwo.new)
+ #
+ # @example Types of mountable applications
+ # require 'hanami/router'
+ #
+ # class RackOne
+ # def self.call(env)
+ # end
+ # end
+ #
+ # class RackTwo
+ # def call(env)
+ # end
+ # end
+ #
+ # class RackThree
+ # def call(env)
+ # end
+ # end
+ #
+ # module Dashboard
+ # class Index
+ # def call(env)
+ # end
+ # end
+ # end
+ #
+ # 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)
+ def mount(app, options)
+ @router.mount(app, options)
+ end
+
+ # Resolve the given Rack env to a registered endpoint and invoke it.
+ #
+ # @param env [Hash] a Rack env instance
+ #
+ # @return [Rack::Response, Array]
+ #
+ # @since 0.1.0
+ def call(env)
+ @router.call(env)
+ end
+
+ # Recognize the given env, path, or name and return a route for testing
+ # inspection.
+ #
+ # If the route cannot be recognized, it still returns an object for testing
+ # inspection.
+ #
+ # @param env [Hash, String, Symbol] Rack env, path or route name
+ # @param options [Hash] a set of options for Rack env or route params
+ # @param params [Hash] a set of params
+ #
+ # @return [Hanami::Routing::RecognizedRoute] the recognized route
+ #
+ # @since 0.5.0
+ #
+ # @see Hanami::Router#env_for
+ # @see Hanami::Routing::RecognizedRoute
+ #
+ # @example Successful Path Recognition
+ # 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" (default)
+ # route.routable? # => true
+ # route.params # => {:id=>"23"}
+ #
+ # @example Successful Rack Env Recognition
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize(Rack::MockRequest.env_for('/books/23'))
+ # route.verb # => "GET" (default)
+ # route.routable? # => true
+ # route.params # => {:id=>"23"}
+ #
+ # @example Successful Named Route Recognition
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize(:book, id: 23)
+ # route.verb # => "GET" (default)
+ # route.routable? # => true
+ # route.params # => {:id=>"23"}
+ #
+ # @example Failing Recognition For Unknown Path
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize('/books')
+ # route.verb # => "GET" (default)
+ # route.routable? # => false
+ #
+ # @example Failing Recognition For Path With Wrong HTTP Verb
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize('/books/23', method: :post)
+ # route.verb # => "POST"
+ # route.routable? # => false
+ #
+ # @example Failing Recognition For Rack Env With Wrong HTTP Verb
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize(Rack::MockRequest.env_for('/books/23', method: :post))
+ # route.verb # => "POST"
+ # route.routable? # => false
+ #
+ # @example Failing Recognition Named Route With Wrong Params
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize(:book)
+ # route.verb # => "GET" (default)
+ # route.routable? # => false
+ #
+ # @example Failing Recognition Named Route With Wrong HTTP Verb
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/books/:id', to: 'books#show', as: :book
+ # end
+ #
+ # route = router.recognize(:book, {method: :post}, {id: 1})
+ # route.verb # => "POST"
+ # route.routable? # => false
+ # route.params # => {:id=>"1"}
+ def recognize(env, options = {}, params = nil)
+ require 'hanami/routing/recognized_route'
+
+ env = env_for(env, options, params)
+ responses, _ = *@router.recognize(env)
+
+ Routing::RecognizedRoute.new(
+ responses.nil? ? responses : responses.first,
+ env, @router)
+ end
+
+ # Generate an relative URL for a specified named route.
+ # The additional arguments will be used to compose the relative URL - in
+ # case it has tokens to match - and for compose the query string.
+ #
+ # @param route [Symbol] the route name
+ #
+ # @return [String]
+ #
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
+ # recognize a route, because of the given arguments.
+ #
+ # @since 0.1.0
+ #
+ # @example
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
+ # router.get '/login', to: 'sessions#new', as: :login
+ # router.get '/:name', to: 'frameworks#show', as: :framework
+ #
+ # router.path(:login) # => "/login"
+ # router.path(:login, return_to: '/dashboard') # => "/login?return_to=%2Fdashboard"
+ # router.path(:framework, name: 'router') # => "/router"
+ def path(route, *args)
+ @router.path(route, *args)
+ end
+
+ # Generate a URL for a specified named route.
+ # The additional arguments will be used to compose the relative URL - in
+ # case it has tokens to match - and for compose the query string.
+ #
+ # @param route [Symbol] the route name
+ #
+ # @return [String]
+ #
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
+ # recognize a route, because of the given arguments.
+ #
+ # @since 0.1.0
+ #
+ # @example
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
+ # router.get '/login', to: 'sessions#new', as: :login
+ # router.get '/:name', to: 'frameworks#show', as: :framework
+ #
+ # router.url(:login) # => "https://hanamirb.org/login"
+ # router.url(:login, return_to: '/dashboard') # => "https://hanamirb.org/login?return_to=%2Fdashboard"
+ # router.url(:framework, name: 'router') # => "https://hanamirb.org/router"
+ def url(route, *args)
+ @router.url(route, *args)
+ end
+
+ # Returns an routes inspector
+ #
+ # @since 0.2.0
+ #
+ # @see Hanami::Routing::RoutesInspector
+ #
+ # @example
+ # require 'hanami/router'
+ #
+ # router = Hanami::Router.new do
+ # get '/', to: 'home#index'
+ # get '/login', to: 'sessions#new', as: :login
+ # post '/login', to: 'sessions#create'
+ # delete '/logout', to: 'sessions#destroy', as: :logout
+ # end
+ #
+ # puts router.inspector
+ # # => GET, HEAD / Home::Index
+ # login GET, HEAD /login Sessions::New
+ # POST /login Sessions::Create
+ # logout GET, HEAD /logout Sessions::Destroy
+ def inspector
+ require 'hanami/routing/routes_inspector'
+ Routing::RoutesInspector.new(@router.routes)
+ end
+
+ protected
+
+ # Fabricate Rack env for the given Rack env, path or named route
+ #
+ # @param env [Hash, String, Symbol] Rack env, path or route name
+ # @param options [Hash] a set of options for Rack env or route params
+ # @param params [Hash] a set of params
+ #
+ # @return [Hash] Rack env
+ #
+ # @since 0.5.0
+ # @api private
+ #
+ # @see Hanami::Router#recognize
+ # @see http://www.rubydoc.info/github/rack/rack/Rack%2FMockRequest.env_for
+ def env_for(env, options = {}, params = nil)
+ env = case env
+ when String
+ Rack::MockRequest.env_for(env, options)
+ when Symbol
+ begin
+ url = path(env, params || options)
+ return env_for(url, options)
+ rescue Hanami::Routing::InvalidRouteException
+ {}
+ end
+ else
+ env
+ end
+ end
end
end