hanami-router 1.3.2 → 2.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -3
  3. data/README.md +192 -154
  4. data/hanami-router.gemspec +23 -20
  5. data/lib/hanami/middleware/body_parser.rb +17 -13
  6. data/lib/hanami/middleware/body_parser/class_interface.rb +56 -56
  7. data/lib/hanami/middleware/body_parser/errors.rb +7 -4
  8. data/lib/hanami/middleware/body_parser/json_parser.rb +5 -3
  9. data/lib/hanami/middleware/error.rb +16 -0
  10. data/lib/hanami/router.rb +262 -149
  11. data/lib/hanami/router/version.rb +3 -1
  12. data/lib/hanami/routing.rb +193 -0
  13. data/lib/hanami/routing/endpoint.rb +122 -104
  14. data/lib/hanami/routing/endpoint_resolver.rb +20 -16
  15. data/lib/hanami/routing/prefix.rb +102 -0
  16. data/lib/hanami/routing/recognized_route.rb +40 -26
  17. data/lib/hanami/routing/resource.rb +9 -7
  18. data/lib/hanami/routing/resource/action.rb +58 -33
  19. data/lib/hanami/routing/resource/nested.rb +4 -1
  20. data/lib/hanami/routing/resource/options.rb +3 -1
  21. data/lib/hanami/routing/resources.rb +6 -4
  22. data/lib/hanami/routing/resources/action.rb +11 -6
  23. data/lib/hanami/routing/routes_inspector.rb +22 -20
  24. data/lib/hanami/routing/scope.rb +112 -0
  25. metadata +47 -25
  26. data/lib/hanami-router.rb +0 -1
  27. data/lib/hanami/routing/error.rb +0 -7
  28. data/lib/hanami/routing/force_ssl.rb +0 -212
  29. data/lib/hanami/routing/http_router.rb +0 -220
  30. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  31. data/lib/hanami/routing/namespace.rb +0 -98
  32. data/lib/hanami/routing/parsers.rb +0 -113
  33. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  34. data/lib/hanami/routing/parsing/parser.rb +0 -61
  35. data/lib/hanami/routing/route.rb +0 -71
@@ -1,30 +1,33 @@
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
- spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-router.gemspec`.split($/)
17
+ spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-router.gemspec`.split($INPUT_RECORD_SEPARATOR)
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.5.0"
21
22
 
22
- spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'http_router', '0.11.2'
24
- spec.add_dependency 'hanami-utils', '~> 1.3'
23
+ spec.add_dependency "rack", "~> 2.0"
24
+ spec.add_dependency "mustermann", "~> 1.0"
25
+ spec.add_dependency "mustermann-contrib", "~> 1.0"
26
+ spec.add_dependency "hanami-utils", "~> 2.0.alpha"
27
+ spec.add_dependency "dry-inflector", "~> 0.1"
25
28
 
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'
29
+ spec.add_development_dependency "bundler", ">= 1.6", "< 3"
30
+ spec.add_development_dependency "rake", "~> 12"
31
+ spec.add_development_dependency "rack-test", "~> 1.0"
32
+ spec.add_development_dependency "rspec", "~> 3.7"
30
33
  end
@@ -1,5 +1,8 @@
1
- require 'hanami/utils/hash'
2
- require_relative 'body_parser/class_interface'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/hash"
4
+ require "hanami/middleware/error"
5
+ require_relative "body_parser/class_interface"
3
6
 
4
7
  module Hanami
5
8
  module Middleware
@@ -8,7 +11,7 @@ module Hanami
8
11
  class BodyParser
9
12
  # @since 1.3.0
10
13
  # @api private
11
- CONTENT_TYPE = 'CONTENT_TYPE'.freeze
14
+ CONTENT_TYPE = "CONTENT_TYPE"
12
15
 
13
16
  # @since 1.3.0
14
17
  # @api private
@@ -16,17 +19,17 @@ module Hanami
16
19
 
17
20
  # @since 1.3.0
18
21
  # @api private
19
- RACK_INPUT = 'rack.input'.freeze
22
+ RACK_INPUT = "rack.input"
20
23
 
