# frozen_string_literal: true require "rack/request" require "dry/inflector" require "hanami/routing" require "hanami/utils/hash" # Hanami # # @since 0.1.0 module Hanami # 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 # rubocop:disable Metrics/ClassLength # @since 2.0.0 # @api private attr_reader :inflector # This error is raised when #call 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 # @since 0.5.0 # @api private REQUEST_METHOD = "REQUEST_METHOD" # @since 0.5.0 # @api private PATH_INFO = "PATH_INFO" # @since 0.5.0 def initialize(env) super %(Cannot find routable endpoint for #{env[REQUEST_METHOD]} "#{env[PATH_INFO]}") end end # Defines root path # # @since 0.7.0 # @api private # # @see Hanami::Router#root ROOT_PATH = "/" # 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 Hanami::Router.define, the block # doesn't get yielded but just returned to the caller as it is. # # Usually the receiver of this block is Hanami::Router#initialize, # 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 [Object, #pluralize, #singularize] :inflector # the inflector class (defaults to `Dry::Inflector.new`) # # @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 'hanami/router' # require 'hanami/middleware/body_parser' # # app = Hanami::Router.new do # patch '/books/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] } # end # # use Hanami::Middleware::BodyParser, :json # run app # # # 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' # require 'hanami/middleware/body_parser' # # # class XmlParser < Hanami::Middleware::BodyParser::Parser # def mime_types # ['application/xml', 'text/xml'] # end # # # Parse body and return a Hash # def parse(body) # # parse xml # end # end # # app = Hanami::Router.new do # patch '/authors/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] } # end # # use Hanami::Middleware::BodyParser, XmlParser # run app # # # From the shell # # curl http://localhost:2300/authors/1 \ # -H "Content-Type: application/xml" \ # -H "Accept: application/xml" \ # -d 'LG' \ # -X PATCH # # # It returns # # [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]] # # rubocop:disable Metrics/MethodLength def initialize(scheme: "http", host: "localhost", port: 80, prefix: "", namespace: nil, configuration: nil, inflector: Dry::Inflector.new, not_found: NOT_FOUND, not_allowed: NOT_ALLOWED, &blk) @routes = [] @named = {} @namespace = namespace @configuration = configuration @base = Routing::Uri.build(scheme: scheme, host: host, port: port) @prefix = Utils::PathPrefix.new(prefix) @inflector = inflector @not_found = not_found @not_allowed = not_allowed instance_eval(&blk) unless blk.nil? freeze end # rubocop:enable Metrics/MethodLength # Freeze the router # # @since 2.0.0 def freeze @routes.freeze super 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 # 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? @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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(GET, path, to, as, namespace, configuration, constraints, &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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(POST, path, to, as, namespace, configuration, constraints, &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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(PUT, path, to, as, namespace, configuration, constraints, &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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(PATCH, path, to, as, namespace, configuration, constraints, &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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(DELETE, path, to, as, namespace, configuration, constraints, &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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(TRACE, path, to, as, namespace, configuration, constraints, &blk) end # Defines a route that accepts a LINK 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.8.0 def link(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(LINK, path, to, as, namespace, configuration, constraints, &blk) end # Defines a route that accepts an UNLINK 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.8.0 def unlink(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(UNLINK, path, to, as, namespace, configuration, constraints, &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, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk) add_route(OPTIONS, path, to, as, namespace, configuration, constraints, &blk) end # Defines a root route (a GET route for '/') # # @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.7.0 # # @example Fixed matching string # require 'hanami/router' # # router = Hanami::Router.new # router.root to: ->(env) { [200, {}, ['Hello from Hanami!']] } # # @example Included names as `root` (for path and url helpers) # require 'hanami/router' # # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org') # router.root to: ->(env) { [200, {}, ['Hello from Hanami!']] } # # router.path(:root) # => "/" # router.url(:root) # => "https://hanamirb.org/" def root(to: nil, as: :root, prefix: Utils::PathPrefix.new, namespace: nil, configuration: nil, &blk) add_route(GET, prefix.join(ROOT_PATH), to, as, namespace, configuration, &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, to:, code: 301) to = Routing::Redirect.new(@prefix.join(to).to_s, code) add_route(GET, path, to) end # Defines a Ruby block: all the routes defined within it will be prefixed # with the given relative path. # # Prefix blocks can be nested multiple times. # # @param path [String] the relative path where the nested routes will # be mounted # @param blk [Proc] the block that defines the resources # # @return [void] # # @since 2.0.0 # # @see Hanami::Router # # @example Basic example # require "hanami/router" # # Hanami::Router.new do # prefix "trees" do # get "/sequoia", to: endpoint # => "/trees/sequoia" # end # end # # @example Nested prefix # require "hanami/router" # # Hanami::Router.new do # prefix "animals" do # prefix "mammals" do # get "/cats", to: endpoint # => "/animals/mammals/cats" # end # end # end def prefix(path, namespace: nil, configuration: nil, &blk) Routing::Prefix.new(self, path, namespace, configuration, &blk) end # Defines a scope for routes. # # A scope is a combination of a path prefix and a Ruby namespace. # # @param prefix [String] the path prefix # @param namespace [Module] the Ruby namespace where to lookup endpoints # @param configuration [Hanami::Controller::Configuration] the action # configuration # @param blk [Proc] the routes definition block # # @since 2.0.0 # @api private # # @example # require "hanami/router" # require "hanami/controller" # # configuration = Hanami::Controller::Configuration.new # # Hanami::Router.new do # scope "/admin", namespace: Admin::Controllers, configuration: configuration do # root to: "home#index" # end # end def scope(prefix, namespace:, configuration:, &blk) Routing::Scope.new(self, prefix, namespace, configuration, &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] :only a subset of the default routes # that we want to generate # @option options [Array] :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: Routing::Endpoint::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] :only a subset of the default routes # that we want to generate # @option options [Array] :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: Routing::Endpoint::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, at:, host: nil) app = App.new(@prefix.join(at).to_s, Routing::Endpoint.find(app, @namespace), host: host) @routes.push(app) 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) (@routes.find { |r| r.match?(env) } || fallback(env)).call(env) end def fallback(env) if @routes.find { |r| r.match_path?(env) } @not_allowed else @not_found end 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) env = env_for(env, options, params) # FIXME: this finder is shared with #call and should be extracted route = @routes.find { |r| r.match?(env) } Routing::RecognizedRoute.new(route, env, @namespace) 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 = {}) @named.fetch(route).path(args) rescue KeyError raise Hanami::Routing::InvalidRouteException.new("No route could be generated for #{route.inspect} - please check given arguments") 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 = {}) @base + path(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(@routes, @prefix) 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) # rubocop:disable Metrics/MethodLength 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 private PATH_INFO = "PATH_INFO" SCRIPT_NAME = "SCRIPT_NAME" SERVER_NAME = "SERVER_NAME" REQUEST_METHOD = "REQUEST_METHOD" PARAMS = "router.params" GET = "GET" HEAD = "HEAD" POST = "POST" PUT = "PUT" PATCH = "PATCH" DELETE = "DELETE" TRACE = "TRACE" OPTIONS = "OPTIONS" LINK = "LINK" UNLINK = "UNLINK" NOT_FOUND = ->(_) { [404, { "Content-Length" => "9" }, ["Not Found"]] }.freeze NOT_ALLOWED = ->(_) { [405, { "Content-Length" => "18" }, ["Method Not Allowed"]] }.freeze ROOT = "/" BODY = 2 attr_reader :configuration # Application # # @since 2.0.0 # @api private class App def initialize(path, endpoint, host: nil) @path = Mustermann.new(path, type: :rails, version: "5.0") @prefix = path.to_s @endpoint = endpoint @host = host freeze end def match?(env) match_path?(env) end def match_path?(env) result = env[PATH_INFO].start_with?(@prefix) result &&= @host == env[SERVER_NAME] unless @host.nil? result end def call(env) env[PARAMS] ||= {} env[PARAMS].merge!(Utils::Hash.deep_symbolize(@path.params(env[PATH_INFO]) || {})) env[SCRIPT_NAME] = @prefix env[PATH_INFO] = env[PATH_INFO].sub(@prefix, "") env[PATH_INFO] = "/" if env[PATH_INFO] == "" @endpoint.call(env) end end def add_route(verb, path, to, as = nil, namespace = nil, config = nil, constraints = {}, &blk) to ||= blk config ||= configuration path = path.to_s endpoint = Routing::Endpoint.find(to, namespace || @namespace, config) route = Routing::Route.new(verb_for(verb), @prefix.join(path).to_s, endpoint, constraints) @routes.push(route) @named[as] = route unless as.nil? end def verb_for(value) if value == GET [GET, HEAD] else [value] end end end end