hanami-router 1.3.1 → 2.0.0.alpha4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/LICENSE.md +1 -1
- data/README.md +97 -443
- data/hanami-router.gemspec +23 -19
- data/lib/hanami/middleware/body_parser.rb +21 -15
- data/lib/hanami/middleware/body_parser/class_interface.rb +62 -56
- data/lib/hanami/middleware/body_parser/errors.rb +7 -4
- data/lib/hanami/middleware/body_parser/json_parser.rb +9 -7
- data/lib/hanami/middleware/body_parser/parser.rb +58 -0
- data/lib/hanami/middleware/error.rb +16 -0
- data/lib/hanami/router.rb +548 -957
- data/lib/hanami/router/block.rb +88 -0
- data/lib/hanami/router/error.rb +67 -0
- data/lib/hanami/router/node.rb +91 -0
- data/lib/hanami/router/params.rb +35 -0
- data/lib/hanami/router/prefix.rb +67 -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 +4 -1
- metadata +61 -41
- data/lib/hanami-router.rb +0 -1
- data/lib/hanami/routing/endpoint.rb +0 -195
- data/lib/hanami/routing/endpoint_resolver.rb +0 -238
- data/lib/hanami/routing/error.rb +0 -7
- data/lib/hanami/routing/force_ssl.rb +0 -212
- data/lib/hanami/routing/http_router.rb +0 -220
- data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
- data/lib/hanami/routing/namespace.rb +0 -98
- data/lib/hanami/routing/parsers.rb +0 -113
- data/lib/hanami/routing/parsing/json_parser.rb +0 -33
- data/lib/hanami/routing/parsing/parser.rb +0 -61
- data/lib/hanami/routing/recognized_route.rb +0 -219
- data/lib/hanami/routing/resource.rb +0 -119
- data/lib/hanami/routing/resource/action.rb +0 -402
- data/lib/hanami/routing/resource/nested.rb +0 -41
- data/lib/hanami/routing/resource/options.rb +0 -74
- data/lib/hanami/routing/resources.rb +0 -48
- data/lib/hanami/routing/resources/action.rb +0 -156
- data/lib/hanami/routing/route.rb +0 -71
- data/lib/hanami/routing/routes_inspector.rb +0 -221
@@ -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,91 @@
|
|
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
|
+
def get(segment) # rubocop:disable Metrics/PerceivedComplexity
|
40
|
+
return unless @variable || @fixed
|
41
|
+
|
42
|
+
found = nil
|
43
|
+
captured = nil
|
44
|
+
|
45
|
+
found = @fixed&.fetch(segment, nil)
|
46
|
+
return [found, nil] if found
|
47
|
+
|
48
|
+
@variable&.each do |matcher, node|
|
49
|
+
break if found
|
50
|
+
|
51
|
+
captured = matcher.match(segment)
|
52
|
+
found = node if captured
|
53
|
+
end
|
54
|
+
|
55
|
+
[found, captured&.named_captures]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
# @since 2.0.0
|
60
|
+
def leaf?
|
61
|
+
@to
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
# @since 2.0.0
|
66
|
+
def leaf!(to)
|
67
|
+
@to = to
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# @api private
|
73
|
+
# @since 2.0.0
|
74
|
+
def variable?(segment)
|
75
|
+
/:/.match?(segment)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
# @since 2.0.0
|
80
|
+
def segment_for(segment, constraints)
|
81
|
+
Segment.fabricate(segment, **constraints)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @api private
|
85
|
+
# @since 2.0.0
|
86
|
+
def fixed?(matcher)
|
87
|
+
matcher.names.empty?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
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)
|
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,67 @@
|
|
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..]
|
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
|
+
return @prefix if path == DEFAULT_SEPARATOR
|
61
|
+
|
62
|
+
(@prefix + DEFAULT_SEPARATOR + path)
|
63
|
+
.gsub(DOUBLE_DEFAULT_SEPARATOR_REGEXP, DEFAULT_SEPARATOR)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
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?
|
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
|