hanami-router 2.0.0.alpha1 → 2.0.0.alpha2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +11 -401
- data/hanami-router.gemspec +2 -4
- data/lib/hanami/middleware/body_parser.rb +2 -2
- data/lib/hanami/middleware/body_parser/class_interface.rb +10 -4
- data/lib/hanami/middleware/body_parser/json_parser.rb +4 -4
- data/lib/hanami/router.rb +525 -1040
- data/lib/hanami/router/block.rb +88 -0
- data/lib/hanami/router/error.rb +67 -0
- data/lib/hanami/router/node.rb +93 -0
- data/lib/hanami/router/params.rb +35 -0
- data/lib/hanami/router/prefix.rb +65 -0
- data/lib/hanami/router/recognized_route.rb +92 -0
- data/lib/hanami/router/redirect.rb +28 -0
- data/lib/hanami/router/segment.rb +19 -0
- data/lib/hanami/router/trie.rb +63 -0
- data/lib/hanami/router/url_helpers.rb +40 -0
- data/lib/hanami/router/version.rb +2 -1
- metadata +17 -48
- data/lib/hanami/routing.rb +0 -193
- data/lib/hanami/routing/endpoint.rb +0 -213
- data/lib/hanami/routing/endpoint_resolver.rb +0 -242
- data/lib/hanami/routing/prefix.rb +0 -102
- data/lib/hanami/routing/recognized_route.rb +0 -233
- data/lib/hanami/routing/resource.rb +0 -121
- data/lib/hanami/routing/resource/action.rb +0 -427
- data/lib/hanami/routing/resource/nested.rb +0 -44
- data/lib/hanami/routing/resource/options.rb +0 -76
- data/lib/hanami/routing/resources.rb +0 -50
- data/lib/hanami/routing/resources/action.rb +0 -161
- data/lib/hanami/routing/routes_inspector.rb +0 -223
- data/lib/hanami/routing/scope.rb +0 -112
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Router
|
5
|
+
# Block endpoint
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
# @since 2.0.0
|
9
|
+
class Block
|
10
|
+
# Context to handle a single incoming HTTP request for a block endpoint
|
11
|
+
#
|
12
|
+
# @since 2.0.0
|
13
|
+
class Context
|
14
|
+
# @api private
|
15
|
+
# @since 2.0.0
|
16
|
+
def initialize(blk, env)
|
17
|
+
@blk = blk
|
18
|
+
@env = env
|
19
|
+
end
|
20
|
+
|
21
|
+
# Rack env
|
22
|
+
#
|
23
|
+
# @return [Hash] the Rack env
|
24
|
+
#
|
25
|
+
# @since 2.0.0
|
26
|
+
attr_reader :env
|
27
|
+
|
28
|
+
# @overload status
|
29
|
+
# Gets the current HTTP status code
|
30
|
+
# @return [Integer] the HTTP status code
|
31
|
+
# @overload status(value)
|
32
|
+
# Sets the HTTP status
|
33
|
+
# @param value [Integer] the HTTP status code
|
34
|
+
def status(value = nil)
|
35
|
+
if value
|
36
|
+
@status = value
|
37
|
+
else
|
38
|
+
@status ||= 200
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @overload headers
|
43
|
+
# Gets the current HTTP headers code
|
44
|
+
# @return [Integer] the HTTP headers code
|
45
|
+
# @overload headers(value)
|
46
|
+
# Sets the HTTP headers
|
47
|
+
# @param value [Integer] the HTTP headers code
|
48
|
+
def headers(value = nil)
|
49
|
+
if value
|
50
|
+
@headers = value
|
51
|
+
else
|
52
|
+
@headers ||= {}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# HTTP Params from URL variables and HTTP body parsing
|
57
|
+
#
|
58
|
+
# @return [Hash] the HTTP params
|
59
|
+
#
|
60
|
+
# @since 2.0.0
|
61
|
+
def params
|
62
|
+
env["router.params"]
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
# @since 2.0.0
|
67
|
+
def call
|
68
|
+
body = instance_exec(&@blk)
|
69
|
+
[status, headers, [body]]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
# @since 2.0.0
|
75
|
+
def initialize(context_class, blk)
|
76
|
+
@context_class = context_class || Context
|
77
|
+
@blk = blk
|
78
|
+
freeze
|
79
|
+
end
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
# @since 2.0.0
|
83
|
+
def call(env)
|
84
|
+
@context_class.new(@blk, env).call
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Router
|
5
|
+
# Base error
|
6
|
+
#
|
7
|
+
# @since 0.5.0
|
8
|
+
class Error < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Missing endpoint error. It's raised when the route definition is missing `to:` endpoint and a block.
|
12
|
+
#
|
13
|
+
# @since 2.0.0
|
14
|
+
class MissingEndpointError < Error
|
15
|
+
def initialize(path)
|
16
|
+
super("missing endpoint for #{path.inspect}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Invalid route exception. It's raised when the router cannot recognize a route
|
21
|
+
#
|
22
|
+
# @since 2.0.0
|
23
|
+
class InvalidRouteException < Error
|
24
|
+
def initialize(name)
|
25
|
+
super("No route could be generated for #{name.inspect} - please check given arguments")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Invalid route expansion exception. It's raised when the router recognizes
|
30
|
+
# a route but given variables cannot be expanded into a path/url
|
31
|
+
#
|
32
|
+
# @since 2.0.0
|
33
|
+
#
|
34
|
+
# @see Hanami::Router#path
|
35
|
+
# @see Hanami::Router#url
|
36
|
+
class InvalidRouteExpansionException < Error
|
37
|
+
def initialize(name, message)
|
38
|
+
super("No route could be generated for `#{name.inspect}': #{message}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Handle unknown HTTP status codes
|
43
|
+
#
|
44
|
+
# @since 2.0.0
|
45
|
+
class UnknownHTTPStatusCodeError < Error
|
46
|
+
def initialize(code)
|
47
|
+
super("Unknown HTTP status code: #{code.inspect}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This error is raised when <tt>#call</tt> is invoked on a non-routable
|
52
|
+
# recognized route.
|
53
|
+
#
|
54
|
+
# @since 0.5.0
|
55
|
+
#
|
56
|
+
# @see Hanami::Router#recognize
|
57
|
+
# @see Hanami::Router::RecognizedRoute
|
58
|
+
# @see Hanami::Router::RecognizedRoute#call
|
59
|
+
# @see Hanami::Router::RecognizedRoute#routable?
|
60
|
+
class NotRoutableEndpointError < Error
|
61
|
+
# @since 0.5.0
|
62
|
+
def initialize(env)
|
63
|
+
super %(Cannot find routable endpoint for: #{env['REQUEST_METHOD']} #{env['PATH_INFO']})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hanami/router/segment"
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
class Router
|
7
|
+
# Trie node
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
# @since 2.0.0
|
11
|
+
class Node
|
12
|
+
# @api private
|
13
|
+
# @since 2.0.0
|
14
|
+
attr_reader :to
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
# @since 2.0.0
|
18
|
+
def initialize
|
19
|
+
@variable = nil
|
20
|
+
@fixed = nil
|
21
|
+
@to = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
# @since 2.0.0
|
26
|
+
def put(segment, constraints)
|
27
|
+
if variable?(segment)
|
28
|
+
@variable ||= {}
|
29
|
+
@variable[segment_for(segment, constraints)] ||= self.class.new
|
30
|
+
else
|
31
|
+
@fixed ||= {}
|
32
|
+
@fixed[segment] ||= self.class.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
# @since 2.0.0
|
38
|
+
#
|
39
|
+
# rubocop:disable Metrics/MethodLength
|
40
|
+
def get(segment)
|
41
|
+
return unless @variable || @fixed
|
42
|
+
|
43
|
+
found = nil
|
44
|
+
captured = nil
|
45
|
+
|
46
|
+
found = @fixed&.fetch(segment, nil)
|
47
|
+
return [found, nil] if found
|
48
|
+
|
49
|
+
@variable&.each do |matcher, node|
|
50
|
+
break if found
|
51
|
+
|
52
|
+
captured = matcher.match(segment)
|
53
|
+
found = node if captured
|
54
|
+
end
|
55
|
+
|
56
|
+
[found, captured&.named_captures]
|
57
|
+
end
|
58
|
+
# rubocop:enable Metrics/MethodLength
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
# @since 2.0.0
|
62
|
+
def leaf?
|
63
|
+
@to
|
64
|
+
end
|
65
|
+
|
66
|
+
# @api private
|
67
|
+
# @since 2.0.0
|
68
|
+
def leaf!(to)
|
69
|
+
@to = to
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# @api private
|
75
|
+
# @since 2.0.0
|
76
|
+
def variable?(segment)
|
77
|
+
/:/.match?(segment)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @api private
|
81
|
+
# @since 2.0.0
|
82
|
+
def segment_for(segment, constraints)
|
83
|
+
Segment.fabricate(segment, **constraints)
|
84
|
+
end
|
85
|
+
|
86
|
+
# @api private
|
87
|
+
# @since 2.0.0
|
88
|
+
def fixed?(matcher)
|
89
|
+
matcher.names.empty?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Router
|
5
|
+
# Params utilities
|
6
|
+
#
|
7
|
+
# @since 2.0.0
|
8
|
+
# @api private
|
9
|
+
class Params
|
10
|
+
# Deep symbolize Hash params
|
11
|
+
#
|
12
|
+
# @param params [Hash] the params to symbolize
|
13
|
+
#
|
14
|
+
# @return [Hash] the symbolized params
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @since 2.0.0
|
18
|
+
def self.deep_symbolize(params) # rubocop:disable Metrics/MethodLength
|
19
|
+
params.each_with_object({}) do |(key, value), output|
|
20
|
+
output[key.to_sym] =
|
21
|
+
case value
|
22
|
+
when ::Hash
|
23
|
+
deep_symbolize(value)
|
24
|
+
when Array
|
25
|
+
value.map do |item|
|
26
|
+
item.is_a?(::Hash) ? deep_symbolize(item) : item
|
27
|
+
end
|
28
|
+
else
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Router
|
5
|
+
# URL Path prefix
|
6
|
+
#
|
7
|
+
# @since 2.0.0
|
8
|
+
# @api private
|
9
|
+
class Prefix
|
10
|
+
# @since 2.0.0
|
11
|
+
# @api private
|
12
|
+
def initialize(prefix)
|
13
|
+
@prefix = prefix
|
14
|
+
end
|
15
|
+
|
16
|
+
# @since 2.0.0
|
17
|
+
# @api private
|
18
|
+
def join(path)
|
19
|
+
self.class.new(
|
20
|
+
_join(path)
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @since 2.0.0
|
25
|
+
# @api private
|
26
|
+
def relative_join(path, separator = DEFAULT_SEPARATOR)
|
27
|
+
_join(path.to_s)
|
28
|
+
.gsub(DEFAULT_SEPARATOR_REGEXP, separator)[1..-1]
|
29
|
+
end
|
30
|
+
|
31
|
+
# @since 2.0.0
|
32
|
+
# @api private
|
33
|
+
def to_s
|
34
|
+
@prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
# @since 2.0.0
|
38
|
+
# @api private
|
39
|
+
def to_sym
|
40
|
+
@prefix.to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @since 2.0.0
|
46
|
+
# @api private
|
47
|
+
DEFAULT_SEPARATOR = "/"
|
48
|
+
|
49
|
+
# @since 2.0.0
|
50
|
+
# @api private
|
51
|
+
DEFAULT_SEPARATOR_REGEXP = /\//.freeze
|
52
|
+
|
53
|
+
# @since 2.0.0
|
54
|
+
# @api private
|
55
|
+
DOUBLE_DEFAULT_SEPARATOR_REGEXP = /[\/]{2,}/.freeze
|
56
|
+
|
57
|
+
# @since 2.0.0
|
58
|
+
# @api private
|
59
|
+
def _join(path)
|
60
|
+
(@prefix + DEFAULT_SEPARATOR + path)
|
61
|
+
.gsub(DOUBLE_DEFAULT_SEPARATOR_REGEXP, DEFAULT_SEPARATOR)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Router
|
5
|
+
# Represents a result of router path recognition.
|
6
|
+
#
|
7
|
+
# @since 0.5.0
|
8
|
+
#
|
9
|
+
# @see Hanami::Router#recognize
|
10
|
+
class RecognizedRoute
|
11
|
+
def initialize(endpoint, env)
|
12
|
+
@endpoint = endpoint
|
13
|
+
@env = env
|
14
|
+
end
|
15
|
+
|
16
|
+
# Rack protocol compatibility
|
17
|
+
#
|
18
|
+
# @param env [Hash] Rack env
|
19
|
+
#
|
20
|
+
# @return [Array] serialized Rack response
|
21
|
+
#
|
22
|
+
# @raise [Hanami::Router::NotRoutableEndpointError] if not routable
|
23
|
+
#
|
24
|
+
# @since 0.5.0
|
25
|
+
# @api public
|
26
|
+
#
|
27
|
+
# @see Hanami::Router::RecognizedRoute#routable?
|
28
|
+
# @see Hanami::Router::NotRoutableEndpointError
|
29
|
+
def call(env)
|
30
|
+
if routable? # rubocop:disable Style/GuardClause
|
31
|
+
@endpoint.call(env)
|
32
|
+
else
|
33
|
+
raise NotRoutableEndpointError.new(@env)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# HTTP verb (aka method)
|
38
|
+
#
|
39
|
+
# @return [String]
|
40
|
+
#
|
41
|
+
# @since 0.5.0
|
42
|
+
# @api public
|
43
|
+
def verb
|
44
|
+
@env["REQUEST_METHOD"]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Relative URL (path)
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
#
|
51
|
+
# @since 0.7.0
|
52
|
+
# @api public
|
53
|
+
def path
|
54
|
+
@env["PATH_INFO"]
|
55
|
+
end
|
56
|
+
|
57
|
+
# @since 0.7.0
|
58
|
+
# @api public
|
59
|
+
def params
|
60
|
+
@env["router.params"]
|
61
|
+
end
|
62
|
+
|
63
|
+
# @since 0.7.0
|
64
|
+
# @api public
|
65
|
+
def endpoint
|
66
|
+
return nil if redirect?
|
67
|
+
|
68
|
+
@endpoint
|
69
|
+
end
|
70
|
+
|
71
|
+
# @since 0.7.0
|
72
|
+
# @api public
|
73
|
+
def routable?
|
74
|
+
!@endpoint.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
# @since 0.7.0
|
78
|
+
# @api public
|
79
|
+
def redirect?
|
80
|
+
@endpoint.is_a?(Redirect)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @since 0.7.0
|
84
|
+
# @api public
|
85
|
+
def redirection_path
|
86
|
+
return nil unless redirect?
|
87
|
+
|
88
|
+
@endpoint.destination
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Router
|
5
|
+
# HTTP Redirect
|
6
|
+
#
|
7
|
+
# @since 2.0.0
|
8
|
+
# @api private
|
9
|
+
class Redirect
|
10
|
+
# @since 2.0.0
|
11
|
+
# @api private
|
12
|
+
attr_reader :destination
|
13
|
+
|
14
|
+
# @since 2.0.0
|
15
|
+
# @api private
|
16
|
+
def initialize(destination, endpoint)
|
17
|
+
@destination = destination
|
18
|
+
@endpoint = endpoint
|
19
|
+
end
|
20
|
+
|
21
|
+
# @since 2.0.0
|
22
|
+
# @api private
|
23
|
+
def call(env)
|
24
|
+
@endpoint.call(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|