hanami-router 1.3.2 → 2.0.0.alpha5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +98 -444
  5. data/hanami-router.gemspec +23 -19
  6. data/lib/hanami/middleware/body_parser.rb +21 -15
  7. data/lib/hanami/middleware/body_parser/class_interface.rb +62 -56
  8. data/lib/hanami/middleware/body_parser/errors.rb +7 -4
  9. data/lib/hanami/middleware/body_parser/json_parser.rb +9 -7
  10. data/lib/hanami/middleware/body_parser/parser.rb +58 -0
  11. data/lib/hanami/middleware/error.rb +16 -0
  12. data/lib/hanami/router.rb +608 -955
  13. data/lib/hanami/router/block.rb +88 -0
  14. data/lib/hanami/router/error.rb +67 -0
  15. data/lib/hanami/router/inspector.rb +38 -0
  16. data/lib/hanami/router/node.rb +91 -0
  17. data/lib/hanami/router/params.rb +35 -0
  18. data/lib/hanami/router/prefix.rb +67 -0
  19. data/lib/hanami/router/recognized_route.rb +92 -0
  20. data/lib/hanami/router/redirect.rb +33 -0
  21. data/lib/hanami/router/route.rb +130 -0
  22. data/lib/hanami/router/segment.rb +19 -0
  23. data/lib/hanami/router/trie.rb +63 -0
  24. data/lib/hanami/router/url_helpers.rb +40 -0
  25. data/lib/hanami/router/version.rb +4 -1
  26. metadata +61 -39
  27. data/lib/hanami-router.rb +0 -1
  28. data/lib/hanami/routing/endpoint.rb +0 -195
  29. data/lib/hanami/routing/endpoint_resolver.rb +0 -238
  30. data/lib/hanami/routing/error.rb +0 -7
  31. data/lib/hanami/routing/force_ssl.rb +0 -212
  32. data/lib/hanami/routing/http_router.rb +0 -220
  33. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  34. data/lib/hanami/routing/namespace.rb +0 -98
  35. data/lib/hanami/routing/parsers.rb +0 -113
  36. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  37. data/lib/hanami/routing/parsing/parser.rb +0 -61
  38. data/lib/hanami/routing/recognized_route.rb +0 -219
  39. data/lib/hanami/routing/resource.rb +0 -119
  40. data/lib/hanami/routing/resource/action.rb +0 -402
  41. data/lib/hanami/routing/resource/nested.rb +0 -41
  42. data/lib/hanami/routing/resource/options.rb +0 -74
  43. data/lib/hanami/routing/resources.rb +0 -48
  44. data/lib/hanami/routing/resources/action.rb +0 -156
  45. data/lib/hanami/routing/route.rb +0 -71
  46. data/lib/hanami/routing/routes_inspector.rb +0 -221
@@ -1,30 +1,34 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'hanami/router/version'
5
+ require "hanami/router/version"
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = 'hanami-router'
8
+ spec.name = "hanami-router"
8
9
  spec.version = Hanami::Router::VERSION
9
- spec.authors = ['Luca Guidi']
10
- spec.email = ['me@lucaguidi.com']
11
- spec.description = %q{Rack compatible HTTP router for Ruby}
12
- spec.summary = %q{Rack compatible HTTP router for Ruby and Hanami}
13
- spec.homepage = 'http://hanamirb.org'
14
- spec.license = 'MIT'
10
+ spec.authors = ["Luca Guidi"]
11
+ spec.email = ["me@lucaguidi.com"]
12
+ spec.description = "Rack compatible HTTP router for Ruby"
13
+ spec.summary = "Rack compatible HTTP router for Ruby and Hanami"
14
+ spec.homepage = "http://hanamirb.org"
15
+ spec.license = "MIT"
15
16
 
16
17
  spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-router.gemspec`.split($/)
17
18
  spec.executables = []
18
19
  spec.test_files = spec.files.grep(%r{^(test)/})
19
- spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
20
+ spec.require_paths = ["lib"]
21
+ spec.required_ruby_version = ">= 2.6.0"
22
+
23
+ spec.add_dependency "rack", "~> 2.0"
24
+ spec.add_dependency "mustermann", "~> 1.0"
25
+ spec.add_dependency "mustermann-contrib", "~> 1.0"
21
26
 
22
- spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'http_router', '0.11.2'
24
- spec.add_dependency 'hanami-utils', '~> 1.3'
27
+ spec.add_development_dependency "bundler", ">= 1.6", "< 3"
28
+ spec.add_development_dependency "rake", "~> 13"
29
+ spec.add_development_dependency "rack-test", "~> 1.0"
30
+ spec.add_development_dependency "rspec", "~> 3.8"
25
31
 
26
- spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
27
- spec.add_development_dependency 'rake', '~> 13'
28
- spec.add_development_dependency 'rack-test', '~> 1.0'
29
- spec.add_development_dependency 'rspec', '~> 3.7'
32
+ spec.add_development_dependency "rubocop", "0.91"
33
+ spec.add_development_dependency "rubocop-performance", "1.8.1"
30
34
  end
@@ -1,14 +1,19 @@
1
- require 'hanami/utils/hash'
2
- require_relative 'body_parser/class_interface'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/router/params"
4
+ require "hanami/middleware/error"
3
5
 
4
6
  module Hanami
5
7
  module Middleware
6
8
  # @since 1.3.0
7
9
  # @api private
8
10
  class BodyParser
11
+ require_relative "body_parser/class_interface"
12
+ require_relative "body_parser/parser"
13
+
9
14
  # @since 1.3.0
10
15
  # @api private
11
- CONTENT_TYPE = 'CONTENT_TYPE'.freeze
16
+ CONTENT_TYPE = "CONTENT_TYPE"
12
17
 
13
18
  # @since 1.3.0
14
19
  # @api private
@@ -16,17 +21,17 @@ module Hanami
16
21
 
17
22
  # @since 1.3.0
18
23
  # @api private
19
- RACK_INPUT = 'rack.input'.freeze
24
+ RACK_INPUT = "rack.input"
20
25
 
21
26
  # @since 1.3.0
22
27
  # @api private
23
- ROUTER_PARAMS = 'router.params'.freeze
28
+ ROUTER_PARAMS = "router.params"
24
29
 
25
30
  # @api private
26
- ROUTER_PARSED_BODY = 'router.parsed_body'.freeze
31
+ ROUTER_PARSED_BODY = "router.parsed_body"
27
32
 
28
33
  # @api private
29
- FALLBACK_KEY = '_'.freeze
34
+ FALLBACK_KEY = "_"
30
35
 
31
36
  extend ClassInterface
32
37
 
@@ -55,21 +60,21 @@ module Hanami
55
60
  parser_names = Array(parser_names)
56
61
  return {} if parser_names.empty?
57
62
 
58
- parser_names.each_with_object({}) { |name, parsers|
63
+ parser_names.each_with_object({}) do |name, parsers|
59
64
  parser = self.class.for(name)
60
65
 
61
66
  parser.mime_types.each do |mime|
62
67
  parsers[mime] = parser
63
68
  end
64
- }
69
+ end
65
70
  end
66
71
 
67
72
  # @api private
68
73
  def _symbolize(body)
69
- if body.is_a?(Hash)
70
- Utils::Hash.deep_symbolize(body)
74
+ if body.is_a?(::Hash)
75
+ Router::Params.deep_symbolize(body)
71
76
  else
72
- { FALLBACK_KEY => body }
77
+ {FALLBACK_KEY => body}
73
78
  end
74
79
  end
75
80
 
@@ -82,9 +87,10 @@ module Hanami
82
87
 
83
88
  # @api private
84
89
  def media_type(env)
85
- if ct = content_type(env)
86
- ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
87
- end
90
+ ct = content_type(env)
91
+ return unless ct
92
+
93
+ ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
88
94
  end
89
95
 
90
96
  # @api private
@@ -1,56 +1,62 @@
1
- require 'hanami/utils/class'
2
- require 'hanami/utils/string'
3
- require_relative 'errors'
4
-
5
- module Hanami
6
- module Middleware
7
- class BodyParser
8
- # @api private
9
- # @since 1.3.0
10
- module ClassInterface
11
- # @api private
12
- # @since 1.3.0
13
- def for(parser)
14
- parser =
15
- case parser
16
- when String, Symbol
17
- require_parser(parser)
18
- when Class
19
- parser.new
20
- else
21
- parser
22
- end
23
-
24
- ensure_parser parser
25
-
26
- parser
27
- end
28
-
29
- private
30
-
31
- # @api private
32
- # @since 1.3.0
33
- PARSER_METHODS = %i[mime_types parse].freeze
34
-
35
- # @api private
36
- # @since 1.3.0
37
- def ensure_parser(parser)
38
- unless PARSER_METHODS.all? { |method| parser.respond_to?(method) }
39
- raise InvalidParserError.new(parser)
40
- end
41
- end
42
-
43
- # @api private
44
- # @since 1.3.0
45
- def require_parser(parser)
46
- require "hanami/middleware/body_parser/#{parser}_parser"
47
-
48
- parser = Utils::String.classify(parser)
49
- Utils::Class.load!("Hanami::Middleware::BodyParser::#{parser}Parser").new
50
- rescue LoadError, NameError
51
- raise UnknownParserError.new(parser)
52
- end
53
- end
54
- end
55
- end
56
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Hanami
6
+ module Middleware
7
+ # HTTP request body parser
8
+ class BodyParser
9
+ # @api private
10
+ # @since 1.3.0
11
+ module ClassInterface
12
+ # @api private
13
+ # @since 1.3.0
14
+ def for(parser)
15
+ parser =
16
+ case parser
17
+ when String, Symbol
18
+ require_parser(parser)
19
+ when Class
20
+ parser.new
21
+ else
22
+ parser
23
+ end
24
+
25
+ ensure_parser parser
26
+
27
+ parser
28
+ end
29
+
30
+ private
31
+
32
+ # @api private
33
+ # @since 1.3.0
34
+ PARSER_METHODS = %i[mime_types parse].freeze
35
+
36
+ # @api private
37
+ # @since 1.3.0
38
+ def ensure_parser(parser)
39
+ raise InvalidParserError.new(parser) unless PARSER_METHODS.all? { |method| parser.respond_to?(method) }
40
+ end
41
+
42
+ # @api private
43
+ # @since 1.3.0
44
+ def require_parser(parser)
45
+ require "hanami/middleware/body_parser/#{parser}_parser"
46
+
47
+ load_parser!("#{classify(parser)}Parser").new
48
+ rescue LoadError, NameError
49
+ raise UnknownParserError.new(parser)
50
+ end
51
+
52
+ def classify(parser)
53
+ parser.to_s.split(/_/).map(&:capitalize).join
54
+ end
55
+
56
+ def load_parser!(class_name)
57
+ Hanami::Middleware::BodyParser.const_get(class_name, false)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,4 +1,4 @@
1
- require 'hanami/routing/error'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Hanami
4
4
  module Middleware
@@ -9,14 +9,17 @@ module Hanami
9
9
  # This is raised when parser fails to parse the body
10
10
  #
11
11
  # @since 1.3.0
12
- class BodyParsingError < Hanami::Routing::Parsing::BodyParsingError
12
+ class BodyParsingError < Hanami::Middleware::Error
13
13
  end
14
14
 
15
15
  # @since 1.3.0
16
- class UnknownParserError < Hanami::Routing::Parsing::UnknownParserError
16
+ class UnknownParserError < Hanami::Middleware::Error
17
+ def initialize(name)
18
+ super("Unknown body parser: `#{name.inspect}'")
19
+ end
17
20
  end
