hanami-router 2.0.0.alpha1 → 2.0.0.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|