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.
@@ -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