18
21
 
19
- class InvalidParserError < Hanami::Routing::Error
22
+ class InvalidParserError < Hanami::Middleware::Error
20
23
  end
21
24
  end
22
25
  end
@@ -1,16 +1,18 @@
1
- require 'hanami/utils/json'
2
- require_relative 'errors'
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "parser"
3
5
 
4
6
  module Hanami
5
7
  module Middleware
6
8
  class BodyParser
7
9
  # @since 1.3.0
8
10
  # @api private
9
- class JsonParser
11
+ class JsonParser < Parser
10
12
  # @since 1.3.0
11
13
  # @api private
12
14
  def mime_types
13
- ['application/json', 'application/vnd.api+json']
15
+ ["application/json", "application/vnd.api+json"]
14
16
  end
15
17
 
16
18
  # Parse a json string
@@ -24,9 +26,9 @@ module Hanami
24
26
  # @since 1.3.0
25
27
  # @api private
26
28
  def parse(body)
27
- Hanami::Utils::Json.parse(body)
28
- rescue Hanami::Utils::Json::ParserError => e
29
- raise BodyParsingError.new(e.message)
29
+ JSON.parse(body)
30
+ rescue StandardError => exception
31
+ raise BodyParsingError.new(exception.message)
30
32
  end
31
33
  end
32
34
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Middleware
5
+ class BodyParser
6
+ # Body parser abstract class
7
+ #
8
+ # @since 2.0.0
9
+ class Parser
10
+ # Declare supported MIME types
11
+ #
12
+ # @return [Array<String>] supported MIME types
13
+ #
14
+ # @abstract
15
+ # @since 2.0.0
16
+ #
17
+ # @example
18
+ # require "hanami/middleware/body_parser"
19
+ #
20
+ # class XMLParser < Hanami::Middleware::BodyParser::Parser
21
+ # def mime_types
22
+ # ["application/xml", "text/xml"]
23
+ # end
24
+ # end
25
+ def mime_types
26
+ raise NotImplementedError
27
+ end
28
+
29
+ # Parse raw HTTP request body
30
+ #
31
+ # @param body [String] HTTP request body
32
+ #
33
+ # @return [Hash] the result of the parsing
34
+ #
35
+ # @raise [Hanami::Middleware::BodyParser::BodyParsingError] the error
36
+ # that must be raised if the parsing cannot be accomplished
37
+ #
38
+ # @abstract
39
+ # @since 2.0.0
40
+ #
41
+ # @example
42
+ # require "hanami/middleware/body_parser"
43
+ #
44
+ # class XMLParser < Hanami::Middleware::BodyParser::Parser
45
+ # def parse(body)
46
+ # # XML parsing
47
+ # # ...
48
+ # rescue => exception
49
+ # raise Hanami::Middleware::BodyParser::BodyParsingError.new(exception.message)
50
+ # end
51
+ # end
52
+ def parse(body)
53
+ raise NotImplementedError
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ # Hanami Rack middleware
5
+ #
6
+ # @since 1.3.0
7
+ module Middleware
8
+ unless defined?(Error)
9
+ # Base error for Rack middleware
10
+ #
11
+ # @since 2.0.0
12
+ class Error < ::StandardError
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/hanami/router.rb CHANGED
@@ -1,131 +1,37 @@
1
- require 'rack/request'
2
- require 'hanami/routing/http_router'
3
- require 'hanami/routing/namespace'
4
- require 'hanami/routing/resource'
5
- require 'hanami/routing/resources'
6
- require 'hanami/routing/error'
7
-
8
- # Hanami
9
- #
10
- # @since 0.1.0
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+
11
5
  module Hanami
12
6
  # Rack compatible, lightweight and fast HTTP Router.
13
7
  #
14
8
  # @since 0.1.0
15
- #
16
- # @example It offers an intuitive DSL, that supports most of the HTTP verbs:
17
- # require 'hanami/router'
18
- #
19
- # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
20
- # router = Hanami::Router.new do
21
- # get '/', to: endpoint # => get and head requests
22
- # post '/', to: endpoint
23
- # put '/', to: endpoint
24
- # patch '/', to: endpoint
25
- # delete '/', to: endpoint
26
- # options '/', to: endpoint
27
- # trace '/', to: endpoint
28
- # end
29
- #
30
- #
31
- #
32
- # @example Specify an endpoint with `:to` (Rack compatible object)
33
- # require 'hanami/router'
34
- #
35
- # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
36
- # router = Hanami::Router.new do
37
- # get '/', to: endpoint
38
- # end
39
- #
40
- # # :to is mandatory for the default resolver (`Hanami::Routing::EndpointResolver.new`),
41
- # # This behavior can be changed by passing a custom resolver to `Hanami::Router#initialize`
42
- #
43
- #
44
- #
45
- # @example Specify an endpoint with `:to` (controller and action string)
46
- # require 'hanami/router'
47
- #
48
- # router = Hanami::Router.new do
49
- # get '/', to: 'articles#show' # => Articles::Show
50
- # end
51
- #
52
- # # This is a builtin feature for a Hanami::Controller convention.
53
- #
54
- #
55
- #
56
- # @example Specify a named route with `:as`
57
- # require 'hanami/router'
58
- #
59
- # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
60
- # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org') do
61
- # get '/', to: endpoint, as: :root
62
- # end
63
- #
64
- # router.path(:root) # => '/'
65
- # router.url(:root) # => 'https://hanamirb.org/'
66
- #
67
- # # This isn't mandatory for the default route class (`Hanami::Routing::Route`),
68
- # # This behavior can be changed by passing a custom route to `Hanami::Router#initialize`
69
- #
70
- # @example Mount an application
71
- # require 'hanami/router'
72
- #
73
- # router = Hanami::Router.new do
74
- # mount Api::App, at: '/api'
75
- # end
76
- #
77
- # # All the requests starting with "/api" will be forwarded to Api::App
78
9
  class Router
79
- # This error is raised when <tt>#call</tt> is invoked on a non-routable
80
- # recognized route.
81
- #
82
- # @since 0.5.0
10
+ require "hanami/router/version"
11
+ require "hanami/router/error"
12
+ require "hanami/router/segment"
13
+ require "hanami/router/redirect"
14
+ require "hanami/router/prefix"
15
+ require "hanami/router/params"
16
+ require "hanami/router/trie"
17
+ require "hanami/router/block"
18
+ require "hanami/router/route"
19
+ require "hanami/router/url_helpers"
20
+
21
+ # URL helpers for other Hanami integrations
83
22
  #
