lib/hanami/router.rb in hanami-router-2.0.0.alpha1 vs lib/hanami/router.rb in hanami-router-2.0.0.alpha2
- old
+ new
@@ -1,1056 +1,475 @@
# frozen_string_literal: true
-require "rack/request"
-require "dry/inflector"
-require "hanami/routing"
-require "hanami/utils/hash"
+require "rack/utils"
-# 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
+ require "hanami/router/version"
+ require "hanami/router/error"
+ require "hanami/router/segment"
+ require "hanami/router/redirect"
+ require "hanami/router/prefix"
+ require "hanami/router/params"
+ require "hanami/router/trie"
+ require "hanami/router/block"
+ require "hanami/router/url_helpers"
- # This error is raised when <tt>#call</tt> is invoked on a non-routable
- # recognized route.
+ # URL helpers for other Hanami integrations
#
- # @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 = "/"
+ # @since 2.0.0
+ attr_reader :url_helpers
# 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'
+ # get "/", to: ->(*) { ... }
# end
def self.define(&blk)
blk
end
- # Initialize the router.
+ # Initialize the router
#
- # @param options [Hash] the options to initialize the router
+ # @param base_url [String] the base URL where the HTTP application is
+ # deployed
+ # @param prefix [String] the relative URL prefix where the HTTP application
+ # is deployed
+ # @param resolver [#call(path, to)] a resolver for route entpoints
+ # @param block_context [Hanami::Router::Block::Context)
+ # @param blk [Proc] the route definitions
#
- # @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'
+ # @return [Hanami::Router]
#
- # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
+ # @example Base usage
+ # require "hanami/router"
#
- # router = Hanami::Router.new
- # router.get '/', to: endpoint
- #
- # # or
- #
- # router = Hanami::Router.new do
- # get '/', to: endpoint
+ # Hanami::Router.new do
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
# end
+ def initialize(base_url: DEFAULT_BASE_URL, prefix: DEFAULT_PREFIX, resolver: DEFAULT_RESOLVER, block_context: nil, &blk)
+ # TODO: verify if Prefix can handle both name and path prefix
+ @path_prefix = Prefix.new(prefix)
+ @name_prefix = Prefix.new("")
+ @url_helpers = UrlHelpers.new(base_url)
+ @resolver = resolver
+ @block_context = block_context
+ @fixed = {}
+ @variable = {}
+ @globbed = {}
+ @mounted = {}
+ instance_eval(&blk)
+ end
+
+ # Resolve the given Rack env to a registered endpoint and invokes it.
#
- # @example Body parsers
+ # @param env [Hash] a Rack env
#
- # require 'hanami/router'
- # require 'hanami/middleware/body_parser'
+ # @return [Array] a finalized Rack env response
#
- # app = Hanami::Router.new do
- # patch '/books/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] }
- # end
+ # @since 0.1.0
+ def call(env)
+ endpoint, params = lookup(env)
+
+ unless endpoint
+ return not_allowed(env) ||
+ not_found
+ end
+
+ endpoint.call(
+ _params(env, params)
+ ).to_a
+ end
+
+ # Defines a named root route (a GET route for "/")
#
- # use Hanami::Middleware::BodyParser, :json
- # run app
+ # @param to [#call] the Rack endpoint
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
#
- # # From the shell
+ # @since 0.7.0
#
- # curl http://localhost:2300/books/1 \
- # -H "Content-Type: application/json" \
- # -H "Accept: application/json" \
- # -d '{"published":"true"}' \
- # -X PATCH
+ # @see #get
+ # @see #path
+ # @see #url
#
- # # It returns
+ # @example Proc endpoint
+ # require "hanami/router"
#
- # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
+ # router = Hanami::Router.new do
+ # root to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
+ # end
#
- # @example Custom body parser
+ # @example Block endpoint
+ # require "hanami/router"
#
- # require 'hanami/router'
- # require 'hanami/middleware/body_parser'
+ # router = Hanami::Router.new do
+ # root do
+ # "Hello from Hanami!"
+ # end
+ # end
#
+ # @example URL helpers
+ # require "hanami/router"
#
- # 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]] }
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
+ # root do
+ # "Hello from Hanami!"
+ # end
# 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 '<name>LG</name>' \
- # -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
+ # router.path(:root) # => "/"
+ # router.url(:root) # => "https://hanamirb.org"
+ def root(to: nil, &blk)
+ get("/", to: to, as: :root, &blk)
end
- # rubocop:enable Metrics/MethodLength
- # Freeze the router
+ # Defines a route that accepts GET requests for the given path.
+ # It also defines a route to accept HEAD requests.
#
- # @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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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'
+ # @see #initialize
+ # @see #path
+ # @see #url
#
- # router = Hanami::Router.new
- # router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
+ # @example Proc endpoint
+ # require "hanami/router"
#
- # @example String matching with variables
- # require 'hanami/router'
+ # Hanami::Router.new do
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
+ # end
#
- # router = Hanami::Router.new
- # router.get '/flowers/:id',
- # to: ->(env) {
- # [
- # 200,
- # {},
- # ["Hello from Flower no. #{ env['router.params'][:id] }!"]
- # ]
- # }
+ # @example Block endpoint
+ # require "hanami/router"
#
- # @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)
- # # ...
+ # Hanami::Router.new do
+ # get "/" do
+ # "OK"
# end
# end
#
- # router = Hanami::Router.new
- # router.get '/hanami', to: 'rack_app' # it will map to RackApp.new
+ # @example Named route
+ # require "hanami/router"
#
- # @example Duck typed endpoints (string: controller + action)
- # require 'hanami/router'
- #
- # module Flowers
- # class Index
- # def call(env)
- # # ...
- # end
- # end
+ # router = Hanami::Router.new do
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }, as: :welcome
# end
#
- # router = Hanami::Router.new
- # router.get '/flowers', to: 'flowers#index'
+ # router.path(:welcome) # => "/"
+ # router.url(:welcome) # => "http://localhost/"
#
- # # 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)
+ # @example Constraints
+ # require "hanami/router"
+ #
+ # Hanami::Router.new do
+ # get "/users/:id", to: ->(*) { [200, {}, ["OK"]] }, id: /\d+/
+ # end
+ def get(path, to: nil, as: nil, **constraints, &blk)
+ add_route("GET", path, to, as, constraints, &blk)
+ add_route("HEAD", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a POST request for the given path.
+ # Defines a route that accepts POST requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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)
+ #
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def post(path, to: nil, as: nil, **constraints, &blk)
+ add_route("POST", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a PUT request for the given path.
+ # Defines a route that accepts PATCH requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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)
+ #
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def patch(path, to: nil, as: nil, **constraints, &blk)
+ add_route("PATCH", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a PATCH request for the given path.
+ # Defines a route that accepts PUT requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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)
+ #
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def put(path, to: nil, as: nil, **constraints, &blk)
+ add_route("PUT", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a DELETE request for the given path.
+ # Defines a route that accepts DELETE requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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)
+ #
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def delete(path, to: nil, as: nil, **constraints, &blk)
+ add_route("DELETE", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a TRACE request for the given path.
+ # Defines a route that accepts TRACE requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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)
+ #
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def trace(path, to: nil, as: nil, **constraints, &blk)
+ add_route("TRACE", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a LINK request for the given path.
+ # Defines a route that accepts OPTIONS requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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
#
- # @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)
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def options(path, to: nil, as: nil, **constraints, &blk)
+ add_route("OPTIONS", path, to, as, constraints, &blk)
end
- # Defines a route that accepts an UNLINK request for the given path.
+ # Defines a route that accepts LINK requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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
#
- # @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)
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def link(path, to: nil, as: nil, **constraints, &blk)
+ add_route("LINK", path, to, as, constraints, &blk)
end
- # Defines a route that accepts a OPTIONS request for the given path.
+ # Defines a route that accepts UNLINK requests 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 to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param constraints [Hash] a set of constraints for path variables
# @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)
+ # @see #get
+ # @see #initialize
+ # @see #path
+ # @see #url
+ def unlink(path, to: nil, as: nil, **constraints, &blk)
+ add_route("UNLINK", path, to, as, constraints, &blk)
end
- # Defines an HTTP redirect
+ # Defines a route that redirects the incoming request to another path.
#
- # @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`)
+ # @param path [String] the relative URL to be matched
+ # @param to [#call] the Rack endpoint
+ # @param as [Symbol] a unique name for the route
+ # @param code [Integer] a HTTP status code to use for the redirect
#
- # @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)
+ # @see #get
+ # @see #initialize
+ def redirect(path, to: nil, as: nil, code: DEFAULT_REDIRECT_CODE)
+ get(path, to: _redirect(to, code), as: as)
end
- # Defines a Ruby block: all the routes defined within it will be prefixed
- # with the given relative path.
+ # Defines a routing scope. Routes defined in the context of a scope,
+ # inherit the given path as path prefix and as a named routes prefix.
#
- # Prefix blocks can be nested multiple times.
+ # @param path [String] the scope path to be used as a path prefix
+ # @param blk [Proc] the routes definitions withing the scope
#
- # @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
+ # @see #path
#
- # @example Basic example
+ # @example
# require "hanami/router"
#
- # Hanami::Router.new do
- # prefix "trees" do
- # get "/sequoia", to: endpoint # => "/trees/sequoia"
+ # router = Hanami::Router.new do
+ # scope "v1" do
+ # get "/users", to: ->(*) { ... }, as: :users
# 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)
+ # router.path(:v1_users) # => "/v1/users"
+ def scope(path, &blk)
+ path_prefix = @path_prefix
+ name_prefix = @name_prefix
+
+ begin
+ @path_prefix = @path_prefix.join(path.to_s)
+ @name_prefix = @name_prefix.join(path.to_s)
+ instance_eval(&blk)
+ ensure
+ @path_prefix = path_prefix
+ @name_prefix = name_prefix
+ end
end
- # Defines a scope for routes.
+ # Mount a Rack application at the specified path.
+ # All the requests starting with the specified path, will be forwarded to
+ # the given application.
#
- # A scope is a combination of a path prefix and a Ruby namespace.
+ # 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 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
+ # @param app [#call] a class or an object that responds to #call
+ # @param at [String] the relative path where to mount the app
+ # @param constraints [Hash] a set of constraints for path variables
#
- # @since 2.0.0
- # @api private
+ # @since 0.1.1
#
# @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
+ # mount MyRackApp.new, at: "/foo"
# end
- def scope(prefix, namespace:, configuration:, &blk)
- Routing::Scope.new(self, prefix, namespace, configuration, &blk)
+ def mount(app, at:, **constraints)
+ path = prefixed_path(at)
+ prefix = Segment.fabricate(path, **constraints)
+ @mounted[prefix] = @resolver.call(path, app)
end
- # Defines a set of named routes for a single RESTful resource.
- # It has a built-in integration for Hanami::Controller.
+ # 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 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
+ # @param name [Symbol] the route name
#
- # @return [Hanami::Routing::Resource]
+ # @return [String]
#
- # @since 0.1.0
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
+ # recognize a route, because of the given arguments.
#
- # @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<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
+ # @see #url
#
- # @example Default usage
- # require 'hanami/router'
+ # @example
+ # require "hanami/router"
#
- # Hanami::Router.new do
- # resources 'articles'
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
+ # get "/login", to: ->(*) { ... }, as: :login
+ # get "/:name", to: ->(*) { ... }, as: :framework
# 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)
+ # router.path(:login) # => "/login"
+ # router.path(:login, return_to: "/dashboard") # => "/login?return_to=%2Fdashboard"
+ # router.path(:framework, name: "router") # => "/router"
+ def path(name, variables = {})
+ @url_helpers.path(name, variables)
end
- # Mount a Rack application at the specified path.
- # All the requests starting with the specified path, will be forwarded to
- # the given application.
+ # Generate an absolute 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.
#
- # 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 name [Symbol] the route name
#
- # @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
+ # @return [String]
#
- # @since 0.1.1
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
+ # recognize a route, because of the given arguments.
#
- # @example Basic usage
- # require 'hanami/router'
+ # @since 0.1.0
#
- # Hanami::Router.new do
- # mount Api::App.new, at: '/api'
- # end
+ # @see #path
#
- # # Requests:
- # #
- # # GET /api # => 200
- # # GET /api/articles # => 200
- # # POST /api/articles # => 200
- # # GET /api/unknown # => 404
+ # @example
+ # require "hanami/router"
#
- # @example Difference between #get and #mount
- # require 'hanami/router'
- #
- # Hanami::Router.new do
- # get '/rack1', to: RackOne.new
- # mount RackTwo.new, at: '/rack2'
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
+ # get "/login", to: ->(*) { ... }, as: :login
+ # get "/:name", to: ->(*) { ... }, as: :framework
# 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)
+ # 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(name, variables = {})
+ @url_helpers.url(name, variables)
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.
@@ -1065,190 +484,164 @@
#
# @see Hanami::Router#env_for
# @see Hanami::Routing::RecognizedRoute
#
# @example Successful Path Recognition
- # require 'hanami/router'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, as: :book
# end
#
- # route = router.recognize('/books/23')
+ # route = router.recognize("/books/23")
# route.verb # => "GET" (default)
# route.routable? # => true
# route.params # => {:id=>"23"}
#
# @example Successful Rack Env Recognition
- # require 'hanami/router'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, as: :book
# end
#
- # route = router.recognize(Rack::MockRequest.env_for('/books/23'))
+ # 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'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, 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'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, as: :book
# end
#
- # route = router.recognize('/books')
+ # route = router.recognize("/books")
# route.verb # => "GET" (default)
# route.routable? # => false
#
# @example Failing Recognition For Path With Wrong HTTP Verb
- # require 'hanami/router'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, as: :book
# end
#
- # route = router.recognize('/books/23', method: :post)
+ # 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'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, as: :book
# end
#
- # route = router.recognize(Rack::MockRequest.env_for('/books/23', method: :post))
+ # 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'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, 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'
+ # require "hanami/router"
#
# router = Hanami::Router.new do
- # get '/books/:id', to: 'books#show', as: :book
+ # get "/books/:id", to: ->(*) { ... }, 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) }
+ def recognize(env, params = {}, options = {})
+ require "hanami/router/recognized_route"
+ env = env_for(env, params, options)
+ endpoint, params = lookup(env)
- Routing::RecognizedRoute.new(route, env, @namespace)
+ RecognizedRoute.new(
+ endpoint, _params(env, params)
+ )
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")
+ # @since 2.0.0
+ # @api private
+ def fixed(env)
+ @fixed.dig(env["REQUEST_METHOD"], env["PATH_INFO"])
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)
+ # @since 2.0.0
+ # @api private
+ def variable(env)
+ @variable[env["REQUEST_METHOD"]]&.find(env["PATH_INFO"])
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)
+ # @since 2.0.0
+ # @api private
+ def globbed(env)
+ @globbed[env["REQUEST_METHOD"]]&.each do |path, to|
+ if (match = path.match(env["PATH_INFO"]))
+ return [to, match.named_captures]
+ end
+ end
+
+ nil
end
+ # @since 2.0.0
+ # @api private
+ def mounted(env)
+ @mounted.each do |prefix, app|
+ next unless (match = prefix.peek_match(env["PATH_INFO"]))
+
+ # TODO: ensure compatibility with existing env["SCRIPT_NAME"]
+ # TODO: cleanup this code
+ env["SCRIPT_NAME"] = env["SCRIPT_NAME"].to_s + prefix.to_s
+ env["PATH_INFO"] = env["PATH_INFO"].sub(prefix.to_s, "")
+ env["PATH_INFO"] = "/" if env["PATH_INFO"] == ""
+
+ return [app, match.named_captures]
+ end
+
+ nil
+ end
+
+ # @since 2.0.0
+ # @api private
+ def not_allowed(env)
+ (_not_allowed_fixed(env) || _not_allowed_variable(env)) and return NOT_ALLOWED
+ end
+
+ # @since 2.0.0
+ # @api private
+ def not_found
+ NOT_FOUND
+ 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
@@ -1260,106 +653,198 @@
# @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
+ def env_for(env, params = {}, options = {}) # rubocop:disable Metrics/MethodLength
+ require "rack/mock"
+
case env
- when String
- Rack::MockRequest.env_for(env, options)
- when Symbol
+ 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
- {}
+ url = path(env, params)
+ return env_for(url, params, options) # rubocop:disable Style/RedundantReturn
+ rescue Hanami::Router::InvalidRouteException
+ EMPTY_RACK_ENV.dup
end
else
env
end
end
private
- PATH_INFO = "PATH_INFO"
- SCRIPT_NAME = "SCRIPT_NAME"
- SERVER_NAME = "SERVER_NAME"
- REQUEST_METHOD = "REQUEST_METHOD"
+ # @since 2.0.0
+ # @api private
+ DEFAULT_BASE_URL = "http://localhost"
+ # @since 2.0.0
+ # @api private
+ DEFAULT_PREFIX = "/"
+
+ # @since 2.0.0
+ # @api private
+ DEFAULT_RESOLVER = ->(_, to) { to }
+
+ # @since 2.0.0
+ # @api private
+ DEFAULT_REDIRECT_CODE = 301
+
+ # @since 2.0.0
+ # @api private
+ NOT_FOUND = [404, { "Content-Length" => "9" }, ["Not Found"]].freeze
+
+ # @since 2.0.0
+ # @api private
+ NOT_ALLOWED = [405, { "Content-Length" => "11" }, ["Not Allowed"]].freeze
+
+ # @since 2.0.0
+ # @api private
PARAMS = "router.params"
- GET = "GET"
- HEAD = "HEAD"
- POST = "POST"
- PUT = "PUT"
- PATCH = "PATCH"
- DELETE = "DELETE"
- TRACE = "TRACE"
- OPTIONS = "OPTIONS"
- LINK = "LINK"
- UNLINK = "UNLINK"
+ # @since 2.0.0
+ # @api private
+ EMPTY_PARAMS = {}.freeze
- NOT_FOUND = ->(_) { [404, { "Content-Length" => "9" }, ["Not Found"]] }.freeze
- NOT_ALLOWED = ->(_) { [405, { "Content-Length" => "18" }, ["Method Not Allowed"]] }.freeze
- ROOT = "/"
+ # @since 2.0.0
+ # @api private
+ EMPTY_RACK_ENV = {}.freeze
- BODY = 2
+ # @since 2.0.0
+ # @api private
+ def lookup(env)
+ endpoint = fixed(env)
+ return [endpoint, EMPTY_PARAMS] if endpoint
- attr_reader :configuration
+ variable(env) || globbed(env) || mounted(env)
+ end
- # 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 add_route(http_method, path, to, as, constraints, &blk)
+ path = prefixed_path(path)
+ to = resolve_endpoint(path, to, blk)
- def match?(env)
- match_path?(env)
+ if globbed?(path)
+ add_globbed_route(http_method, path, to, constraints)
+ elsif variable?(path)
+ add_variable_route(http_method, path, to, constraints)
+ else
+ add_fixed_route(http_method, path, to)
end
- def match_path?(env)
- result = env[PATH_INFO].start_with?(@prefix)
- result &&= @host == env[SERVER_NAME] unless @host.nil?
+ add_named_route(path, as, constraints) if as
+ end
- result
- end
+ # @since 2.0.0
+ # @api private
+ def resolve_endpoint(path, to, blk)
+ (to || blk) or raise MissingEndpointError.new(path)
+ to = Block.new(@block_context, blk) if to.nil?
- def call(env)
- env[PARAMS] ||= {}
- env[PARAMS].merge!(Utils::Hash.deep_symbolize(@path.params(env[PATH_INFO]) || {}))
+ @resolver.call(path, to)
+ end
- env[SCRIPT_NAME] = @prefix
- env[PATH_INFO] = env[PATH_INFO].sub(@prefix, "")
- env[PATH_INFO] = "/" if env[PATH_INFO] == ""
+ # @since 2.0.0
+ # @api private
+ def add_globbed_route(http_method, path, to, constraints)
+ @globbed[http_method] ||= []
+ @globbed[http_method] << [Segment.fabricate(path, **constraints), to]
+ end
- @endpoint.call(env)
+ # @since 2.0.0
+ # @api private
+ def add_variable_route(http_method, path, to, constraints)
+ @variable[http_method] ||= Trie.new
+ @variable[http_method].add(path, to, constraints)
+ end
+
+ # @since 2.0.0
+ # @api private
+ def add_fixed_route(http_method, path, to)
+ @fixed[http_method] ||= {}
+ @fixed[http_method][path] = to
+ end
+
+ # @since 2.0.0
+ # @api private
+ def add_named_route(path, as, constraints)
+ @url_helpers.add(prefixed_name(as), Segment.fabricate(path, **constraints))
+ end
+
+ # @since 2.0.0
+ # @api private
+ def variable?(path)
+ /:/.match?(path)
+ end
+
+ # @since 2.0.0
+ # @api private
+ def globbed?(path)
+ /\*/.match?(path)
+ end
+
+ # @since 2.0.0
+ # @api private
+ def prefixed_path(path)
+ @path_prefix.join(path).to_s
+ end
+
+ # @since 2.0.0
+ # @api private
+ def prefixed_name(name)
+ @name_prefix.relative_join(name, "_").to_sym
+ end
+
+ # @since 2.0.0
+ # @api private
+ def _redirect(to, code)
+ body = Rack::Utils::HTTP_STATUS_CODES.fetch(code) do
+ raise UnknownHTTPStatusCodeError.new(code)
end
+
+ destination = prefixed_path(to)
+ Redirect.new(destination, ->(*) { [code, { "Location" => destination }, [body]] })
end
- def add_route(verb, path, to, as = nil, namespace = nil, config = nil, constraints = {}, &blk)
- to ||= blk
- config ||= configuration
+ # @since 2.0.0
+ # @api private
+ def _params(env, params)
+ params ||= {}
+ env[PARAMS] ||= {}
+ env[PARAMS].merge!(Rack::Utils.parse_nested_query(env["QUERY_STRING"]))
+ env[PARAMS].merge!(params)
+ env[PARAMS] = Params.deep_symbolize(env[PARAMS])
+ env
+ end
- 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)
+ # @since 2.0.0
+ # @api private
+ def _not_allowed_fixed(env)
+ found = false
- @routes.push(route)
- @named[as] = route unless as.nil?
+ @fixed.each_value do |routes|
+ break if found
+
+ found = routes.key?(env["PATH_INFO"])
+ end
+
+ found
end
- def verb_for(value)
- if value == GET
- [GET, HEAD]
- else
- [value]
+ # @since 2.0.0
+ # @api private
+ def _not_allowed_variable(env)
+ found = false
+
+ @variable.each_value do |routes|
+ break if found
+
+ found = routes.find(env["PATH_INFO"])
end
+
+ found
end
end
end