21
24
  # @since 1.3.0
22
25
  # @api private
23
- ROUTER_PARAMS = 'router.params'.freeze
26
+ ROUTER_PARAMS = "router.params"
24
27
 
25
28
  # @api private
26
- ROUTER_PARSED_BODY = 'router.parsed_body'.freeze
29
+ ROUTER_PARSED_BODY = "router.parsed_body"
27
30
 
28
31
  # @api private
29
- FALLBACK_KEY = '_'.freeze
32
+ FALLBACK_KEY = "_"
30
33
 
31
34
  extend ClassInterface
32
35
 
@@ -55,18 +58,18 @@ module Hanami
55
58
  parser_names = Array(parser_names)
56
59
  return {} if parser_names.empty?
57
60
 
58
- parser_names.each_with_object({}) { |name, parsers|
61
+ parser_names.each_with_object({}) do |name, parsers|
59
62
  parser = self.class.for(name)
60
63
 
61
64
  parser.mime_types.each do |mime|
62
65
  parsers[mime] = parser
63
66
  end
64
- }
67
+ end
65
68
  end
66
69
 
67
70
  # @api private
68
71
  def _symbolize(body)
69
- if body.is_a?(Hash)
72
+ if body.is_a?(::Hash)
70
73
  Utils::Hash.deep_symbolize(body)
71
74
  else
72
75
  { FALLBACK_KEY => body }
@@ -82,9 +85,10 @@ module Hanami
82
85
 
83
86
  # @api private
84
87
  def media_type(env)
85
- if ct = content_type(env)
86
- ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
87
- end
88
+ ct = content_type(env)
89
+ return unless ct
90
+
91
+ ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
88
92
  end
89
93
 
90
94
  # @api private
@@ -1,56 +1,56 @@
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 "hanami/utils/class"
4
+ require "hanami/utils/string"
5
+ require_relative "errors"
6
+
7
+ module Hanami
8
+ module Middleware
9
+ class BodyParser
10
+ # @api private
11
+ # @since 1.3.0
12
+ module ClassInterface
13
+ # @api private
14
+ # @since 1.3.0
15
+ def for(parser) # rubocop:disable Metrics/MethodLength
16
+ parser =
17
+ case parser
18
+ when String, Symbol
19
+ require_parser(parser)
20
+ when Class
21
+ parser.new
22
+ else
23
+ parser
24
+ end
25
+
26
+ ensure_parser parser
27
+
28
+ parser
29
+ end
30
+
31
+ private
32
+
33
+ # @api private
34
+ # @since 1.3.0
35
+ PARSER_METHODS = %i[mime_types parse].freeze
36
+
37
+ # @api private
38
+ # @since 1.3.0
39
+ def ensure_parser(parser)
40
+ raise InvalidParserError.new(parser) unless PARSER_METHODS.all? { |method| parser.respond_to?(method) }
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,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,5 +1,7 @@
1
- require 'hanami/utils/json'
2
- require_relative 'errors'
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/json"
4
+ require_relative "errors"
3
5
 
4
6
  module Hanami
5
7
  module Middleware
@@ -10,7 +12,7 @@ module Hanami
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
@@ -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,9 +1,9 @@
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'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/request"
4
+ require "dry/inflector"
5
+ require "hanami/routing"
6
+ require "hanami/utils/hash"
7
7
 
8
8
  # Hanami
9
9
  #
@@ -75,7 +75,12 @@ module Hanami
75
75
  # end
76
76
  #
77
77
  # # All the requests starting with "/api" will be forwarded to Api::App
78
- class Router
78
+ #
79
+ class Router # rubocop:disable Metrics/ClassLength
80
+ # @since 2.0.0
81
+ # @api private
82
+ attr_reader :inflector
83
+
79
84
  # This error is raised when <tt>#call</tt> is invoked on a non-routable
80
85
  # recognized route.
81
86
  #
@@ -88,15 +93,15 @@ module Hanami
88
93
  class NotRoutableEndpointError < Hanami::Routing::Error
89
94
  # @since 0.5.0
90
95
  # @api private
91
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
96
+ REQUEST_METHOD = "REQUEST_METHOD"
92
97
 