84
- # @see Hanami::Router#recognize
85
- # @see Hanami::Routing::RecognizedRoute
86
- # @see Hanami::Routing::RecognizedRoute#call
87
- # @see Hanami::Routing::RecognizedRoute#routable?
88
- class NotRoutableEndpointError < Hanami::Routing::Error
89
- # @since 0.5.0
90
- # @api private
91
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
92
-
93
- # @since 0.5.0
94
- # @api private
95
- PATH_INFO = 'PATH_INFO'.freeze
96
-
97
- # @since 0.5.0
98
- def initialize(env)
99
- super %(Cannot find routable endpoint for #{ env[REQUEST_METHOD] } "#{ env[PATH_INFO] }")
100
- end
101
- end
23
+ # @api private
24
+ # @since 2.0.0
25
+ attr_reader :url_helpers
102
26
 
103
- # Defines root path
27
+ # Routes for inspection
104
28
  #
105
- # @since 0.7.0
106
29
  # @api private
107
- #
108
- # @see Hanami::Router#root
109
- ROOT_PATH = '/'.freeze
30
+ # @since 2.0.0
31
+ attr_reader :routes
110
32
 
111
33
  # Returns the given block as it is.
112
34
  #
113
- # When Hanami::Router is used as a standalone gem and the routes are defined
114
- # into a configuration file, some systems could raise an exception.
115
- #
116
- # Imagine the following file into a Ruby on Rails application:
117
- #
118
- # get '/', to: 'api#index'
119
- #
120
- # Because Ruby on Rails in production mode use to eager load code and the
121
- # routes file uses top level method calls, it crashes the application.
122
- #
123
- # If we wrap these routes with <tt>Hanami::Router.define</tt>, the block
124
- # doesn't get yielded but just returned to the caller as it is.
125
- #
126
- # Usually the receiver of this block is <tt>Hanami::Router#initialize</tt>,
127
- # which finally evaluates the block.
128
- #
129
35
  # @param blk [Proc] a set of route definitions
130
36
  #
131
37
  # @return [Proc] the given block
@@ -135,888 +41,447 @@ module Hanami
135
41
  # @example
136
42
  # # apps/web/config/routes.rb
137
43
  # Hanami::Router.define do
138
- # get '/', to: 'home#index'
44
+ # get "/", to: ->(*) { ... }
139
45
  # end
140
46
  def self.define(&blk)
141
47
  blk
142
48
  end
143
49
 
144
- # Initialize the router.
145
- #
146
- # @param options [Hash] the options to initialize the router
147
- #
148
- # @option options [String] :scheme The HTTP scheme (defaults to `"http"`)
149
- # @option options [String] :host The URL host (defaults to `"localhost"`)
150
- # @option options [String] :port The URL port (defaults to `"80"`)
151
- # @option options [Object, #resolve, #find, #action_separator] :resolver
152
- # the route resolver (defaults to `Hanami::Routing::EndpointResolver.new`)
153
- # @option options [Object, #generate] :route the route class
154
- # (defaults to `Hanami::Routing::Route`)
155
- # @option options [String] :action_separator the separator between controller
156
- # and action name (eg. 'dashboard#show', where '#' is the :action_separator)
157
- # @option options [Array<Symbol,String,Object #mime_types, parse>] :parsers
158
- # the body parsers for mime types
159
- #
160
- # @param blk [Proc] the optional block to define the routes
50
+ # Initialize the router
161
51
  #
162
- # @return [Hanami::Router] self
52
+ # @param base_url [String] the base URL where the HTTP application is
53
+ # deployed
54
+ # @param prefix [String] the relative URL prefix where the HTTP application
55
+ # is deployed
56
+ # @param resolver [#call(path, to)] a resolver for route entpoints
57
+ # @param block_context [Hanami::Router::Block::Context)
58
+ # @param not_found [#call(env)] default handler when route is not matched
59
+ # @param blk [Proc] the route definitions
163
60
  #
164
61
  # @since 0.1.0
165
62
  #
166
- # @example Basic example
167
- # require 'hanami/router'
168
- #
169
- # endpoint = ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
170
- #
171
- # router = Hanami::Router.new
172
- # router.get '/', to: endpoint
173
- #
174
- # # or
175
- #
176
- # router = Hanami::Router.new do
177
- # get '/', to: endpoint
178
- # end
179
- #
180
- # @example Body parsers
181
- # require 'json'
182
- # require 'hanami/router'
183
- #
184
- # # It parses JSON body and makes the attributes available to the params
185
- #
186
- # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
187
- #
188
- # router = Hanami::Router.new(parsers: [:json]) do
189
- # patch '/books/:id', to: endpoint
190
- # end
191
- #
192
- # # From the shell
193
- #
194
- # curl http://localhost:2300/books/1 \
195
- # -H "Content-Type: application/json" \
196
- # -H "Accept: application/json" \
197
- # -d '{"published":"true"}' \
198
- # -X PATCH
199
- #
200
- # # It returns
201
- #
202
- # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
203
- #
204
- # @example Custom body parser
205
- # require 'hanami/router'
206
- #
207
- # class XmlParser
208
- # def mime_types
209
- # ['application/xml', 'text/xml']
210
- # end
211
- #
212
- # # Parse body and return a Hash
213
- # def parse(body)
214
- # # ...
215
- # end
216
- # end
217
- #
218
- # # It parses XML body and makes the attributes available to the params
219
- #
220
- # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
221
- #
222
- # router = Hanami::Router.new(parsers: [XmlParser.new]) do
223
- # patch '/authors/:id', to: endpoint
224
- # end
63
+ # @return [Hanami::Router]
225
64
  #
226
- # # From the shell
65
+ # @example Base usage
66
+ # require "hanami/router"
227
67
  #
228
- # curl http://localhost:2300/authors/1 \
229
- # -H "Content-Type: application/xml" \
230
- # -H "Accept: application/xml" \
231
- # -d '<name>LG</name>' \
232
- # -X PATCH
233
- #
234
- # # It returns
235
- #
236
- # [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]
237
- def initialize(options = {}, &blk)
238
- @router = Routing::HttpRouter.new(options)
239
- define(&blk)
68
+ # Hanami::Router.new do
69
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
70
+ # end
71
+ def initialize(base_url: DEFAULT_BASE_URL, prefix: DEFAULT_PREFIX, resolver: DEFAULT_RESOLVER, not_found: NOT_FOUND, block_context: nil, inspector: nil, &blk) # rubocop:disable Layout/LineLength
72
+ # TODO: verify if Prefix can handle both name and path prefix
73
+ @path_prefix = Prefix.new(prefix)
74
+ @name_prefix = Prefix.new("")
75
+ @url_helpers = UrlHelpers.new(base_url)
76
+ @base_url = base_url
77
+ @resolver = resolver
78
+ @not_found = not_found
79
+ @block_context = block_context
80
+ @fixed = {}
81
+ @variable = {}
82
+ @globbed = {}
83
+ @mounted = {}
84
+ @blk = blk
85
+ @inspector = inspector
86
+ instance_eval(&blk) if blk
240
87
  end
241
88
 
242
- # Returns self
89
+ # Resolve the given Rack env to a registered endpoint and invokes it.
243
90
  #
244
- # This is a duck-typing trick for compatibility with `Hanami::Application`.
245
- # It's used by `Hanami::Routing::RoutesInspector` to inspect both apps and
246
- # routers.
91
+ # @param env [Hash] a Rack env
247
92
  #
248
- # @return [self]
93
+ # @return [Array] a finalized Rack env response
249
94
  #
250
- # @since 0.2.0
251
- # @api private
252
- def routes
253
- self
95
+ # @since 0.1.0
96
+ def call(env)
97
+ endpoint, params = lookup(env)
98
+
99
+ unless endpoint
100
+ return not_allowed(env) ||
101
+ not_found(env)
102
+ end
103
+
104
+ endpoint.call(
105
+ _params(env, params)
106
+ ).to_a
254
107
  end
255
108
 
256
- # To support defining routes in the `define` wrapper.
109
+ # Defines a named root route (a GET route for "/")
257
110
  #
258
- # @param blk [Proc] the block to define the routes
259
- #
260
- # @return [Hanami::Routing::Route]
111
+ # @param to [#call] the Rack endpoint
112
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
261
113
  #
262
- # @since 0.2.0
114
+ # @since 0.7.0
263
115
  #
264
- # @example In Hanami framework
265
- # class Application < Hanami::Application
266
- # configure do
267
- # routes 'config/routes'
268
- # end
269
- # end
116
+ # @see #get
117
+ # @see #path
118
+ # @see #url
270
119
  #
271
- # # In `config/routes`
120
+ # @example Proc endpoint
121
+ # require "hanami/router"
272
122
  #
273
- # define do
274
- # get # ...
123
+ # router = Hanami::Router.new do
124
+ # root to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
275
125
  # end
276
- def define(&blk)
277
- instance_eval(&blk) if block_given?
278
- end
279
-
280
- # Check if there are defined routes
281
126
  #
282
- # @return [TrueClass,FalseClass] the result of the check
127
+ # @example Block endpoint
128
+ # require "hanami/router"
283
129
  #
284
- # @since 0.2.0
285
- # @api private
130
+ # router = Hanami::Router.new do
131
+ # root do
132
+ # "Hello from Hanami!"
133
+ # end
134
+ # end
286
135
  #
287
- # @example
136
+ # @example URL helpers
137
+ # require "hanami/router"
288
138
  #
289
- # router = Hanami::Router.new
290
- # router.defined? # => false
139
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
140
+ # root do
141
+ # "Hello from Hanami!"
142
+ # end
143
+ # end
291
144
  #
292
- # router = Hanami::Router.new { get '/', to: ->(env) { } }
293
- # router.defined? # => true
294
- def defined?
295
- @router.routes.any?
145
+ # router.path(:root) # => "/"
146
+ # router.url(:root) # => "https://hanamirb.org"
147
+ def root(to: nil, &blk)
148
+ get("/", to: to, as: :root, &blk)
296
149
  end
297
150
 
298
- # Defines a route that accepts a GET request for the given path.
151
+ # Defines a route that accepts GET requests for the given path.
152
+ # It also defines a route to accept HEAD requests.
299
153
  #
300
154
  # @param path [String] the relative URL to be matched
301
- #
302
- # @param options [Hash] the options to customize the route
303
- # @option options [String,Proc,Class,Object#call] :to the endpoint
304
- #
155
+ # @param to [#call] the Rack endpoint
156
+ # @param as [Symbol] a unique name for the route
157
+ # @param constraints [Hash] a set of constraints for path variables
305
158
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
306
159
  #
307
- # @return [Hanami::Routing::Route] this may vary according to the :route
308
- # option passed to the constructor
309
- #
310
160
  # @since 0.1.0
311
161
  #
312
- # @example Fixed matching string
313
- # require 'hanami/router'
314
- #
315
- # router = Hanami::Router.new
316
- # router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
317
- #
318
- # @example String matching with variables
319
- # require 'hanami/router'
320
- #
321
- # router = Hanami::Router.new
322
- # router.get '/flowers/:id',
323
- # to: ->(env) {
324
- # [
325
- # 200,
326
- # {},
327
- # ["Hello from Flower no. #{ env['router.params'][:id] }!"]
328
- # ]
329
- # }
330
- #
331
- # @example Variables Constraints
332
- # require 'hanami/router'
333
- #
334
- # router = Hanami::Router.new
335
- # router.get '/flowers/:id',
336
- # id: /\d+/,
337
- # to: ->(env) { [200, {}, [":id must be a number!"]] }
338
- #
339
- # @example String matching with globbling
340
- # require 'hanami/router'
341
- #
342
- # router = Hanami::Router.new
343
- # router.get '/*',
344
- # to: ->(env) {
345
- # [
346
- # 200,
347
- # {},
348
- # ["This is catch all: #{ env['router.params'].inspect }!"]
349
- # ]
350
- # }
351
- #
352
- # @example String matching with optional tokens
353
- # require 'hanami/router'
162
+ # @see #initialize
163
+ # @see #path
164
+ # @see #url
354
165
  #
355
- # router = Hanami::Router.new
356
- # router.get '/hanami(.:format)',
357
- # to: ->(env) {
358
- # [200, {}, ["You've requested #{ env['router.params'][:format] }!"]]
359
- # }
166
+ # @example Proc endpoint
167
+ # require "hanami/router"
360
168
  #
361
- # @example Named routes
362
- # require 'hanami/router'
363
- #
364
- # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
365
- # router.get '/hanami',
366
- # to: ->(env) { [200, {}, ['Hello from Hanami!']] },
367
- # as: :hanami
368
- #
369
- # router.path(:hanami) # => "/hanami"
370
- # router.url(:hanami) # => "https://hanamirb.org/hanami"
371
- #
372
- # @example Duck typed endpoints (Rack compatible objects)
373
- # require 'hanami/router'
374
- #
375
- # router = Hanami::Router.new
376
- #
377
- # router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
378
- # router.get '/middleware', to: Middleware
379
- # router.get '/rack-app', to: RackApp.new
380
- # router.get '/method', to: ActionControllerSubclass.action(:new)
381
- #
382
- # # Everything that responds to #call is invoked as it is
383
- #
384
- # @example Duck typed endpoints (strings)
385
- # require 'hanami/router'
386
- #
387
- # class RackApp
388
- # def call(env)
389
- # # ...
390
- # end
169
+ # Hanami::Router.new do
170
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
391
171
  # end
392
172
  #
393
- # router = Hanami::Router.new
394
- # router.get '/hanami', to: 'rack_app' # it will map to RackApp.new
395
- #
396
- # @example Duck typed endpoints (string: controller + action)
397
- # require 'hanami/router'
173
+ # @example Block endpoint
174
+ # require "hanami/router"
398
175
  #
399
- # module Flowers
400
- # class Index
401
- # def call(env)
402
- # # ...
403
- # end
176
+ # Hanami::Router.new do
177
+ # get "/" do
178
+ # "OK"
404
179
  # end
405
180
  # end
406
181
  #
407
- # router = Hanami::Router.new
408
- # router.get '/flowers', to: 'flowers#index'
409
- #
410
- # # It will map to Flowers::Index.new, which is the
411
- # # Hanami::Controller convention.
412
- def get(path, options = {}, &blk)
413
- @router.get(path, options, &blk)
414
- end
415
-
416
- # Defines a route that accepts a POST request for the given path.
417
- #
418
- # @param path [String] the relative URL to be matched
419
- #
420
- # @param options [Hash] the options to customize the route
421
- # @option options [String,Proc,Class,Object#call] :to the endpoint
182
+ # @example Named route
183
+ # require "hanami/router"
422
184
  #
423
- # @param blk [Proc] the anonymous proc to be used as endpoint for the route
185
+ # router = Hanami::Router.new do
186
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }, as: :welcome
187
+ # end
424
188
  #
