hanami-router 1.3.1 → 2.0.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +97 -443
  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 +548 -957
  13. data/lib/hanami/router/block.rb +88 -0
  14. data/lib/hanami/router/error.rb +67 -0
  15. data/lib/hanami/router/node.rb +91 -0
  16. data/lib/hanami/router/params.rb +35 -0
  17. data/lib/hanami/router/prefix.rb +67 -0
  18. data/lib/hanami/router/recognized_route.rb +92 -0
  19. data/lib/hanami/router/redirect.rb +28 -0
  20. data/lib/hanami/router/segment.rb +19 -0
  21. data/lib/hanami/router/trie.rb +63 -0
  22. data/lib/hanami/router/url_helpers.rb +40 -0
  23. data/lib/hanami/router/version.rb +4 -1
  24. metadata +61 -41
  25. data/lib/hanami-router.rb +0 -1
  26. data/lib/hanami/routing/endpoint.rb +0 -195
  27. data/lib/hanami/routing/endpoint_resolver.rb +0 -238
  28. data/lib/hanami/routing/error.rb +0 -7
  29. data/lib/hanami/routing/force_ssl.rb +0 -212
  30. data/lib/hanami/routing/http_router.rb +0 -220
  31. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  32. data/lib/hanami/routing/namespace.rb +0 -98
  33. data/lib/hanami/routing/parsers.rb +0 -113
  34. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  35. data/lib/hanami/routing/parsing/parser.rb +0 -61
  36. data/lib/hanami/routing/recognized_route.rb +0 -219
  37. data/lib/hanami/routing/resource.rb +0 -119
  38. data/lib/hanami/routing/resource/action.rb +0 -402
  39. data/lib/hanami/routing/resource/nested.rb +0 -41
  40. data/lib/hanami/routing/resource/options.rb +0 -74
  41. data/lib/hanami/routing/resources.rb +0 -48
  42. data/lib/hanami/routing/resources/action.rb +0 -156
  43. data/lib/hanami/routing/route.rb +0 -71
  44. 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', '~> 12'
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
@@ -1,131 +1,30 @@
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
83
- #
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
102
-
103
- # Defines root path
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/url_helpers"
19
+
20
+ # URL helpers for other Hanami integrations
104
21
  #
105
- # @since 0.7.0
106
22
  # @api private
107
- #
108
- # @see Hanami::Router#root
109
- ROOT_PATH = '/'.freeze
23
+ # @since 2.0.0
24
+ attr_reader :url_helpers
110
25
 
111
26
  # Returns the given block as it is.
112
27
  #
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
28
  # @param blk [Proc] a set of route definitions
130
29
  #
131
30
  # @return [Proc] the given block
@@ -135,888 +34,440 @@ module Hanami
135
34
  # @example
136
35
  # # apps/web/config/routes.rb
137
36
  # Hanami::Router.define do
138
- # get '/', to: 'home#index'
37
+ # get "/", to: ->(*) { ... }
139
38
  # end
140
39
  def self.define(&blk)
141
40
  blk
142
41
  end
143
42
 
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
43
+ # Initialize the router
161
44
  #
162
- # @return [Hanami::Router] self
45
+ # @param base_url [String] the base URL where the HTTP application is
46
+ # deployed
47
+ # @param prefix [String] the relative URL prefix where the HTTP application
48
+ # is deployed
49
+ # @param resolver [#call(path, to)] a resolver for route entpoints
50
+ # @param block_context [Hanami::Router::Block::Context)
51
+ # @param not_found [#call(env)] default handler when route is not matched
52
+ # @param blk [Proc] the route definitions
163
53
  #
164
54
  # @since 0.1.0
165
55
  #
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
56
+ # @return [Hanami::Router]
201
57
  #
202
- # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
58
+ # @example Base usage
59
+ # require "hanami/router"
203
60
  #
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
225
- #
226
- # # From the shell
227
- #
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)
61
+ # Hanami::Router.new do
62
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
63
+ # end
64
+ def initialize(base_url: DEFAULT_BASE_URL, prefix: DEFAULT_PREFIX, resolver: DEFAULT_RESOLVER, not_found: NOT_FOUND, block_context: nil, &blk) # rubocop:disable Layout/LineLength
65
+ # TODO: verify if Prefix can handle both name and path prefix
66
+ @path_prefix = Prefix.new(prefix)
67
+ @name_prefix = Prefix.new("")
68
+ @url_helpers = UrlHelpers.new(base_url)
69
+ @resolver = resolver
70
+ @not_found = not_found
71
+ @block_context = block_context
72
+ @fixed = {}
73
+ @variable = {}
74
+ @globbed = {}
75
+ @mounted = {}
76
+ instance_eval(&blk) if blk
240
77
  end
