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