hanami-router 1.3.2 → 2.0.0.alpha5

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