241
78
 
242
- # Returns self
79
+ # Resolve the given Rack env to a registered endpoint and invokes it.
243
80
  #
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.
81
+ # @param env [Hash] a Rack env
247
82
  #
248
- # @return [self]
83
+ # @return [Array] a finalized Rack env response
249
84
  #
250
- # @since 0.2.0
251
- # @api private
252
- def routes
253
- self
85
+ # @since 0.1.0
86
+ def call(env)
87
+ endpoint, params = lookup(env)
88
+
89
+ unless endpoint
90
+ return not_allowed(env) ||
91
+ not_found(env)
92
+ end
93
+
94
+ endpoint.call(
95
+ _params(env, params)
96
+ ).to_a
254
97
  end
255
98
 
256
- # To support defining routes in the `define` wrapper.
257
- #
258
- # @param blk [Proc] the block to define the routes
99
+ # Defines a named root route (a GET route for "/")
259
100
  #
260
- # @return [Hanami::Routing::Route]
101
+ # @param to [#call] the Rack endpoint
102
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
261
103
  #
262
- # @since 0.2.0
104
+ # @since 0.7.0
263
105
  #
264
- # @example In Hanami framework
265
- # class Application < Hanami::Application
266
- # configure do
267
- # routes 'config/routes'
268
- # end
269
- # end
106
+ # @see #get
107
+ # @see #path
108
+ # @see #url
270
109
  #
271
- # # In `config/routes`
110
+ # @example Proc endpoint
111
+ # require "hanami/router"
272
112
  #
273
- # define do
274
- # get # ...
113
+ # router = Hanami::Router.new do
114
+ # root to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
275
115
  # end
276
- def define(&blk)
277
- instance_eval(&blk) if block_given?
278
- end
279
-
280
- # Check if there are defined routes
281
116
  #
282
- # @return [TrueClass,FalseClass] the result of the check
117
+ # @example Block endpoint
118
+ # require "hanami/router"
283
119
  #
284
- # @since 0.2.0
285
- # @api private
120
+ # router = Hanami::Router.new do
121
+ # root do
122
+ # "Hello from Hanami!"
123
+ # end
124
+ # end
286
125
  #
287
- # @example
126
+ # @example URL helpers
127
+ # require "hanami/router"
288
128
  #
289
- # router = Hanami::Router.new
290
- # router.defined? # => false
129
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
130
+ # root do
131
+ # "Hello from Hanami!"
132
+ # end
133
+ # end
291
134
  #
292
- # router = Hanami::Router.new { get '/', to: ->(env) { } }
293
- # router.defined? # => true
294
- def defined?
295
- @router.routes.any?
135
+ # router.path(:root) # => "/"
136
+ # router.url(:root) # => "https://hanamirb.org"
137
+ def root(to: nil, &blk)
138
+ get("/", to: to, as: :root, &blk)
296
139
  end
297
140
 
298
- # Defines a route that accepts a GET request for the given path.
141
+ # Defines a route that accepts GET requests for the given path.
142
+ # It also defines a route to accept HEAD requests.
299
143
  #
300
144
  # @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
- #
145
+ # @param to [#call] the Rack endpoint
146
+ # @param as [Symbol] a unique name for the route
147
+ # @param constraints [Hash] a set of constraints for path variables
305
148
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
306
149
  #
307
- # @return [Hanami::Routing::Route] this may vary according to the :route
308
- # option passed to the constructor
309
- #
310
150
  # @since 0.1.0
311
151
  #
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
- # }
152
+ # @see #initialize
153
+ # @see #path
154
+ # @see #url
330
155
  #
331
- # @example Variables Constraints
332
- # require 'hanami/router'
156
+ # @example Proc endpoint
157
+ # require "hanami/router"
333
158
  #
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'
354
- #
355
- # router = Hanami::Router.new
356
- # router.get '/hanami(.:format)',
357
- # to: ->(env) {
358
- # [200, {}, ["You've requested #{ env['router.params'][:format] }!"]]
359
- # }
360
- #
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
159
+ # Hanami::Router.new do
160
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
391
161
  # end
392
162
  #
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'
163
+ # @example Block endpoint
164
+ # require "hanami/router"
398
165
  #
399
- # module Flowers
400
- # class Index
401
- # def call(env)
402
- # # ...
403
- # end
166
+ # Hanami::Router.new do
167
+ # get "/" do
168
+ # "OK"
404
169
  # end
405
170
  # end
406
171
  #
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
172
+ # @example Named route
173
+ # require "hanami/router"
422
174
  #
423
- # @param blk [Proc] the anonymous proc to be used as endpoint for the route
175
+ # router = Hanami::Router.new do
176
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }, as: :welcome
177
+ # end
424
178
  #