425
- # @return [Hanami::Routing::Route] this may vary according to the :route
426
- # option passed to the constructor
189
+ # router.path(:welcome) # => "/"
190
+ # router.url(:welcome) # => "http://localhost/"
427
191
  #
428
- # @see Hanami::Router#get
192
+ # @example Constraints
193
+ # require "hanami/router"
429
194
  #
430
- # @since 0.1.0
431
- def post(path, options = {}, &blk)
432
- @router.post(path, options, &blk)
195
+ # Hanami::Router.new do
196
+ # get "/users/:id", to: ->(*) { [200, {}, ["OK"]] }, id: /\d+/
197
+ # end
198
+ def get(path, to: nil, as: nil, **constraints, &blk)
199
+ add_route("GET", path, to, as, constraints, &blk)
200
+ add_route("HEAD", path, to, as, constraints, &blk)
433
201
  end
434
202
 
435
- # Defines a route that accepts a PUT request for the given path.
203
+ # Defines a route that accepts POST requests for the given path.
436
204
  #
437
205
  # @param path [String] the relative URL to be matched
438
- #
439
- # @param options [Hash] the options to customize the route
440
- # @option options [String,Proc,Class,Object#call] :to the endpoint
441
- #
206
+ # @param to [#call] the Rack endpoint
207
+ # @param as [Symbol] a unique name for the route
208
+ # @param constraints [Hash] a set of constraints for path variables
442
209
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
443
210
  #
444
- # @return [Hanami::Routing::Route] this may vary according to the :route
445
- # option passed to the constructor
446
- #
447
- # @see Hanami::Router#get
448
- #
449
211
  # @since 0.1.0
450
- def put(path, options = {}, &blk)
451
- @router.put(path, options, &blk)
212
+ #
213
+ # @see #get
214
+ # @see #initialize
215
+ # @see #path
216
+ # @see #url
217
+ def post(path, to: nil, as: nil, **constraints, &blk)
218
+ add_route("POST", path, to, as, constraints, &blk)
452
219
  end
453
220
 
454
- # Defines a route that accepts a PATCH request for the given path.
221
+ # Defines a route that accepts PATCH requests for the given path.
455
222
  #
456
223
  # @param path [String] the relative URL to be matched
457
- #
458
- # @param options [Hash] the options to customize the route
459
- # @option options [String,Proc,Class,Object#call] :to the endpoint
460
- #
224
+ # @param to [#call] the Rack endpoint
225
+ # @param as [Symbol] a unique name for the route
226
+ # @param constraints [Hash] a set of constraints for path variables
461
227
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
462
228
  #
463
- # @return [Hanami::Routing::Route] this may vary according to the :route
464
- # option passed to the constructor
465
- #
466
- # @see Hanami::Router#get
467
- #
468
229
  # @since 0.1.0
469
- def patch(path, options = {}, &blk)
470
- @router.patch(path, options, &blk)
230
+ #
231
+ # @see #get
232
+ # @see #initialize
233
+ # @see #path
234
+ # @see #url
235
+ def patch(path, to: nil, as: nil, **constraints, &blk)
236
+ add_route("PATCH", path, to, as, constraints, &blk)
471
237
  end
472
238
 
473
- # Defines a route that accepts a DELETE request for the given path.
239
+ # Defines a route that accepts PUT requests for the given path.
474
240
  #
475
241
  # @param path [String] the relative URL to be matched
476
- #
477
- # @param options [Hash] the options to customize the route
478
- # @option options [String,Proc,Class,Object#call] :to the endpoint
479
- #
242
+ # @param to [#call] the Rack endpoint
243
+ # @param as [Symbol] a unique name for the route
244
+ # @param constraints [Hash] a set of constraints for path variables
480
245
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
481
246
  #
482
- # @return [Hanami::Routing::Route] this may vary according to the :route
483
- # option passed to the constructor
484
- #
485
- # @see Hanami::Router#get
486
- #
487
247
  # @since 0.1.0
488
- def delete(path, options = {}, &blk)
489
- @router.delete(path, options, &blk)
248
+ #
249
+ # @see #get
250
+ # @see #initialize
251
+ # @see #path
252
+ # @see #url
253
+ def put(path, to: nil, as: nil, **constraints, &blk)
254
+ add_route("PUT", path, to, as, constraints, &blk)
490
255
  end
491
256
 
492
- # Defines a route that accepts a TRACE request for the given path.
257
+ # Defines a route that accepts DELETE requests for the given path.
493
258
  #
494
259
  # @param path [String] the relative URL to be matched
495
- #
496
- # @param options [Hash] the options to customize the route
497
- # @option options [String,Proc,Class,Object#call] :to the endpoint
498
- #
260
+ # @param to [#call] the Rack endpoint
261
+ # @param as [Symbol] a unique name for the route
262
+ # @param constraints [Hash] a set of constraints for path variables
499
263
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
500
264
  #
501
- # @return [Hanami::Routing::Route] this may vary according to the :route
502
- # option passed to the constructor
503
- #
504
- # @see Hanami::Router#get
505
- #
506
265
  # @since 0.1.0
507
- def trace(path, options = {}, &blk)
508
- @router.trace(path, options, &blk)
266
+ #
267
+ # @see #get
268
+ # @see #initialize
269
+ # @see #path
270
+ # @see #url
271
+ def delete(path, to: nil, as: nil, **constraints, &blk)
272
+ add_route("DELETE", path, to, as, constraints, &blk)
509
273
  end
510
274
 
511
- # Defines a route that accepts a LINK request for the given path.
275
+ # Defines a route that accepts TRACE requests for the given path.
512
276
  #
513
277
  # @param path [String] the relative URL to be matched
514
- #
515
- # @param options [Hash] the options to customize the route
516
- # @option options [String,Proc,Class,Object#call] :to the endpoint
517
- #
278
+ # @param to [#call] the Rack endpoint
279
+ # @param as [Symbol] a unique name for the route
280
+ # @param constraints [Hash] a set of constraints for path variables
518
281
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
519
282
  #
520
- # @return [Hanami::Routing::Route] this may vary according to the :route
521
- # option passed to the constructor
522
- #
523
- # @see Hanami::Router#get
283
+ # @since 0.1.0
524
284
  #
525
- # @since 0.8.0
526
- def link(path, options = {}, &blk)
527
- @router.link(path, options, &blk)
285
+ # @see #get
286
+ # @see #initialize
287
+ # @see #path
288
+ # @see #url
289
+ def trace(path, to: nil, as: nil, **constraints, &blk)
290
+ add_route("TRACE", path, to, as, constraints, &blk)
528
291
  end
529
292
 
530
- # Defines a route that accepts an UNLINK request for the given path.
293
+ # Defines a route that accepts OPTIONS requests for the given path.
531
294
  #
532
295
  # @param path [String] the relative URL to be matched
533
- #
534
- # @param options [Hash] the options to customize the route
535
- # @option options [String,Proc,Class,Object#call] :to the endpoint
536
- #
296
+ # @param to [#call] the Rack endpoint
297
+ # @param as [Symbol] a unique name for the route
298
+ # @param constraints [Hash] a set of constraints for path variables
537
299
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
538
300
  #
539
- # @return [Hanami::Routing::Route] this may vary according to the :route
540
- # option passed to the constructor
541
- #
542
- # @see Hanami::Router#get
301
+ # @since 0.1.0
543
302
  #
544
- # @since 0.8.0
545
- def unlink(path, options = {}, &blk)
546
- @router.unlink(path, options, &blk)
303
+ # @see #get
304
+ # @see #initialize
305
+ # @see #path
306
+ # @see #url
307
+ def options(path, to: nil, as: nil, **constraints, &blk)
308
+ add_route("OPTIONS", path, to, as, constraints, &blk)
547
309
  end
548
310
 