93
98
  # @since 0.5.0
94
99
  # @api private
95
- PATH_INFO = 'PATH_INFO'.freeze
100
+ PATH_INFO = "PATH_INFO"
96
101
 
97
102
  # @since 0.5.0
98
103
  def initialize(env)
99
- super %(Cannot find routable endpoint for #{ env[REQUEST_METHOD] } "#{ env[PATH_INFO] }")
104
+ super %(Cannot find routable endpoint for #{env[REQUEST_METHOD]} "#{env[PATH_INFO]}")
100
105
  end
101
106
  end
102
107
 
@@ -106,7 +111,7 @@ module Hanami
106
111
  # @api private
107
112
  #
108
113
  # @see Hanami::Router#root
109
- ROOT_PATH = '/'.freeze
114
+ ROOT_PATH = "/"
110
115
 
111
116
  # Returns the given block as it is.
112
117
  #
@@ -154,8 +159,8 @@ module Hanami
154
159
  # (defaults to `Hanami::Routing::Route`)
155
160
  # @option options [String] :action_separator the separator between controller
156
161
  # 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
162
+ # @option options [Object, #pluralize, #singularize] :inflector
163
+ # the inflector class (defaults to `Dry::Inflector.new`)
159
164
  #
160
165
  # @param blk [Proc] the optional block to define the routes
161
166
  #
@@ -178,51 +183,53 @@ module Hanami
178
183
  # end
179
184
  #
180
185
  # @example Body parsers
181
- # require 'json'
182
- # require 'hanami/router'
183
186
  #
184
- # # It parses JSON body and makes the attributes available to the params
187
+ # require 'hanami/router'
188
+ # require 'hanami/middleware/body_parser'
185
189
  #
186
- # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
190
+ # app = Hanami::Router.new do
191
+ # patch '/books/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] }
192
+ # end
187
193
  #
188
- # router = Hanami::Router.new(parsers: [:json]) do
189
- # patch '/books/:id', to: endpoint
190
- # end
194
+ # use Hanami::Middleware::BodyParser, :json
195
+ # run app
191
196
  #
192
- # # From the shell
197
+ # # From the shell
193
198
  #
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
+ # curl http://localhost:2300/books/1 \
200
+ # -H "Content-Type: application/json" \
201
+ # -H "Accept: application/json" \
202
+ # -d '{"published":"true"}' \
203
+ # -X PATCH
199
204
  #
200
205
  # # It returns
201
206
  #
202
207
  # [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]
203
208
  #
204
209
  # @example Custom body parser
205
- # require 'hanami/router'
206
210
  #
207
- # class XmlParser
208
- # def mime_types
209
- # ['application/xml', 'text/xml']
210
- # end
211
+ # require 'hanami/router'
212
+ # require 'hanami/middleware/body_parser'
211
213
  #
212
- # # Parse body and return a Hash
213
- # def parse(body)
214
- # # ...
215
- # end
216
- # end
217
214
  #
218
- # # It parses XML body and makes the attributes available to the params
215
+ # class XmlParser < Hanami::Middleware::BodyParser::Parser
216
+ # def mime_types
217
+ # ['application/xml', 'text/xml']
218
+ # end
219
219
  #
220
- # endpoint = ->(env) { [200, {},[env['router.params'].inspect]] }
220
+ # # Parse body and return a Hash
221
+ # def parse(body)
222
+ # # parse xml
223
+ # end
224
+ # end
221
225
  #
222
- # router = Hanami::Router.new(parsers: [XmlParser.new]) do
223
- # patch '/authors/:id', to: endpoint
226
+ # app = Hanami::Router.new do
227
+ # patch '/authors/:id', to: ->(env) { [200, {},[env['router.params'].inspect]] }
224
228
  # end
225
229
  #
230
+ # use Hanami::Middleware::BodyParser, XmlParser
231
+ # run app
232
+ #
226
233
  # # From the shell
227
234
  #
228
235
  # curl http://localhost:2300/authors/1 \
@@ -234,9 +241,29 @@ module Hanami
234
241
  # # It returns
235
242
  #
236
243
  # [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]
237
- def initialize(options = {}, &blk)
238
- @router = Routing::HttpRouter.new(options)
239
- define(&blk)
244
+ #
245
+ # rubocop:disable Metrics/MethodLength
246
+ def initialize(scheme: "http", host: "localhost", port: 80, prefix: "", namespace: nil, configuration: nil, inflector: Dry::Inflector.new, not_found: NOT_FOUND, not_allowed: NOT_ALLOWED, &blk)
247
+ @routes = []
248
+ @named = {}
249
+ @namespace = namespace
250
+ @configuration = configuration
251
+ @base = Routing::Uri.build(scheme: scheme, host: host, port: port)
252
+ @prefix = Utils::PathPrefix.new(prefix)
253
+ @inflector = inflector
254
+ @not_found = not_found
255
+ @not_allowed = not_allowed
256
+ instance_eval(&blk) unless blk.nil?
257
+ freeze
258
+ end
259
+ # rubocop:enable Metrics/MethodLength
260
+
261
+ # Freeze the router
262
+ #
263
+ # @since 2.0.0
264
+ def freeze
265
+ @routes.freeze
266
+ super
240
267
  end
241
268
 
242
269
  # Returns self
@@ -253,30 +280,6 @@ module Hanami
253
280
  self
254
281
  end
255
282
 
256
- # To support defining routes in the `define` wrapper.
257
- #
258
- # @param blk [Proc] the block to define the routes
259
- #
260
- # @return [Hanami::Routing::Route]
261
- #
262
- # @since 0.2.0
263
- #
264
- # @example In Hanami framework
265
- # class Application < Hanami::Application
266
- # configure do
267
- # routes 'config/routes'
268
- # end
269
- # end
270
- #
271
- # # In `config/routes`
272
- #
273
- # define do
274
- # get # ...
275
- # end
276
- def define(&blk)
277
- instance_eval(&blk) if block_given?
278
- end
279
-
280
283
  # Check if there are defined routes
281
284
  #
282
285
  # @return [TrueClass,FalseClass] the result of the check
@@ -292,7 +295,7 @@ module Hanami
292
295
  # router = Hanami::Router.new { get '/', to: ->(env) { } }
293
296
  # router.defined? # => true
294
297
  def defined?
295
- @router.routes.any?
298
+ @routes.any?
296
299
  end
297
300
 
298
301
  # Defines a route that accepts a GET request for the given path.
@@ -409,8 +412,8 @@ module Hanami
409
412
  #
410
413
  # # It will map to Flowers::Index.new, which is the
411
414
  # # Hanami::Controller convention.
412
- def get(path, options = {}, &blk)
413
- @router.get(path, options, &blk)
415
+ def get(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
416
+ add_route(GET, path, to, as, namespace, configuration, constraints, &blk)
414
417
  end
415
418
 
416
419
  # Defines a route that accepts a POST request for the given path.
@@ -428,8 +431,8 @@ module Hanami
428
431
  # @see Hanami::Router#get
429
432
  #
430
433
  # @since 0.1.0
431
- def post(path, options = {}, &blk)
432
- @router.post(path, options, &blk)
434
+ def post(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
435
+ add_route(POST, path, to, as, namespace, configuration, constraints, &blk)
433
436
  end
434
437
 
435
438
  # Defines a route that accepts a PUT request for the given path.
@@ -447,8 +450,8 @@ module Hanami
447
450
  # @see Hanami::Router#get
448
451
  #
449
452
  # @since 0.1.0
450
- def put(path, options = {}, &blk)
451
- @router.put(path, options, &blk)
453
+ def put(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
454
+ add_route(PUT, path, to, as, namespace, configuration, constraints, &blk)
452
455
  end
453
456
 
454
457
  # Defines a route that accepts a PATCH request for the given path.
@@ -466,8 +469,8 @@ module Hanami
466
469
  # @see Hanami::Router#get
467
470
  #
468
471
  # @since 0.1.0
469
- def patch(path, options = {}, &blk)
470
- @router.patch(path, options, &blk)
472
+ def patch(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
473
+ add_route(PATCH, path, to, as, namespace, configuration, constraints, &blk)
471
474
  end
472
475
 
473
476
  # Defines a route that accepts a DELETE request for the given path.
@@ -485,8 +488,8 @@ module Hanami
485
488
  # @see Hanami::Router#get
486
489
  #
487
490
  # @since 0.1.0
488
- def delete(path, options = {}, &blk)
489
- @router.delete(path, options, &blk)
491
+ def delete(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
492
+ add_route(DELETE, path, to, as, namespace, configuration, constraints, &blk)
490
493
  end
491
494
 
492
495
  # Defines a route that accepts a TRACE request for the given path.
@@ -504,8 +507,8 @@ module Hanami
504
507
  # @see Hanami::Router#get
505
508
  #
506
509
  # @since 0.1.0
507
- def trace(path, options = {}, &blk)
508
- @router.trace(path, options, &blk)
510
+ def trace(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
511
+ add_route(TRACE, path, to, as, namespace, configuration, constraints, &blk)
509
512
  end
510
513
 
511
514
  # Defines a route that accepts a LINK request for the given path.
@@ -523,8 +526,8 @@ module Hanami
523
526
  # @see Hanami::Router#get
524
527
  #
525
528
  # @since 0.8.0
526
- def link(path, options = {}, &blk)
527
- @router.link(path, options, &blk)
529
+ def link(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
530
+ add_route(LINK, path, to, as, namespace, configuration, constraints, &blk)
528
531
  end
529
532
 
530
533
  # Defines a route that accepts an UNLINK request for the given path.
@@ -542,8 +545,27 @@ module Hanami
542
545
  # @see Hanami::Router#get
543
546
  #
544
547
  # @since 0.8.0
545
- def unlink(path, options = {}, &blk)
546
- @router.unlink(path, options, &blk)
548
+ def unlink(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
549
+ add_route(UNLINK, path, to, as, namespace, configuration, constraints, &blk)
550
+ end
551
+
552
+ # Defines a route that accepts a OPTIONS request for the given path.
553
+ #
554
+ # @param path [String] the relative URL to be matched
555
+ #
556
+ # @param options [Hash] the options to customize the route
557
+ # @option options [String,Proc,Class,Object#call] :to the endpoint
558
+ #
559
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
560
+ #
561
+ # @return [Hanami::Routing::Route] this may vary according to the :route
562
+ # option passed to the constructor
563
+ #
564
+ # @see Hanami::Router#get
565
+ #
566
+ # @since 0.1.0
567
+ def options(path, to: nil, as: nil, namespace: nil, configuration: nil, **constraints, &blk)
568
+ add_route(OPTIONS, path, to, as, namespace, configuration, constraints, &blk)
547
569
  end
548
570
 
549
571
  # Defines a root route (a GET route for '/')
@@ -572,27 +594,8 @@ module Hanami
572
594
  #
573
595
  # router.path(:root) # => "/"
574
596
  # router.url(:root) # => "https://hanamirb.org/"
575
- def root(options = {}, &blk)
576
- @router.get(ROOT_PATH, options.merge(as: :root), &blk)
577
- end
578
-
579
- # Defines a route that accepts a OPTIONS request for the given path.
580
- #
581
- # @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
- #
586
- # @param blk [Proc] the anonymous proc to be used as endpoint for the route
587
- #
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)
597
+ def root(to: nil, as: :root, prefix: Utils::PathPrefix.new, namespace: nil, configuration: nil, &blk)
598
+ add_route(GET, prefix.join(ROOT_PATH), to, as, namespace, configuration, &blk)
596
599
  end
597
600
 
598
601
  # Defines an HTTP redirect
@@ -621,57 +624,75 @@ module Hanami
621
624
  #
622
625
  # router = Hanami::Router.new
623
626
  # 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
627
+ def redirect(path, to:, code: 301)
628
+ to = Routing::Redirect.new(@prefix.join(to).to_s, code)
629
+ add_route(GET, path, to)
629
630
  end
630
631
 
631
- # Defines a Ruby block: all the routes defined within it will be namespaced
632
+ # Defines a Ruby block: all the routes defined within it will be prefixed
632
633
  # with the given relative path.
633
634
  #
634
- # Namespaces blocks can be nested multiple times.
635
+ # Prefix blocks can be nested multiple times.
635
636
  #
636
- # @param namespace [String] the relative path where the nested routes will
637
+ # @param path [String] the relative path where the nested routes will
637
638
  # be mounted
638
639
  # @param blk [Proc] the block that defines the resources
639
640
  #
640
- # @return [Hanami::Routing::Namespace] the generated namespace.
641
+ # @return [void]
641
642
  #
642
- # @since 0.1.0
643
+ # @since 2.0.0
643
644
  #
644
645
  # @see Hanami::Router
645
646
  #
646
647
  # @example Basic example
647
- # require 'hanami/router'
648
+ # require "hanami/router"
648
649
  #
649
650
  # Hanami::Router.new do
650
- # namespace 'trees' do
651
- # get '/sequoia', to: endpoint # => '/trees/sequoia'
651
+ # prefix "trees" do
652
+ # get "/sequoia", to: endpoint # => "/trees/sequoia"
652
653
  # end
653
654
  # end
654
655
  #
655
- # @example Nested namespaces
656
- # require 'hanami/router'
656
+ # @example Nested prefix
657
+ # require "hanami/router"
657
658
  #
658
659
  # Hanami::Router.new do
659
- # namespace 'animals' do
660
- # namespace 'mammals' do
661
- # get '/cats', to: endpoint # => '/animals/mammals/cats'
660
+ # prefix "animals" do
661
+ # prefix "mammals" do
662
+ # get "/cats", to: endpoint # => "/animals/mammals/cats"
662
663
  # end
663
664
  # end
664
665
  # end
666
+ def prefix(path, namespace: nil, configuration: nil, &blk)
667
+ Routing::Prefix.new(self, path, namespace, configuration, &blk)
668
+ end
669
+
670
+ # Defines a scope for routes.
671
+ #
672
+ # A scope is a combination of a path prefix and a Ruby namespace.
673
+ #
674
+ # @param prefix [String] the path prefix
675
+ # @param namespace [Module] the Ruby namespace where to lookup endpoints
676
+ # @param configuration [Hanami::Controller::Configuration] the action
677
+ # configuration
678
+ # @param blk [Proc] the routes definition block
679
+ #
680
+ # @since 2.0.0
681
+ # @api private
665
682
  #
666
683
  # @example
667
- # require 'hanami/router'
684
+ # require "hanami/router"
685
+ # require "hanami/controller"
668
686
  #
669
- # router = Hanami::Router.new
670
- # router.namespace 'trees' do
671
- # get '/sequoia', to: endpoint # => '/trees/sequoia'
687
+ # configuration = Hanami::Controller::Configuration.new
688
+ #
689
+ # Hanami::Router.new do
690
+ # scope "/admin", namespace: Admin::Controllers, configuration: configuration do
691
+ # root to: "home#index"
692
+ # end
672
693
  # end
673
- def namespace(namespace, &blk)
674
- Routing::Namespace.new(self, namespace, &blk)
694
+ def scope(prefix, namespace:, configuration:, &blk)
695
+ Routing::Scope.new(self, prefix, namespace, configuration, &blk)
675
696
  end
676
697
 
677
698
  # Defines a set of named routes for a single RESTful resource.
@@ -793,7 +814,7 @@ module Hanami
793
814
  # # | GET | /identity/keys | Identity::Keys | | :keys_identity |
794
815
  # # +------+----------------+----------------+------+----------------+
795
816
  def resource(name, options = {}, &blk)
796
- Routing::Resource.new(self, name, options.merge(separator: @router.action_separator), &blk)
817
+ Routing::Resource.new(self, name, options.merge(separator: Routing::Endpoint::ACTION_SEPARATOR), &blk)
797
818
  end
798
819
 
799
820
  # Defines a set of named routes for a plural RESTful resource.
@@ -916,7 +937,7 @@ module Hanami
916
937
  # # | GET | /articles/search | Articles::Search | | :search_articles |
917
938
  # # +------+------------------+------------------+------+------------------+
918
939
  def resources(name, options = {}, &blk)
919
- Routing::Resources.new(self, name, options.merge(separator: @router.action_separator), &blk)
940
+ Routing::Resources.new(self, name, options.merge(separator: Routing::Endpoint::ACTION_SEPARATOR), &blk)
920
941
  end
921
942
 
922
943
  # Mount a Rack application at the specified path.
@@ -1004,8 +1025,9 @@ module Hanami
1004
1025
  # # 3. RackThree is used as it is (object), because it respond to #call
1005
1026
  # # 4. That Proc is used as it is, because it respond to #call
1006
1027
  # # 5. That string is resolved as Dashboard::Index (Hanami::Controller)
1007
- def mount(app, options)
1008
- @router.mount(app, options)
1028
+ def mount(app, at:, host: nil)
1029
+ app = App.new(@prefix.join(at).to_s, Routing::Endpoint.find(app, @namespace), host: host)
1030
+ @routes.push(app)
1009
1031
  end
1010
1032
 
1011
1033
  # Resolve the given Rack env to a registered endpoint and invoke it.
@@ -1016,7 +1038,15 @@ module Hanami
1016
1038
  #
1017
1039
  # @since 0.1.0
1018
1040
  def call(env)
1019
- @router.call(env)
1041
+ (@routes.find { |r| r.match?(env) } || fallback(env)).call(env)
1042
+ end
1043
+
1044
+ def fallback(env)
1045
+ if @routes.find { |r| r.match_path?(env) }
1046
+ @not_allowed
1047
+ else
1048
+ @not_found
1049
+ end
1020
1050
  end
1021
1051
 
1022
1052
  # Recognize the given env, path, or name and return a route for testing
@@ -1128,14 +1158,11 @@ module Hanami
1128
1158
  # route.routable? # => false
1129
1159
  # route.params # => {:id=>"1"}
1130
1160
  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)
1161
+ env = env_for(env, options, params)
1162
+ # FIXME: this finder is shared with #call and should be extracted
1163
+ route = @routes.find { |r| r.match?(env) }
1135
1164
 
1136
- Routing::RecognizedRoute.new(
1137
- responses.nil? ? responses : responses.first,
1138
- env, @router)
1165
+ Routing::RecognizedRoute.new(route, env, @namespace)
1139
1166
  end
1140
1167
 
1141
1168
  # Generate an relative URL for a specified named route.
@@ -1161,8 +1188,10 @@ module Hanami
1161
1188
  # router.path(:login) # => "/login"
1162
1189
  # router.path(:login, return_to: '/dashboard') # => "/login?return_to=%2Fdashboard"
1163
1190
  # router.path(:framework, name: 'router') # => "/router"
1164
- def path(route, *args)
1165
- @router.path(route, *args)
1191
+ def path(route, args = {})
1192
+ @named.fetch(route).path(args)
1193
+ rescue KeyError
1194
+ raise Hanami::Routing::InvalidRouteException.new("No route could be generated for #{route.inspect} - please check given arguments")
1166
1195
  end
1167
1196
 
1168
1197
  # Generate a URL for a specified named route.
@@ -1188,8 +1217,8 @@ module Hanami
1188
1217
  # router.url(:login) # => "https://hanamirb.org/login"
1189
1218
  # router.url(:login, return_to: '/dashboard') # => "https://hanamirb.org/login?return_to=%2Fdashboard"
1190
1219
  # router.url(:framework, name: 'router') # => "https://hanamirb.org/router"
1191
- def url(route, *args)
1192
- @router.url(route, *args)
1220
+ def url(route, args = {})
1221
+ @base + path(route, args)
1193
1222
  end
1194
1223
 
1195
1224
  # Returns an routes inspector
@@ -1214,8 +1243,8 @@ module Hanami
1214
1243
  # POST /login Sessions::Create
1215
1244
  # logout GET, HEAD /logout Sessions::Destroy
1216
1245
  def inspector
1217
- require 'hanami/routing/routes_inspector'
1218
- Routing::RoutesInspector.new(@router.routes, @router.prefix)
1246
+ require "hanami/routing/routes_inspector"
1247
+ Routing::RoutesInspector.new(@routes, @prefix)
1219
1248
  end
1220
1249
 
1221
1250
  protected
@@ -1233,8 +1262,8 @@ module Hanami
1233
1262
  #
1234
1263
  # @see Hanami::Router#recognize
1235
1264
  # @see http://www.rubydoc.info/github/rack/rack/Rack%2FMockRequest.env_for
1236
- def env_for(env, options = {}, params = nil)
1237
- env = case env
1265
+ def env_for(env, options = {}, params = nil) # rubocop:disable Metrics/MethodLength
1266
+ case env
1238
1267
  when String
1239
1268
  Rack::MockRequest.env_for(env, options)
1240
1269
  when Symbol
@@ -1248,5 +1277,89 @@ module Hanami
1248
1277
  env
1249
1278
  end
1250
1279
  end
1280
+
1281
+ private
1282
+
1283
+ PATH_INFO = "PATH_INFO"
1284
+ SCRIPT_NAME = "SCRIPT_NAME"
1285
+ SERVER_NAME = "SERVER_NAME"
1286
+ REQUEST_METHOD = "REQUEST_METHOD"
1287
+
1288
+ PARAMS = "router.params"
1289
+
1290
+ GET = "GET"
1291
+ HEAD = "HEAD"
1292
+ POST = "POST"
1293
+ PUT = "PUT"
1294
+ PATCH = "PATCH"
1295
+ DELETE = "DELETE"
1296
+ TRACE = "TRACE"
1297
+ OPTIONS = "OPTIONS"
1298
+ LINK = "LINK"
1299
+ UNLINK = "UNLINK"
1300
+
1301
+ NOT_FOUND = ->(_) { [404, { "Content-Length" => "9" }, ["Not Found"]] }.freeze
1302
+ NOT_ALLOWED = ->(_) { [405, { "Content-Length" => "18" }, ["Method Not Allowed"]] }.freeze
1303
+ ROOT = "/"
1304
+
1305
+ BODY = 2
1306
+
1307
+ attr_reader :configuration
1308
+
1309
+ # Application
1310
+ #
1311
+ # @since 2.0.0
1312
+ # @api private
1313
+ class App
1314
+ def initialize(path, endpoint, host: nil)
1315
+ @path = Mustermann.new(path, type: :rails, version: "5.0")
1316
+ @prefix = path.to_s
1317
+ @endpoint = endpoint
1318
+ @host = host
1319
+ freeze
1320
+ end
1321
+
1322
+ def match?(env)
1323
+ match_path?(env)
1324
+ end
1325
+
1326
+ def match_path?(env)
1327
+ result = env[PATH_INFO].start_with?(@prefix)
1328
+ result &&= @host == env[SERVER_NAME] unless @host.nil?
1329
+
1330
+ result
1331
+ end
1332
+
1333
+ def call(env)
1334
+ env[PARAMS] ||= {}
1335
+ env[PARAMS].merge!(Utils::Hash.deep_symbolize(@path.params(env[PATH_INFO]) || {}))
1336
+
1337
+ env[SCRIPT_NAME] = @prefix
1338
+ env[PATH_INFO] = env[PATH_INFO].sub(@prefix, "")
1339
+ env[PATH_INFO] = "/" if env[PATH_INFO] == ""
1340
+
1341
+ @endpoint.call(env)
1342
+ end
1343
+ end
1344
+
1345
+ def add_route(verb, path, to, as = nil, namespace = nil, config = nil, constraints = {}, &blk)
1346
+ to ||= blk
1347
+ config ||= configuration
1348
+
1349
+ path = path.to_s
1350
+ endpoint = Routing::Endpoint.find(to, namespace || @namespace, config)
1351
+ route = Routing::Route.new(verb_for(verb), @prefix.join(path).to_s, endpoint, constraints)
1352
+
1353
+ @routes.push(route)
1354
+ @named[as] = route unless as.nil?
1355
+ end
1356
+
1357
+ def verb_for(value)
1358
+ if value == GET
1359
+ [GET, HEAD]
1360
+ else
1361
+ [value]
1362
+ end
1363
+ end
1251
1364
  end
1252
1365
  end