hanami-router 1.3.1 → 2.0.0.alpha4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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