549
- # Defines a root route (a GET route for '/')
550
- #
551
- # @param options [Hash] the options to customize the route
552
- # @option options [String,Proc,Class,Object#call] :to the endpoint
311
+ # Defines a route that accepts LINK requests for the given path.
553
312
  #
313
+ # @param path [String] the relative URL to be matched
314
+ # @param to [#call] the Rack endpoint
315
+ # @param as [Symbol] a unique name for the route
316
+ # @param constraints [Hash] a set of constraints for path variables
554
317
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
555
318
  #
556
- # @return [Hanami::Routing::Route] this may vary according to the :route
557
- # option passed to the constructor
558
- #
559
- # @since 0.7.0
560
- #
561
- # @example Fixed matching string
562
- # require 'hanami/router'
563
- #
564
- # router = Hanami::Router.new
565
- # router.root to: ->(env) { [200, {}, ['Hello from Hanami!']] }
566
- #
567
- # @example Included names as `root` (for path and url helpers)
568
- # require 'hanami/router'
569
- #
570
- # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
571
- # router.root to: ->(env) { [200, {}, ['Hello from Hanami!']] }
319
+ # @since 0.1.0
572
320
  #
573
- # router.path(:root) # => "/"
574
- # router.url(:root) # => "https://hanamirb.org/"
575
- def root(options = {}, &blk)
576
- @router.get(ROOT_PATH, options.merge(as: :root), &blk)
321
+ # @see #get
322
+ # @see #initialize
323
+ # @see #path
324
+ # @see #url
325
+ def link(path, to: nil, as: nil, **constraints, &blk)
326
+ add_route("LINK", path, to, as, constraints, &blk)
577
327
  end
578
328
 
579
- # Defines a route that accepts a OPTIONS request for the given path.
329
+ # Defines a route that accepts UNLINK requests for the given path.
580
330
  #
581
331
  # @param path [String] the relative URL to be matched
582
- #
583
- # @param options [Hash] the options to customize the route
584
- # @option options [String,Proc,Class,Object#call] :to the endpoint
585
- #
332
+ # @param to [#call] the Rack endpoint
333
+ # @param as [Symbol] a unique name for the route
334
+ # @param constraints [Hash] a set of constraints for path variables
586
335
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
587
336
  #
588
- # @return [Hanami::Routing::Route] this may vary according to the :route
589
- # option passed to the constructor
590
- #
591
- # @see Hanami::Router#get
592
- #
593
- # @since 0.1.0
594
- def options(path, options = {}, &blk)
595
- @router.options(path, options, &blk)
596
- end
597
-
598
- # Defines an HTTP redirect
599
- #
600
- # @param path [String] the path that needs to be redirected
601
- # @param options [Hash] the options to customize the redirect behavior
602
- # @option options [Fixnum] the HTTP status to return (defaults to `301`)
603
- #
604
- # @return [Hanami::Routing::Route] the generated route.
605
- # This may vary according to the `:route` option passed to the initializer
606
- #
607
337
  # @since 0.1.0
608
338
  #
609
- # @see Hanami::Router
610
- #
611
- # @example
612
- # require 'hanami/router'
613
- #
614
- # Hanami::Router.new do
615
- # redirect '/legacy', to: '/new_endpoint'
616
- # redirect '/legacy2', to: '/new_endpoint2', code: 302
617
- # end
618
- #
619
- # @example
620
- # require 'hanami/router'
621
- #
622
- # router = Hanami::Router.new
623
- # router.redirect '/legacy', to: '/new_endpoint'
624
- def redirect(path, options = {}, &endpoint)
625
- destination_path = @router.find(options)
626
- get(path).redirect(destination_path, options[:code] || 301).tap do |route|
627
- route.dest = Hanami::Routing::RedirectEndpoint.new(destination_path, route.dest)
628
- end
339
+ # @see #get
340
+ # @see #initialize
341
+ # @see #path
342
+ # @see #url
343
+ def unlink(path, to: nil, as: nil, **constraints, &blk)
344
+ add_route("UNLINK", path, to, as, constraints, &blk)
629
345
  end
630
346
 
631
- # Defines a Ruby block: all the routes defined within it will be namespaced
632
- # with the given relative path.
347
+ # Defines a route that redirects the incoming request to another path.
633
348
  #
634
- # Namespaces blocks can be nested multiple times.
635
- #
636
- # @param namespace [String] the relative path where the nested routes will
637
- # be mounted
638
- # @param blk [Proc] the block that defines the resources
639
- #
640
- # @return [Hanami::Routing::Namespace] the generated namespace.
349
+ # @param path [String] the relative URL to be matched
350
+ # @param to [#call] the Rack endpoint
351
+ # @param as [Symbol] a unique name for the route
352
+ # @param code [Integer] a HTTP status code to use for the redirect
641
353
  #
642
354
  # @since 0.1.0
643
355
  #
644
- # @see Hanami::Router
645
- #
646
- # @example Basic example
647
- # require 'hanami/router'
648
- #
649
- # Hanami::Router.new do
650
- # namespace 'trees' do
651
- # get '/sequoia', to: endpoint # => '/trees/sequoia'
652
- # end
653
- # end
654
- #
655
- # @example Nested namespaces
656
- # require 'hanami/router'
657
- #
658
- # Hanami::Router.new do
659
- # namespace 'animals' do
660
- # namespace 'mammals' do
661
- # get '/cats', to: endpoint # => '/animals/mammals/cats'
662
- # end
663
- # end
664
- # end
665
- #
666
- # @example
667
- # require 'hanami/router'
668
- #
669
- # router = Hanami::Router.new
670
- # router.namespace 'trees' do
671
- # get '/sequoia', to: endpoint # => '/trees/sequoia'
672
- # end
673
- def namespace(namespace, &blk)
674
- Routing::Namespace.new(self, namespace, &blk)
356
+ # @see #get
357
+ # @see #initialize
358
+ def redirect(path, to: nil, as: nil, code: DEFAULT_REDIRECT_CODE)
359
+ get(path, to: _redirect(to, code), as: as)
675
360
  end
676
361
 
677
- # Defines a set of named routes for a single RESTful resource.
678
- # It has a built-in integration for Hanami::Controller.
679
- #
680
- # @param name [String] the name of the resource
681
- # @param options [Hash] a set of options to customize the routes
682
- # @option options [Array<Symbol>] :only a subset of the default routes
683
- # that we want to generate
684
- # @option options [Array<Symbol>] :except prevent the given routes to be
685
- # generated
686
- # @param blk [Proc] a block of code to generate additional routes
687
- #
688
- # @return [Hanami::Routing::Resource]
689
- #
690
- # @since 0.1.0
362
+ # Defines a routing scope. Routes defined in the context of a scope,
363
+ # inherit the given path as path prefix and as a named routes prefix.
691
364
  #
692
- # @see Hanami::Routing::Resource
693
- # @see Hanami::Routing::Resource::Action
694
- # @see Hanami::Routing::Resource::Options
365
+ # @param path [String] the scope path to be used as a path prefix
366
+ # @param blk [Proc] the routes definitions withing the scope
695
367
  #
696
- # @example Default usage
697
- # require 'hanami/router'
698
- #
699
- # Hanami::Router.new do
700
- # resource 'identity'
701
- # end
702
- #
703
- # # It generates:
704
- # #
705
- # # +--------+----------------+-------------------+----------+----------------+
706
- # # | Verb | Path | Action | Name | Named Route |
707
- # # +--------+----------------+-------------------+----------+----------------+
708
- # # | GET | /identity | Identity::Show | :show | :identity |
709
- # # | GET | /identity/new | Identity::New | :new | :new_identity |
710
- # # | POST | /identity | Identity::Create | :create | :identity |
711
- # # | GET | /identity/edit | Identity::Edit | :edit | :edit_identity |
712
- # # | PATCH | /identity | Identity::Update | :update | :identity |
713
- # # | DELETE | /identity | Identity::Destroy | :destroy | :identity |
714
- # # +--------+----------------+-------------------+----------+----------------+
368
+ # @since 2.0.0
715
369
  #
370
+ # @see #path
716
371
  #
372
+ # @example
373
+ # require "hanami/router"
717
374
  #
718
- # @example Limit the generated routes with :only
719
- # require 'hanami/router'
720
- #
721
- # Hanami::Router.new do
722
- # resource 'identity', only: [:show, :new, :create]
723
- # end
724
- #
725
- # # It generates:
726
- # #
727
- # # +--------+----------------+------------------+----------+----------------+
728
- # # | Verb | Path | Action | Name | Named Route |
729
- # # +--------+----------------+------------------+----------+----------------+
730
- # # | GET | /identity | Identity::Show | :show | :identity |
731
- # # | GET | /identity/new | Identity::New | :new | :new_identity |
732
- # # | POST | /identity | Identity::Create | :create | :identity |
733
- # # +--------+----------------+------------------+----------+----------------+
734
- #
735
- #
736
- #
737
- # @example Limit the generated routes with :except
738
- # require 'hanami/router'
739
- #
740
- # Hanami::Router.new do
741
- # resource 'identity', except: [:edit, :update, :destroy]
742
- # end
743
- #
744
- # # It generates:
745
- # #
746
- # # +--------+----------------+------------------+----------+----------------+
747
- # # | Verb | Path | Action | Name | Named Route |
748
- # # +--------+----------------+------------------+----------+----------------+
749
- # # | GET | /identity | Identity::Show | :show | :identity |
750
- # # | GET | /identity/new | Identity::New | :new | :new_identity |
751
- # # | POST | /identity | Identity::Create | :create | :identity |
752
- # # +--------+----------------+------------------+----------+----------------+
753
- #
754
- #
755
- #
756
- # @example Additional single routes
757
- # require 'hanami/router'
758
- #
759
- # Hanami::Router.new do
760
- # resource 'identity', only: [] do
761
- # member do
762
- # patch 'activate'
763
- # end
764
- # end
765
- # end
766
- #
767
- # # It generates:
768
- # #
769
- # # +--------+--------------------+--------------------+------+--------------------+
770
- # # | Verb | Path | Action | Name | Named Route |
771
- # # +--------+--------------------+--------------------+------+--------------------+
772
- # # | PATCH | /identity/activate | Identity::Activate | | :activate_identity |
773
- # # +--------+--------------------+--------------------+------+--------------------+
774
- #
775
- #
776
- #
777
- # @example Additional collection routes
778
- # require 'hanami/router'
779
- #
780
- # Hanami::Router.new do
781
- # resource 'identity', only: [] do
782
- # collection do
783
- # get 'keys'
784
- # end
375
+ # router = Hanami::Router.new do
376
+ # scope "v1" do
377
+ # get "/users", to: ->(*) { ... }, as: :users
785
378
  # end