425
- # @return [Hanami::Routing::Route] this may vary according to the :route
426
- # option passed to the constructor
179
+ # router.path(:welcome) # => "/"
180
+ # router.url(:welcome) # => "http://localhost/"
427
181
  #
428
- # @see Hanami::Router#get
182
+ # @example Constraints
183
+ # require "hanami/router"
429
184
  #
430
- # @since 0.1.0
431
- def post(path, options = {}, &blk)
432
- @router.post(path, options, &blk)
185
+ # Hanami::Router.new do
186
+ # get "/users/:id", to: ->(*) { [200, {}, ["OK"]] }, id: /\d+/
187
+ # end
188
+ def get(path, to: nil, as: nil, **constraints, &blk)
189
+ add_route("GET", path, to, as, constraints, &blk)
190
+ add_route("HEAD", path, to, as, constraints, &blk)
433
191
  end
434
192
 
435
- # Defines a route that accepts a PUT request for the given path.
193
+ # Defines a route that accepts POST requests for the given path.
436
194
  #
437
195
  # @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
- #
196
+ # @param to [#call] the Rack endpoint
197
+ # @param as [Symbol] a unique name for the route
198
+ # @param constraints [Hash] a set of constraints for path variables
442
199
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
443
200
  #
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
201
  # @since 0.1.0
450
- def put(path, options = {}, &blk)
451
- @router.put(path, options, &blk)
202
+ #
203
+ # @see #get
204
+ # @see #initialize
205
+ # @see #path
206
+ # @see #url
207
+ def post(path, to: nil, as: nil, **constraints, &blk)
208
+ add_route("POST", path, to, as, constraints, &blk)
452
209
  end
453
210
 
454
- # Defines a route that accepts a PATCH request for the given path.
211
+ # Defines a route that accepts PATCH requests for the given path.
455
212
  #
456
213
  # @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
- #
214
+ # @param to [#call] the Rack endpoint
215
+ # @param as [Symbol] a unique name for the route
216
+ # @param constraints [Hash] a set of constraints for path variables
461
217
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
462
218
  #
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
219
  # @since 0.1.0
469
- def patch(path, options = {}, &blk)
470
- @router.patch(path, options, &blk)
220
+ #
221
+ # @see #get
222
+ # @see #initialize
223
+ # @see #path
224
+ # @see #url
225
+ def patch(path, to: nil, as: nil, **constraints, &blk)
226
+ add_route("PATCH", path, to, as, constraints, &blk)
471
227
  end
472
228
 
473
- # Defines a route that accepts a DELETE request for the given path.
229
+ # Defines a route that accepts PUT requests for the given path.
474
230
  #
475
231
  # @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
- #
232
+ # @param to [#call] the Rack endpoint
233
+ # @param as [Symbol] a unique name for the route
234
+ # @param constraints [Hash] a set of constraints for path variables
480
235
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
481
236
  #
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
237
  # @since 0.1.0
488
- def delete(path, options = {}, &blk)
489
- @router.delete(path, options, &blk)
238
+ #
239
+ # @see #get
240
+ # @see #initialize
241
+ # @see #path
242
+ # @see #url
243
+ def put(path, to: nil, as: nil, **constraints, &blk)
244
+ add_route("PUT", path, to, as, constraints, &blk)
490
245
  end
491
246
 
492
- # Defines a route that accepts a TRACE request for the given path.
247
+ # Defines a route that accepts DELETE requests for the given path.
493
248
  #
494
249
  # @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
- #
250
+ # @param to [#call] the Rack endpoint
251
+ # @param as [Symbol] a unique name for the route
252
+ # @param constraints [Hash] a set of constraints for path variables
499
253
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
500
254
  #
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
255
  # @since 0.1.0
507
- def trace(path, options = {}, &blk)
508
- @router.trace(path, options, &blk)
256
+ #
257
+ # @see #get
258
+ # @see #initialize
259
+ # @see #path
260
+ # @see #url
261
+ def delete(path, to: nil, as: nil, **constraints, &blk)
262
+ add_route("DELETE", path, to, as, constraints, &blk)
509
263
  end
510
264
 
511
- # Defines a route that accepts a LINK request for the given path.
265
+ # Defines a route that accepts TRACE requests for the given path.
512
266
  #
513
267
  # @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
- #
268
+ # @param to [#call] the Rack endpoint
269
+ # @param as [Symbol] a unique name for the route
270
+ # @param constraints [Hash] a set of constraints for path variables
518
271
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
519
272
  #
520
- # @return [Hanami::Routing::Route] this may vary according to the :route
521
- # option passed to the constructor
522
- #
523
- # @see Hanami::Router#get
273
+ # @since 0.1.0
524
274
  #