786
379
  # end
787
380
  #
788
- # # It generates:
789
- # #
790
- # # +------+----------------+----------------+------+----------------+
791
- # # | Verb | Path | Action | Name | Named Route |
792
- # # +------+----------------+----------------+------+----------------+
793
- # # | GET | /identity/keys | Identity::Keys | | :keys_identity |
794
- # # +------+----------------+----------------+------+----------------+
795
- def resource(name, options = {}, &blk)
796
- Routing::Resource.new(self, name, options.merge(separator: @router.action_separator), &blk)
797
- end
381
+ # router.path(:v1_users) # => "/v1/users"
382
+ def scope(path, &blk)
383
+ path_prefix = @path_prefix
384
+ name_prefix = @name_prefix
798
385
 
799
- # Defines a set of named routes for a plural RESTful resource.
800
- # It has a built-in integration for Hanami::Controller.
801
- #
802
- # @param name [String] the name of the resource
803
- # @param options [Hash] a set of options to customize the routes
804
- # @option options [Array<Symbol>] :only a subset of the default routes
805
- # that we want to generate
806
- # @option options [Array<Symbol>] :except prevent the given routes to be
807
- # generated
808
- # @param blk [Proc] a block of code to generate additional routes
809
- #
810
- # @return [Hanami::Routing::Resources]
811
- #
812
- # @since 0.1.0
813
- #
814
- # @see Hanami::Routing::Resources
815
- # @see Hanami::Routing::Resources::Action
816
- # @see Hanami::Routing::Resource::Options
817
- #
818
- # @example Default usage
819
- # require 'hanami/router'
820
- #
821
- # Hanami::Router.new do
822
- # resources 'articles'
823
- # end
824
- #
825
- # # It generates:
826
- # #
827
- # # +--------+--------------------+-------------------+----------+----------------+
828
- # # | Verb | Path | Action | Name | Named Route |
829
- # # +--------+--------------------+-------------------+----------+----------------+
830
- # # | GET | /articles | Articles::Index | :index | :articles |
831
- # # | GET | /articles/:id | Articles::Show | :show | :articles |
832
- # # | GET | /articles/new | Articles::New | :new | :new_articles |
833
- # # | POST | /articles | Articles::Create | :create | :articles |
834
- # # | GET | /articles/:id/edit | Articles::Edit | :edit | :edit_articles |
835
- # # | PATCH | /articles/:id | Articles::Update | :update | :articles |
836
- # # | DELETE | /articles/:id | Articles::Destroy | :destroy | :articles |
837
- # # +--------+--------------------+-------------------+----------+----------------+
838
- #
839
- #
840
- #
841
- # @example Limit the generated routes with :only
842
- # require 'hanami/router'
843
- #
844
- # Hanami::Router.new do
845
- # resources 'articles', only: [:index]
846
- # end
847
- #
848
- # # It generates:
849
- # #
850
- # # +------+-----------+-----------------+--------+-------------+
851
- # # | Verb | Path | Action | Name | Named Route |
852
- # # +------+-----------+-----------------+--------+-------------+
853
- # # | GET | /articles | Articles::Index | :index | :articles |
854
- # # +------+-----------+-----------------+--------+-------------+
855
- #
856
- #
857
- #
858
- # @example Limit the generated routes with :except
859
- # require 'hanami/router'
860
- #
861
- # Hanami::Router.new do
862
- # resources 'articles', except: [:edit, :update]
863
- # end
864
- #
865
- # # It generates:
866
- # #
867
- # # +--------+--------------------+-------------------+----------+----------------+
868
- # # | Verb | Path | Action | Name | Named Route |
869
- # # +--------+--------------------+-------------------+----------+----------------+
870
- # # | GET | /articles | Articles::Index | :index | :articles |
871
- # # | GET | /articles/:id | Articles::Show | :show | :articles |
872
- # # | GET | /articles/new | Articles::New | :new | :new_articles |
873
- # # | POST | /articles | Articles::Create | :create | :articles |
874
- # # | DELETE | /articles/:id | Articles::Destroy | :destroy | :articles |
875
- # # +--------+--------------------+-------------------+----------+----------------+
876
- #
877
- #
878
- #
879
- # @example Additional single routes
880
- # require 'hanami/router'
881
- #
882
- # Hanami::Router.new do
883
- # resources 'articles', only: [] do
884
- # member do
885
- # patch 'publish'
886
- # end
887
- # end
888
- # end
889
- #
890
- # # It generates:
891
- # #
892
- # # +--------+-----------------------+-------------------+------+-------------------+
893
- # # | Verb | Path | Action | Name | Named Route |
894
- # # +--------+-----------------------+-------------------+------+-------------------+
895
- # # | PATCH | /articles/:id/publish | Articles::Publish | | :publish_articles |
896
- # # +--------+-----------------------+-------------------+------+-------------------+
897
- #
898
- #
899
- #
900
- # @example Additional collection routes
901
- # require 'hanami/router'
902
- #
903
- # Hanami::Router.new do
904
- # resources 'articles', only: [] do
905
- # collection do
906
- # get 'search'
907
- # end
908
- # end
909
- # end
910
- #
911
- # # It generates:
912
- # #
913
- # # +------+------------------+------------------+------+------------------+
914
- # # | Verb | Path | Action | Name | Named Route |
915
- # # +------+------------------+------------------+------+------------------+
916
- # # | GET | /articles/search | Articles::Search | | :search_articles |
917
- # # +------+------------------+------------------+------+------------------+
918
- def resources(name, options = {}, &blk)
919
- Routing::Resources.new(self, name, options.merge(separator: @router.action_separator), &blk)
386
+ begin
387
+ @path_prefix = @path_prefix.join(path.to_s)
388
+ @name_prefix = @name_prefix.join(path.to_s)
389
+ instance_eval(&blk)
390
+ ensure
391
+ @path_prefix = path_prefix
392
+ @name_prefix = name_prefix
393
+ end
920
394
  end
921
395
 
922
396
  # Mount a Rack application at the specified path.
923
397
  # All the requests starting with the specified path, will be forwarded to
924
398
  # the given application.
925
399
  #
926
- # All the other methods (eg #get) support callable objects, but they
400
+ # All the other methods (eg `#get`) support callable objects, but they
927
401
  # restrict the range of the acceptable HTTP verb. Mounting an application
928
402
  # with #mount doesn't apply this kind of restriction at the router level,
929
403
  # but let the application to decide.
930
404
  #
931
405
  # @param app [#call] a class or an object that responds to #call
932
- # @param options [Hash] the options to customize the mount
933
- # @option options [:at] the relative path where to mount the app
406
+ # @param at [String] the relative path where to mount the app
407
+ # @param constraints [Hash] a set of constraints for path variables
934
408
  #
935
409
  # @since 0.1.1
936
410
  #
937
- # @example Basic usage
938
- # require 'hanami/router'
411
+ # @example
412
+ # require "hanami/router"
939
413
  #
940
414
  # Hanami::Router.new do
941
- # mount Api::App.new, at: '/api'
415
+ # mount MyRackApp.new, at: "/foo"
942
416
  # end
417
+ def mount(app, at:, **constraints)
418
+ path = prefixed_path(at)
419
+ prefix = Segment.fabricate(path, **constraints)
420
+
421
+ @mounted[prefix] = @resolver.call(path, app)
422
+ if inspect?
423
+ @inspector.add_route(Route.new(http_method: "*", path: at, to: app, constraints: constraints))
424
+ end
425
+ end
426
+
427
+ # Generate an relative URL for a specified named route.
428
+ # The additional arguments will be used to compose the relative URL - in
429
+ # case it has tokens to match - and for compose the query string.
943
430
  #
944
- # # Requests:
945
- # #
946
- # # GET /api # => 200
947
- # # GET /api/articles # => 200
948
- # # POST /api/articles # => 200
949
- # # GET /api/unknown # => 404
950
- #
951
- # @example Difference between #get and #mount
952
- # require 'hanami/router'
431
+ # @param name [Symbol] the route name
953
432
  #
954
- # Hanami::Router.new do
955
- # get '/rack1', to: RackOne.new
956
- # mount RackTwo.new, at: '/rack2'
957
- # end
433
+ # @return [String]
958
434
  #
959
- # # Requests:
960
- # #
961
- # # # /rack1 will only accept GET
962
- # # GET /rack1 # => 200 (RackOne.new)
963
- # # POST /rack1 # => 405
964
- # #
965
- # # # /rack2 accepts all the verbs and delegate the decision to RackTwo
966
- # # GET /rack2 # => 200 (RackTwo.new)
967
- # # POST /rack2 # => 200 (RackTwo.new)
968
- #
969
- # @example Types of mountable applications
970
- # require 'hanami/router'
971
- #
972
- # class RackOne
973
- # def self.call(env)
974
- # end
975
- # end
435
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
436
+ # recognize a route, because of the given arguments.
976
437
  #
977
- # class RackTwo
978
- # def call(env)
979
- # end
980
- # end
438
+ # @since 0.1.0
981
439
  #
982
- # class RackThree
983
- # def call(env)
984
- # end
985
- # end
440
+ # @see #url
986
441
  #
987
- # module Dashboard
988
- # class Index
989
- # def call(env)
990
- # end
991
- # end
992
- # end
442
+ # @example
443
+ # require "hanami/router"
993
444
  #
994
- # Hanami::Router.new do
995
- # mount RackOne, at: '/rack1'
996
- # mount RackTwo, at: '/rack2'
997
- # mount RackThree.new, at: '/rack3'
998
- # mount ->(env) {[200, {}, ['Rack Four']]}, at: '/rack4'
999
- # mount 'dashboard#index', at: '/dashboard'
445
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
446
+ # get "/login", to: ->(*) { ... }, as: :login
447
+ # get "/:name", to: ->(*) { ... }, as: :framework
1000
448
  # end
1001
449
  #
1002
- # # 1. RackOne is used as it is (class), because it respond to .call
1003
- # # 2. RackTwo is initialized, because it respond to #call
1004
- # # 3. RackThree is used as it is (object), because it respond to #call
1005
- # # 4. That Proc is used as it is, because it respond to #call
1006
- # # 5. That string is resolved as Dashboard::Index (Hanami::Controller)
1007
- def mount(app, options)
1008
- @router.mount(app, options)
450
+ # router.path(:login) # => "/login"
451
+ # router.path(:login, return_to: "/dashboard") # => "/login?return_to=%2Fdashboard"
452
+ # router.path(:framework, name: "router") # => "/router"
453
+ def path(name, variables = {})
454
+ @url_helpers.path(name, variables)
1009
455
  end
1010
456
 
1011
- # Resolve the given Rack env to a registered endpoint and invoke it.
457
+ # Generate an absolute URL for a specified named route.
458
+ # The additional arguments will be used to compose the relative URL - in
459
+ # case it has tokens to match - and for compose the query string.
1012
460
  #
1013
- # @param env [Hash] a Rack env instance
461
+ # @param name [Symbol] the route name
1014
462
  #
1015
- # @return [Rack::Response, Array]
463
+ # @return [String]
464
+ #
465
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
466
+ # recognize a route, because of the given arguments.
1016
467
  #
1017
468
  # @since 0.1.0
1018
- def call(env)
1019
- @router.call(env)
469
+ #
470
+ # @see #path
471
+ #
472
+ # @example
473
+ # require "hanami/router"
474
+ #
475
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
476
+ # get "/login", to: ->(*) { ... }, as: :login
477
+ # get "/:name", to: ->(*) { ... }, as: :framework
478
+ # end
479
+ #
480
+ # router.url(:login) # => "https://hanamirb.org/login"
481
+ # router.url(:login, return_to: "/dashboard") # => "https://hanamirb.org/login?return_to=%2Fdashboard"
482
+ # router.url(:framework, name: "router") # => "https://hanamirb.org/router"
483
+ def url(name, variables = {})
484
+ @url_helpers.url(name, variables)
1020
485
  end
1021
486
 
1022
487
  # Recognize the given env, path, or name and return a route for testing
@@ -1037,34 +502,34 @@ module Hanami
1037
502
  # @see Hanami::Routing::RecognizedRoute
1038
503
  #
1039
504
  # @example Successful Path Recognition
1040
- # require 'hanami/router'
505
+ # require "hanami/router"
1041
506
  #
1042
507
  # router = Hanami::Router.new do
1043
- # get '/books/:id', to: 'books#show', as: :book
508
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1044
509
  # end
1045
510
  #
1046
- # route = router.recognize('/books/23')
511
+ # route = router.recognize("/books/23")
1047
512
  # route.verb # => "GET" (default)
1048
513
  # route.routable? # => true
1049
514
  # route.params # => {:id=>"23"}
1050
515
  #
1051
516
  # @example Successful Rack Env Recognition
1052
- # require 'hanami/router'
517
+ # require "hanami/router"
1053
518
  #
1054
519
  # router = Hanami::Router.new do
1055
- # get '/books/:id', to: 'books#show', as: :book
520
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1056
521
  # end
1057
522
  #
1058
- # route = router.recognize(Rack::MockRequest.env_for('/books/23'))
523
+ # route = router.recognize(Rack::MockRequest.env_for("/books/23"))
1059
524
  # route.verb # => "GET" (default)
1060
525
  # route.routable? # => true
1061
526
  # route.params # => {:id=>"23"}
1062
527
  #
1063
528
  # @example Successful Named Route Recognition
1064
- # require 'hanami/router'
529
+ # require "hanami/router"
1065
530
  #
1066
531
  # router = Hanami::Router.new do
1067
- # get '/books/:id', to: 'books#show', as: :book
532
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1068
533
  # end
1069
534
  #
1070
535
  # route = router.recognize(:book, id: 23)
@@ -1073,43 +538,43 @@ module Hanami
1073
538
  # route.params # => {:id=>"23"}
1074
539
  #
1075
540
  # @example Failing Recognition For Unknown Path
1076
- # require 'hanami/router'
541
+ # require "hanami/router"
1077
542
  #
1078
543
  # router = Hanami::Router.new do
1079
- # get '/books/:id', to: 'books#show', as: :book
544
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1080
545
  # end
1081
546
  #
1082
- # route = router.recognize('/books')
547
+ # route = router.recognize("/books")
1083
548
  # route.verb # => "GET" (default)
1084
549
  # route.routable? # => false
1085
550
  #
1086
551
  # @example Failing Recognition For Path With Wrong HTTP Verb
1087
- # require 'hanami/router'
552
+ # require "hanami/router"
1088
553
  #
1089
554
  # router = Hanami::Router.new do
1090
- # get '/books/:id', to: 'books#show', as: :book
555
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1091
556
  # end
1092
557
  #
1093
- # route = router.recognize('/books/23', method: :post)
558
+ # route = router.recognize("/books/23", method: :post)
1094
559
  # route.verb # => "POST"
1095
560
  # route.routable? # => false
1096
561
  #
1097
562
  # @example Failing Recognition For Rack Env With Wrong HTTP Verb
1098
- # require 'hanami/router'
563
+ # require "hanami/router"
1099
564
  #
1100
565
  # router = Hanami::Router.new do
1101
- # get '/books/:id', to: 'books#show', as: :book
566
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1102
567
  # end
1103
568
  #
1104
- # route = router.recognize(Rack::MockRequest.env_for('/books/23', method: :post))
569
+ # route = router.recognize(Rack::MockRequest.env_for("/books/23", method: :post))
1105
570
  # route.verb # => "POST"
1106
571
  # route.routable? # => false
1107
572
  #
1108
573
  # @example Failing Recognition Named Route With Wrong Params
1109
- # require 'hanami/router'
574
+ # require "hanami/router"
1110
575
  #
1111
576
  # router = Hanami::Router.new do
1112
- # get '/books/:id', to: 'books#show', as: :book
577
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1113
578
  # end
1114
579
  #
1115
580
  # route = router.recognize(:book)
@@ -1117,105 +582,94 @@ module Hanami
1117
582
  # route.routable? # => false
1118
583
  #
1119
584
  # @example Failing Recognition Named Route With Wrong HTTP Verb
1120
- # require 'hanami/router'
585
+ # require "hanami/router"
1121
586
  #
1122
587
  # router = Hanami::Router.new do
1123
- # get '/books/:id', to: 'books#show', as: :book
588
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1124
589
  # end
1125
590
  #
1126
591
  # route = router.recognize(:book, {method: :post}, {id: 1})
1127
592
  # route.verb # => "POST"
1128
593
  # route.routable? # => false
1129
594
  # route.params # => {:id=>"1"}
1130
- def recognize(env, options = {}, params = nil)
1131
- require 'hanami/routing/recognized_route'
1132
-
1133
- env = env_for(env, options, params)
1134
- responses, _ = *@router.recognize(env)
1135
-
1136
- Routing::RecognizedRoute.new(
1137
- responses.nil? ? responses : responses.first,
1138
- env, @router)
595
+ def recognize(env, params = {}, options = {})
596
+ require "hanami/router/recognized_route"
597
+ env = env_for(env, params, options)
598
+ endpoint, params = lookup(env)
599
+
600
+ RecognizedRoute.new(
601
+ endpoint, _params(env, params)
602
+ )
1139
603
  end
1140
604
 
1141
- # Generate an relative URL for a specified named route.
1142
- # The additional arguments will be used to compose the relative URL - in
1143
- # case it has tokens to match - and for compose the query string.
1144
- #
1145
- # @param route [Symbol] the route name
1146
- #
1147
- # @return [String]
605
+ # Returns formatted routes
1148
606
  #
1149
- # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
1150
- # recognize a route, because of the given arguments.
1151
- #
1152
- # @since 0.1.0
607
+ # @return [String] formatted routes
1153
608
  #
1154
- # @example
1155
- # require 'hanami/router'
1156
- #
1157
- # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
1158
- # router.get '/login', to: 'sessions#new', as: :login
1159
- # router.get '/:name', to: 'frameworks#show', as: :framework
1160
- #
1161
- # router.path(:login) # => "/login"
1162
- # router.path(:login, return_to: '/dashboard') # => "/login?return_to=%2Fdashboard"
1163
- # router.path(:framework, name: 'router') # => "/router"
1164
- def path(route, *args)
1165
- @router.path(route, *args)
609
+ # @since 2.0.0
610
+ # @api private
611
+ def to_inspect
612
+ require "hanami/router/inspector"
613
+
614
+ inspector = Inspector.new
615
+ with(inspector: inspector)
616
+
617
+ inspector.call
1166
618
  end
1167
619
 
1168
- # Generate a URL for a specified named route.
1169
- # The additional arguments will be used to compose the relative URL - in
1170
- # case it has tokens to match - and for compose the query string.
1171
- #
1172
- # @param route [Symbol] the route name
1173
- #
1174
- # @return [String]
1175
- #
1176
- # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
1177
- # recognize a route, because of the given arguments.
1178
- #
1179
- # @since 0.1.0
1180
- #
1181
- # @example
1182
- # require 'hanami/router'
1183
- #
1184
- # router = Hanami::Router.new(scheme: 'https', host: 'hanamirb.org')
1185
- # router.get '/login', to: 'sessions#new', as: :login
1186
- # router.get '/:name', to: 'frameworks#show', as: :framework
1187
- #
1188
- # router.url(:login) # => "https://hanamirb.org/login"
1189
- # router.url(:login, return_to: '/dashboard') # => "https://hanamirb.org/login?return_to=%2Fdashboard"
1190
- # router.url(:framework, name: 'router') # => "https://hanamirb.org/router"
1191
- def url(route, *args)
1192
- @router.url(route, *args)
620
+ # @since 2.0.0
621
+ # @api private
622
+ def fixed(env)
623
+ @fixed.dig(env["REQUEST_METHOD"], env["PATH_INFO"])
1193
624
  end