525
- # @since 0.8.0
526
- def link(path, options = {}, &blk)
527
- @router.link(path, options, &blk)
275
+ # @see #get
276
+ # @see #initialize
277
+ # @see #path
278
+ # @see #url
279
+ def trace(path, to: nil, as: nil, **constraints, &blk)
280
+ add_route("TRACE", path, to, as, constraints, &blk)
528
281
  end
529
282
 
530
- # Defines a route that accepts an UNLINK request for the given path.
283
+ # Defines a route that accepts OPTIONS requests for the given path.
531
284
  #
532
285
  # @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
- #
286
+ # @param to [#call] the Rack endpoint
287
+ # @param as [Symbol] a unique name for the route
288
+ # @param constraints [Hash] a set of constraints for path variables
537
289
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
538
290
  #
539
- # @return [Hanami::Routing::Route] this may vary according to the :route
540
- # option passed to the constructor
541
- #
542
- # @see Hanami::Router#get
291
+ # @since 0.1.0
543
292
  #
544
- # @since 0.8.0
545
- def unlink(path, options = {}, &blk)
546
- @router.unlink(path, options, &blk)
293
+ # @see #get
294
+ # @see #initialize
295
+ # @see #path
296
+ # @see #url
297
+ def options(path, to: nil, as: nil, **constraints, &blk)
298
+ add_route("OPTIONS", path, to, as, constraints, &blk)
547
299
  end
548
300
 
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
301
+ # Defines a route that accepts LINK requests for the given path.
553
302
  #
303
+ # @param path [String] the relative URL to be matched
304
+ # @param to [#call] the Rack endpoint
305
+ # @param as [Symbol] a unique name for the route
306
+ # @param constraints [Hash] a set of constraints for path variables
554
307
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
555
308
  #
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!']] }
309
+ # @since 0.1.0
572
310
  #
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)
311
+ # @see #get
312
+ # @see #initialize
313
+ # @see #path
314
+ # @see #url
315
+ def link(path, to: nil, as: nil, **constraints, &blk)
316
+ add_route("LINK", path, to, as, constraints, &blk)
577
317
  end
578
318
 
579
- # Defines a route that accepts a OPTIONS request for the given path.
319
+ # Defines a route that accepts UNLINK requests for the given path.
580
320
  #
581
321
  # @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
- #
322
+ # @param to [#call] the Rack endpoint
323
+ # @param as [Symbol] a unique name for the route
324
+ # @param constraints [Hash] a set of constraints for path variables
586
325
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
587
326
  #
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
327
  # @since 0.1.0
608
328
  #
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
329
+ # @see #get
330
+ # @see #initialize
331
+ # @see #path
332
+ # @see #url
333
+ def unlink(path, to: nil, as: nil, **constraints, &blk)
334
+ add_route("UNLINK", path, to, as, constraints, &blk)
629
335
  end
630
336
 
631
- # Defines a Ruby block: all the routes defined within it will be namespaced
632
- # with the given relative path.
633
- #
634
- # Namespaces blocks can be nested multiple times.
337
+ # Defines a route that redirects the incoming request to another path.
635
338
  #
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.
339
+ # @param path [String] the relative URL to be matched
340
+ # @param to [#call] the Rack endpoint
341
+ # @param as [Symbol] a unique name for the route
342
+ # @param code [Integer] a HTTP status code to use for the redirect
641
343
  #
642
344
  # @since 0.1.0
643
345
  #
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)
346
+ # @see #get
347
+ # @see #initialize
348
+ def redirect(path, to: nil, as: nil, code: DEFAULT_REDIRECT_CODE)
349
+ get(path, to: _redirect(to, code), as: as)
675
350
  end
676
351
 
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
691
- #
692
- # @see Hanami::Routing::Resource
693
- # @see Hanami::Routing::Resource::Action
694
- # @see Hanami::Routing::Resource::Options
695
- #
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
- # # +--------+----------------+-------------------+----------+----------------+
715
- #
716
- #
717
- #
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
352
+ # Defines a routing scope. Routes defined in the context of a scope,
353
+ # inherit the given path as path prefix and as a named routes prefix.
743
354
  #
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
- # # +--------+----------------+------------------+----------+----------------+
355
+ # @param path [String] the scope path to be used as a path prefix
356
+ # @param blk [Proc] the routes definitions withing the scope
753
357
  #
358
+ # @since 2.0.0
754
359
  #
360
+ # @see #path
755
361
  #
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'
362
+ # @example
363
+ # require "hanami/router"
779
364
  #
780
- # Hanami::Router.new do
781
- # resource 'identity', only: [] do
782
- # collection do
783
- # get 'keys'
784
- # end
365
+ # router = Hanami::Router.new do
366
+ # scope "v1" do
367
+ # get "/users", to: ->(*) { ... }, as: :users
785
368
  # end
786
369
  # end
787
370
  #
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
371
+ # router.path(:v1_users) # => "/v1/users"
372
+ def scope(path, &blk)
373
+ path_prefix = @path_prefix
374
+ name_prefix = @name_prefix
798
375
 
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)
376
+ begin
377
+ @path_prefix = @path_prefix.join(path.to_s)
378
+ @name_prefix = @name_prefix.join(path.to_s)
379
+ instance_eval(&blk)
380
+ ensure
381
+ @path_prefix = path_prefix
382
+ @name_prefix = name_prefix
383
+ end
920
384
  end
921
385
 
922
386
  # Mount a Rack application at the specified path.
923
387
  # All the requests starting with the specified path, will be forwarded to
924
388
  # the given application.
925
389
  #
926
- # All the other methods (eg #get) support callable objects, but they
390
+ # All the other methods (eg `#get`) support callable objects, but they
927
391
  # restrict the range of the acceptable HTTP verb. Mounting an application
928
392
  # with #mount doesn't apply this kind of restriction at the router level,
929
393
  # but let the application to decide.
930
394
  #
931
395
  # @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
396
+ # @param at [String] the relative path where to mount the app
397
+ # @param constraints [Hash] a set of constraints for path variables
934
398
  #
935
399
  # @since 0.1.1
936
400
  #
937
- # @example Basic usage
938
- # require 'hanami/router'
401
+ # @example
402
+ # require "hanami/router"
939
403
  #
940
404
  # Hanami::Router.new do
941
- # mount Api::App.new, at: '/api'
405
+ # mount MyRackApp.new, at: "/foo"
942
406
  # end
407
+ def mount(app, at:, **constraints)
408
+ path = prefixed_path(at)
409
+ prefix = Segment.fabricate(path, **constraints)
410
+ @mounted[prefix] = @resolver.call(path, app)
411
+ end
412
+
413
+ # Generate an relative URL for a specified named route.
414
+ # The additional arguments will be used to compose the relative URL - in
415
+ # case it has tokens to match - and for compose the query string.
943
416
  #
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'
417
+ # @param name [Symbol] the route name
953
418
  #
954
- # Hanami::Router.new do
955
- # get '/rack1', to: RackOne.new
956
- # mount RackTwo.new, at: '/rack2'
957
- # end
419
+ # @return [String]
958
420
  #
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
421
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
422
+ # recognize a route, because of the given arguments.
976
423
  #
977
- # class RackTwo
978
- # def call(env)
979
- # end
980
- # end
424
+ # @since 0.1.0
981
425
  #
982
- # class RackThree
983
- # def call(env)
984
- # end
985
- # end
426
+ # @see #url
986
427
  #
987
- # module Dashboard
988
- # class Index
989
- # def call(env)
990
- # end
991
- # end
992
- # end
428
+ # @example
429
+ # require "hanami/router"
993
430
  #
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'
431
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
432
+ # get "/login", to: ->(*) { ... }, as: :login
433
+ # get "/:name", to: ->(*) { ... }, as: :framework
1000
434
  # end
1001
435
  #
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)
436
+ # router.path(:login) # => "/login"
437
+ # router.path(:login, return_to: "/dashboard") # => "/login?return_to=%2Fdashboard"
438
+ # router.path(:framework, name: "router") # => "/router"
439
+ def path(name, variables = {})
440
+ @url_helpers.path(name, variables)
1009
441
  end
1010
442
 
1011
- # Resolve the given Rack env to a registered endpoint and invoke it.
443
+ # Generate an absolute URL for a specified named route.
444
+ # The additional arguments will be used to compose the relative URL - in
445
+ # case it has tokens to match - and for compose the query string.
1012
446
  #
1013
- # @param env [Hash] a Rack env instance
447
+ # @param name [Symbol] the route name
1014
448
  #
1015
- # @return [Rack::Response, Array]
449
+ # @return [String]
450
+ #
451
+ # @raise [Hanami::Routing::InvalidRouteException] when the router fails to
452
+ # recognize a route, because of the given arguments.
1016
453
  #
1017
454
  # @since 0.1.0
1018
- def call(env)
1019
- @router.call(env)
455
+ #
456
+ # @see #path
457
+ #
458
+ # @example
459
+ # require "hanami/router"
460
+ #
461
+ # router = Hanami::Router.new(base_url: "https://hanamirb.org") do
462
+ # get "/login", to: ->(*) { ... }, as: :login
463
+ # get "/:name", to: ->(*) { ... }, as: :framework
464
+ # end
465
+ #
466
+ # router.url(:login) # => "https://hanamirb.org/login"
467
+ # router.url(:login, return_to: "/dashboard") # => "https://hanamirb.org/login?return_to=%2Fdashboard"
468
+ # router.url(:framework, name: "router") # => "https://hanamirb.org/router"
469
+ def url(name, variables = {})
470
+ @url_helpers.url(name, variables)
1020
471
  end