1194
625
 
1195
- # Returns an routes inspector
1196
- #
1197
- # @since 0.2.0
1198
- #
1199
- # @see Hanami::Routing::RoutesInspector
1200
- #
1201
- # @example
1202
- # require 'hanami/router'
1203
- #
1204
- # router = Hanami::Router.new do
1205
- # get '/', to: 'home#index'
1206
- # get '/login', to: 'sessions#new', as: :login
1207
- # post '/login', to: 'sessions#create'
1208
- # delete '/logout', to: 'sessions#destroy', as: :logout
1209
- # end
1210
- #
1211
- # puts router.inspector
1212
- # # => GET, HEAD / Home::Index
1213
- # login GET, HEAD /login Sessions::New
1214
- # POST /login Sessions::Create
1215
- # logout GET, HEAD /logout Sessions::Destroy
1216
- def inspector
1217
- require 'hanami/routing/routes_inspector'
1218
- Routing::RoutesInspector.new(@router.routes, @router.prefix)
626
+ # @since 2.0.0
627
+ # @api private
628
+ def variable(env)
629
+ @variable[env["REQUEST_METHOD"]]&.find(env["PATH_INFO"])
630
+ end
631
+
632
+ # @since 2.0.0
633
+ # @api private
634
+ def globbed(env)
635
+ @globbed[env["REQUEST_METHOD"]]&.each do |path, to|
636
+ if (match = path.match(env["PATH_INFO"]))
637
+ return [to, match.named_captures]
638
+ end
639
+ end
640
+
641
+ nil
642
+ end
643
+
644
+ # @since 2.0.0
645
+ # @api private
646
+ def mounted(env)
647
+ @mounted.each do |prefix, app|
648
+ next unless (match = prefix.peek_match(env["PATH_INFO"]))
649
+
650
+ # TODO: ensure compatibility with existing env["SCRIPT_NAME"]
651
+ # TODO: cleanup this code
652
+ env["SCRIPT_NAME"] = env["SCRIPT_NAME"].to_s + prefix.to_s
653
+ env["PATH_INFO"] = env["PATH_INFO"].sub(prefix.to_s, "")
654
+ env["PATH_INFO"] = "/" if env["PATH_INFO"] == ""
655
+
656
+ return [app, match.named_captures]
657
+ end
658
+
659
+ nil
660
+ end
661
+
662
+ # @since 2.0.0
663
+ # @api private
664
+ def not_allowed(env)
665
+ (_not_allowed_fixed(env) ||
666
+ _not_allowed_variable(env)) and return [405, {"Content-Length" => "11"}, ["Not Allowed"]]
667
+ end
668
+
669
+ # @since 2.0.0
670
+ # @api private
671
+ def not_found(env)
672
+ @not_found.call(env)
1219
673
  end
1220
674
 
1221
675
  protected
@@ -1233,20 +687,219 @@ module Hanami
1233
687
  #
1234
688
  # @see Hanami::Router#recognize
1235
689
  # @see http://www.rubydoc.info/github/rack/rack/Rack%2FMockRequest.env_for
1236
- def env_for(env, options = {}, params = nil)
1237
- env = case env
1238
- when String
1239
- Rack::MockRequest.env_for(env, options)
1240
- when Symbol
690
+ def env_for(env, params = {}, options = {})
691
+ require "rack/mock"
692
+
693
+ case env
694
+ when ::String
695
+ ::Rack::MockRequest.env_for(env, options)
696
+ when ::Symbol
1241
697
  begin
1242
- url = path(env, params || options)
1243
- return env_for(url, options)
1244
- rescue Hanami::Routing::InvalidRouteException
1245
- {}
698
+ url = path(env, params)
699
+ return env_for(url, params, options) # rubocop:disable Style/RedundantReturn
700
+ rescue Hanami::Router::InvalidRouteException
701
+ {} # Empty Rack env
1246
702
  end
1247
703
  else
1248
704
  env
1249
705
  end
1250
706
  end
707
+
708
+ private
709
+
710
+ # @since 2.0.0
711
+ # @api private
712
+ DEFAULT_BASE_URL = "http://localhost"
713
+
714
+ # @since 2.0.0
715
+ # @api private
716
+ DEFAULT_PREFIX = "/"
717
+
718
+ # @since 2.0.0
719
+ # @api private
720
+ DEFAULT_RESOLVER = ->(_, to) { to }
721
+
722
+ # @since 2.0.0
723
+ # @api private
724
+ DEFAULT_REDIRECT_CODE = 301
725
+
726
+ # @since 2.0.0
727
+ # @api private
728
+ PARAMS = "router.params"
729
+
730
+ # Default response when no route was matched
731
+ #
732
+ # @api private
733
+ # @since 2.0.0
734
+ NOT_FOUND = ->(*) { [404, {"Content-Length" => "9"}, ["Not Found"]] }.freeze
735
+
736
+ # @since 2.0.0
737
+ # @api private
738
+ def lookup(env)
739
+ endpoint = fixed(env)
740
+ return [endpoint, {}] if endpoint
741
+
742
+ variable(env) || globbed(env) || mounted(env)
743
+ end
744
+
745
+ # @since 2.0.0
746
+ # @api private
747
+ def add_route(http_method, path, to, as, constraints, &blk)
748
+ path = prefixed_path(path)
749
+ to = resolve_endpoint(path, to, blk)
750
+
751
+ if globbed?(path)
752
+ add_globbed_route(http_method, path, to, constraints)
753
+ elsif variable?(path)
754
+ add_variable_route(http_method, path, to, constraints)
755
+ else
756
+ add_fixed_route(http_method, path, to)
757
+ end
758
+
759
+ add_named_route(path, as, constraints) if as
760
+
761
+ if inspect?
762
+ @inspector.add_route(
763
+ Route.new(http_method: http_method, path: path, to: to, as: as, constraints: constraints, blk: blk)
764
+ )
765
+ end
766
+ end
767
+
768
+ # @since 2.0.0
769
+ # @api private
770
+ def resolve_endpoint(path, to, blk)
771
+ (to || blk) or raise MissingEndpointError.new(path)
772
+ to = Block.new(@block_context, blk) if to.nil?
773
+
774
+ @resolver.call(path, to)
775
+ end
776
+
777
+ # @since 2.0.0
778
+ # @api private
779
+ def add_globbed_route(http_method, path, to, constraints)
780
+ @globbed[http_method] ||= []
781
+ @globbed[http_method] << [Segment.fabricate(path, **constraints), to]
782
+ end
783
+
784
+ # @since 2.0.0
785
+ # @api private
786
+ def add_variable_route(http_method, path, to, constraints)
787
+ @variable[http_method] ||= Trie.new
788
+ @variable[http_method].add(path, to, constraints)
789
+ end
790
+
791
+ # @since 2.0.0
792
+ # @api private
793
+ def add_fixed_route(http_method, path, to)
794
+ @fixed[http_method] ||= {}
795
+ @fixed[http_method][path] = to
796
+ end
797
+
798
+ # @since 2.0.0
799
+ # @api private
800
+ def add_named_route(path, as, constraints)
801
+ @url_helpers.add(prefixed_name(as), Segment.fabricate(path, **constraints))
802
+ end
803
+
804
+ # @since 2.0.0
805
+ # @api private
806
+ def variable?(path)
807
+ /:/.match?(path)
808
+ end
809
+
810
+ # @since 2.0.0
811
+ # @api private
812
+ def globbed?(path)
813
+ /\*/.match?(path)
814
+ end
815
+
816
+ # @since 2.0.0
817
+ # @api private
818
+ def inspect?
819
+ !@inspector.nil?
820
+ end
821
+
822
+ # @since 2.0.0
823
+ # @api private
824
+ def prefixed_path(path)
825
+ @path_prefix.join(path).to_s
826
+ end
827
+
828
+ # @since 2.0.0
829
+ # @api private
830
+ def prefixed_name(name)
831
+ @name_prefix.relative_join(name, "_").to_sym
832
+ end
833
+
834
+ # Returns a new instance of Hanami::Router with the modified options.
835
+ #
836
+ # @return [Hanami::Route] a new instance of Hanami::Router
837
+ #
838
+ # @see Hanami::Router#initialize
839
+ #
840
+ # @since 2.0.0
841
+ # @api private
842
+ def with(**new_options, &blk)
843
+ options = {
844
+ base_url: @base_url,
845
+ prefix: @path_prefix.to_s,
846
+ resolver: @resolver,
847
+ not_found: @not_found,
848
+ block_context: @block_context,
849
+ inspector: @inspector
850
+ }
851
+
852
+ self.class.new(**options.merge(new_options), &(blk || @blk))
853
+ end
854
+
855
+ # @since 2.0.0
856
+ # @api private
857
+ def _redirect(to, code)
858
+ body = Rack::Utils::HTTP_STATUS_CODES.fetch(code) do
859
+ raise UnknownHTTPStatusCodeError.new(code)
860
+ end
861
+
862
+ destination = prefixed_path(to)
863
+ Redirect.new(destination, code, ->(*) { [code, {"Location" => destination}, [body]] })
864
+ end
865
+
866
+ # @since 2.0.0
867
+ # @api private
868
+ def _params(env, params)
869
+ params ||= {}
870
+ env[PARAMS] ||= {}
871
+ env[PARAMS].merge!(Rack::Utils.parse_nested_query(env["QUERY_STRING"]))
872
+ env[PARAMS].merge!(params)
873
+ env[PARAMS] = Params.deep_symbolize(env[PARAMS])
874
+ env
875
+ end
876
+
877
+ # @since 2.0.0
878
+ # @api private
879
+ def _not_allowed_fixed(env)
880
+ found = false
881
+
882
+ @fixed.each_value do |routes|
883
+ break if found
884
+
885
+ found = routes.key?(env["PATH_INFO"])
886
+ end
887
+
888
+ found
889
+ end
890
+
891
+ # @since 2.0.0
892
+ # @api private
893
+ def _not_allowed_variable(env)
894
+ found = false
895
+
896
+ @variable.each_value do |routes|
897
+ break if found
898
+
899
+ found = routes.find(env["PATH_INFO"])
900
+ end
901
+
902
+ found
903
+ end
1251
904
  end
1252
905
  end