1021
472
 
1022
473
  # Recognize the given env, path, or name and return a route for testing
@@ -1037,34 +488,34 @@ module Hanami
1037
488
  # @see Hanami::Routing::RecognizedRoute
1038
489
  #
1039
490
  # @example Successful Path Recognition
1040
- # require 'hanami/router'
491
+ # require "hanami/router"
1041
492
  #
1042
493
  # router = Hanami::Router.new do
1043
- # get '/books/:id', to: 'books#show', as: :book
494
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1044
495
  # end
1045
496
  #
1046
- # route = router.recognize('/books/23')
497
+ # route = router.recognize("/books/23")
1047
498
  # route.verb # => "GET" (default)
1048
499
  # route.routable? # => true
1049
500
  # route.params # => {:id=>"23"}
1050
501
  #
1051
502
  # @example Successful Rack Env Recognition
1052
- # require 'hanami/router'
503
+ # require "hanami/router"
1053
504
  #
1054
505
  # router = Hanami::Router.new do
1055
- # get '/books/:id', to: 'books#show', as: :book
506
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1056
507
  # end
1057
508
  #
1058
- # route = router.recognize(Rack::MockRequest.env_for('/books/23'))
509
+ # route = router.recognize(Rack::MockRequest.env_for("/books/23"))
1059
510
  # route.verb # => "GET" (default)
1060
511
  # route.routable? # => true
1061
512
  # route.params # => {:id=>"23"}
1062
513
  #
1063
514
  # @example Successful Named Route Recognition
1064
- # require 'hanami/router'
515
+ # require "hanami/router"
1065
516
  #
1066
517
  # router = Hanami::Router.new do
1067
- # get '/books/:id', to: 'books#show', as: :book
518
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1068
519
  # end
1069
520
  #
1070
521
  # route = router.recognize(:book, id: 23)
@@ -1073,43 +524,43 @@ module Hanami
1073
524
  # route.params # => {:id=>"23"}
1074
525
  #
1075
526
  # @example Failing Recognition For Unknown Path
1076
- # require 'hanami/router'
527
+ # require "hanami/router"
1077
528
  #
1078
529
  # router = Hanami::Router.new do
1079
- # get '/books/:id', to: 'books#show', as: :book
530
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1080
531
  # end
1081
532
  #
1082
- # route = router.recognize('/books')
533
+ # route = router.recognize("/books")
1083
534
  # route.verb # => "GET" (default)
1084
535
  # route.routable? # => false
1085
536
  #
1086
537
  # @example Failing Recognition For Path With Wrong HTTP Verb
1087
- # require 'hanami/router'
538
+ # require "hanami/router"
1088
539
  #
1089
540
  # router = Hanami::Router.new do
1090
- # get '/books/:id', to: 'books#show', as: :book
541
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1091
542
  # end
1092
543
  #
1093
- # route = router.recognize('/books/23', method: :post)
544
+ # route = router.recognize("/books/23", method: :post)
1094
545
  # route.verb # => "POST"
1095
546
  # route.routable? # => false
1096
547
  #
1097
548
  # @example Failing Recognition For Rack Env With Wrong HTTP Verb
1098
- # require 'hanami/router'
549
+ # require "hanami/router"
1099
550
  #
1100
551
  # router = Hanami::Router.new do
1101
- # get '/books/:id', to: 'books#show', as: :book
552
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1102
553
  # end
1103
554
  #
1104
- # route = router.recognize(Rack::MockRequest.env_for('/books/23', method: :post))
555
+ # route = router.recognize(Rack::MockRequest.env_for("/books/23", method: :post))
1105
556
  # route.verb # => "POST"
1106
557
  # route.routable? # => false
1107
558
  #
1108
559
  # @example Failing Recognition Named Route With Wrong Params
1109
- # require 'hanami/router'
560
+ # require "hanami/router"
1110
561
  #
1111
562
  # router = Hanami::Router.new do
1112
- # get '/books/:id', to: 'books#show', as: :book
563
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1113
564
  # end
1114
565
  #
1115
566
  # route = router.recognize(:book)
@@ -1117,105 +568,79 @@ module Hanami
1117
568
  # route.routable? # => false
1118
569
  #
1119
570
  # @example Failing Recognition Named Route With Wrong HTTP Verb
1120
- # require 'hanami/router'
571
+ # require "hanami/router"
1121
572
  #
1122
573
  # router = Hanami::Router.new do
1123
- # get '/books/:id', to: 'books#show', as: :book
574
+ # get "/books/:id", to: ->(*) { ... }, as: :book
1124
575
  # end
1125
576
  #
1126
577
  # route = router.recognize(:book, {method: :post}, {id: 1})
1127
578
  # route.verb # => "POST"
1128
579
  # route.routable? # => false
1129
580
  # route.params # => {:id=>"1"}
1130
- def recognize(env, options = {}, params = nil)
1131
- require 'hanami/routing/recognized_route'
581
+ def recognize(env, params = {}, options = {})
582
+ require "hanami/router/recognized_route"
583
+ env = env_for(env, params, options)
584
+ endpoint, params = lookup(env)
585
+
586
+ RecognizedRoute.new(
587
+ endpoint, _params(env, params)
588
+ )
589
+ end
1132
590
 
1133
- env = env_for(env, options, params)
1134
- responses, _ = *@router.recognize(env)
591
+ # @since 2.0.0
592
+ # @api private
593
+ def fixed(env)
594
+ @fixed.dig(env["REQUEST_METHOD"], env["PATH_INFO"])
595
+ end
1135
596
 
1136
- Routing::RecognizedRoute.new(
1137
- responses.nil? ? responses : responses.first,
1138
- env, @router)
597
+ # @since 2.0.0
598
+ # @api private
599
+ def variable(env)
600
+ @variable[env["REQUEST_METHOD"]]&.find(env["PATH_INFO"])
1139
601
  end
1140
602
 
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]
1148
- #
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
1153
- #
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)
603
+ # @since 2.0.0
604
+ # @api private
605
+ def globbed(env)
606
+ @globbed[env["REQUEST_METHOD"]]&.each do |path, to|
607
+ if (match = path.match(env["PATH_INFO"]))
608
+ return [to, match.named_captures]
609
+ end
610
+ end
611
+
612
+ nil
1166
613
  end
1167
614
 
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)
615
+ # @since 2.0.0
616
+ # @api private
617
+ def mounted(env)
618
+ @mounted.each do |prefix, app|
619
+ next unless (match = prefix.peek_match(env["PATH_INFO"]))
620
+
621
+ # TODO: ensure compatibility with existing env["SCRIPT_NAME"]
622
+ # TODO: cleanup this code
623
+ env["SCRIPT_NAME"] = env["SCRIPT_NAME"].to_s + prefix.to_s
624
+ env["PATH_INFO"] = env["PATH_INFO"].sub(prefix.to_s, "")
625
+ env["PATH_INFO"] = "/" if env["PATH_INFO"] == ""
626
+
627
+ return [app, match.named_captures]
628
+ end
629
+
630
+ nil
1193
631
  end
1194
632
 
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)
633
+ # @since 2.0.0
634
+ # @api private
635
+ def not_allowed(env)
636
+ (_not_allowed_fixed(env) ||
637
+ _not_allowed_variable(env)) and return [405, {"Content-Length" => "11"}, ["Not Allowed"]]
638
+ end
639
+
640
+ # @since 2.0.0
641
+ # @api private
642
+ def not_found(env)
643
+ @not_found.call(env)
1219
644
  end
1220
645
 
1221
646
  protected
@@ -1233,20 +658,186 @@ module Hanami
1233
658
  #
1234
659
  # @see Hanami::Router#recognize
1235
660
  # @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
661
+ def env_for(env, params = {}, options = {})
662
+ require "rack/mock"
663
+
664
+ case env
665
+ when ::String
666
+ ::Rack::MockRequest.env_for(env, options)
667
+ when ::Symbol
1241
668
  begin
1242
- url = path(env, params || options)
1243
- return env_for(url, options)
1244
- rescue Hanami::Routing::InvalidRouteException
1245
- {}
669
+ url = path(env, params)
670
+ return env_for(url, params, options) # rubocop:disable Style/RedundantReturn
671
+ rescue Hanami::Router::InvalidRouteException
672
+ {} # Empty Rack env
1246
673
  end
1247
674
  else
1248
675
  env
1249
676
  end
1250
677
  end
678
+
679
+ private
680
+
681
+ # @since 2.0.0
682
+ # @api private
683
+ DEFAULT_BASE_URL = "http://localhost"
684
+
685
+ # @since 2.0.0
686
+ # @api private
687
+ DEFAULT_PREFIX = "/"
688
+
689
+ # @since 2.0.0
690
+ # @api private
691
+ DEFAULT_RESOLVER = ->(_, to) { to }
692
+
693
+ # @since 2.0.0
694
+ # @api private
695
+ DEFAULT_REDIRECT_CODE = 301
696
+
697
+ # @since 2.0.0
698
+ # @api private
699
+ PARAMS = "router.params"
700
+
701
+ # Default response when no route was matched
702
+ #
703
+ # @api private
704
+ # @since 2.0.0
705
+ NOT_FOUND = ->(*) { [404, {"Content-Length" => "9"}, ["Not Found"]] }.freeze
706
+
707
+ # @since 2.0.0
708
+ # @api private
709
+ def lookup(env)
710
+ endpoint = fixed(env)
711
+ return [endpoint, {}] if endpoint
712
+
713
+ variable(env) || globbed(env) || mounted(env)
714
+ end
715
+
716
+ # @since 2.0.0
717
+ # @api private
718
+ def add_route(http_method, path, to, as, constraints, &blk)
719
+ path = prefixed_path(path)
720
+ to = resolve_endpoint(path, to, blk)
721
+
722
+ if globbed?(path)
723
+ add_globbed_route(http_method, path, to, constraints)
724
+ elsif variable?(path)
725
+ add_variable_route(http_method, path, to, constraints)
726
+ else
727
+ add_fixed_route(http_method, path, to)
728
+ end
729
+
730
+ add_named_route(path, as, constraints) if as
731
+ end
732
+
733
+ # @since 2.0.0
734
+ # @api private
735
+ def resolve_endpoint(path, to, blk)
736
+ (to || blk) or raise MissingEndpointError.new(path)
737
+ to = Block.new(@block_context, blk) if to.nil?
738
+
739
+ @resolver.call(path, to)
740
+ end
741
+
742
+ # @since 2.0.0
743
+ # @api private
744
+ def add_globbed_route(http_method, path, to, constraints)
745
+ @globbed[http_method] ||= []
746
+ @globbed[http_method] << [Segment.fabricate(path, **constraints), to]
747
+ end
748
+
749
+ # @since 2.0.0
750
+ # @api private
751
+ def add_variable_route(http_method, path, to, constraints)
752
+ @variable[http_method] ||= Trie.new
753
+ @variable[http_method].add(path, to, constraints)
754
+ end
755
+
756
+ # @since 2.0.0
757
+ # @api private
758
+ def add_fixed_route(http_method, path, to)
759
+ @fixed[http_method] ||= {}
760
+ @fixed[http_method][path] = to
761
+ end
762
+
763
+ # @since 2.0.0
764
+ # @api private
765
+ def add_named_route(path, as, constraints)
766
+ @url_helpers.add(prefixed_name(as), Segment.fabricate(path, **constraints))
767
+ end
768
+
769
+ # @since 2.0.0
770
+ # @api private
771
+ def variable?(path)
772
+ /:/.match?(path)
773
+ end
774
+
775
+ # @since 2.0.0
776
+ # @api private
777
+ def globbed?(path)
778
+ /\*/.match?(path)
779
+ end
780
+
781
+ # @since 2.0.0
782
+ # @api private
783
+ def prefixed_path(path)
784
+ @path_prefix.join(path).to_s
785
+ end
786
+
787
+ # @since 2.0.0
788
+ # @api private
789
+ def prefixed_name(name)
790
+ @name_prefix.relative_join(name, "_").to_sym
791
+ end
792
+
793
+ # @since 2.0.0
794
+ # @api private
795
+ def _redirect(to, code)
796
+ body = Rack::Utils::HTTP_STATUS_CODES.fetch(code) do
797
+ raise UnknownHTTPStatusCodeError.new(code)
798
+ end
799
+
800
+ destination = prefixed_path(to)
801
+ Redirect.new(destination, ->(*) { [code, {"Location" => destination}, [body]] })
802
+ end
803
+
804
+ # @since 2.0.0
805
+ # @api private
806
+ def _params(env, params)
807
+ params ||= {}
808
+ env[PARAMS] ||= {}
809
+ env[PARAMS].merge!(Rack::Utils.parse_nested_query(env["QUERY_STRING"]))
810
+ env[PARAMS].merge!(params)
811
+ env[PARAMS] = Params.deep_symbolize(env[PARAMS])
812
+ env
813
+ end
814
+
815
+ # @since 2.0.0
816
+ # @api private
817
+ def _not_allowed_fixed(env)
818
+ found = false
819
+
820
+ @fixed.each_value do |routes|
821
+ break if found
822
+
823
+ found = routes.key?(env["PATH_INFO"])
824
+ end
825
+
826
+ found
827
+ end
828
+
829
+ # @since 2.0.0
830
+ # @api private
831
+ def _not_allowed_variable(env)
832
+ found = false
833
+
834
+ @variable.each_value do |routes|
835
+ break if found
836
+
837
+ found = routes.find(env["PATH_INFO"])
838
+ end
839
+
840
+ found
841
+ end
1251
842
  end
1252
843
  end