hanami-router 1.3.2 → 2.0.0.alpha5

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +98 -444
  5. data/hanami-router.gemspec +23 -19
  6. data/lib/hanami/middleware/body_parser.rb +21 -15
  7. data/lib/hanami/middleware/body_parser/class_interface.rb +62 -56
  8. data/lib/hanami/middleware/body_parser/errors.rb +7 -4
  9. data/lib/hanami/middleware/body_parser/json_parser.rb +9 -7
  10. data/lib/hanami/middleware/body_parser/parser.rb +58 -0
  11. data/lib/hanami/middleware/error.rb +16 -0
  12. data/lib/hanami/router.rb +608 -955
  13. data/lib/hanami/router/block.rb +88 -0
  14. data/lib/hanami/router/error.rb +67 -0
  15. data/lib/hanami/router/inspector.rb +38 -0
  16. data/lib/hanami/router/node.rb +91 -0
  17. data/lib/hanami/router/params.rb +35 -0
  18. data/lib/hanami/router/prefix.rb +67 -0
  19. data/lib/hanami/router/recognized_route.rb +92 -0
  20. data/lib/hanami/router/redirect.rb +33 -0
  21. data/lib/hanami/router/route.rb +130 -0
  22. data/lib/hanami/router/segment.rb +19 -0
  23. data/lib/hanami/router/trie.rb +63 -0
  24. data/lib/hanami/router/url_helpers.rb +40 -0
  25. data/lib/hanami/router/version.rb +4 -1
  26. metadata +61 -39
  27. data/lib/hanami-router.rb +0 -1
  28. data/lib/hanami/routing/endpoint.rb +0 -195
  29. data/lib/hanami/routing/endpoint_resolver.rb +0 -238
  30. data/lib/hanami/routing/error.rb +0 -7
  31. data/lib/hanami/routing/force_ssl.rb +0 -212
  32. data/lib/hanami/routing/http_router.rb +0 -220
  33. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  34. data/lib/hanami/routing/namespace.rb +0 -98
  35. data/lib/hanami/routing/parsers.rb +0 -113
  36. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  37. data/lib/hanami/routing/parsing/parser.rb +0 -61
  38. data/lib/hanami/routing/recognized_route.rb +0 -219
  39. data/lib/hanami/routing/resource.rb +0 -119
  40. data/lib/hanami/routing/resource/action.rb +0 -402
  41. data/lib/hanami/routing/resource/nested.rb +0 -41
  42. data/lib/hanami/routing/resource/options.rb +0 -74
  43. data/lib/hanami/routing/resources.rb +0 -48
  44. data/lib/hanami/routing/resources/action.rb +0 -156
  45. data/lib/hanami/routing/route.rb +0 -71
  46. 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,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Router
5
+ # Routes inspector
6
+ #
7
+ # @api private
8
+ # @since 2.0.0
9
+ class Inspector
10
+ # @api private
11
+ # @since 2.0.0
12
+ def initialize(routes: [])
13
+ @routes = routes
14
+ end
15
+
16
+ # @param route [Hash] serialized route
17
+ #
18
+ # @api private
19
+ # @since 2.0.0
20
+ def add_route(route)
21
+ @routes.push(route)
22
+ end
23
+
24
+ # @return [String] The inspected routes
25
+ #
26
+ # @api private
27
+ # @since 2.0.0
28
+ def call(*)
29
+ @routes.map(&:to_inspect).join(NEW_LINE)
30
+ end
31
+
32
+ # @api private
33
+ # @since 2.0.0
34
+ NEW_LINE = $/
35
+ private_constant :NEW_LINE
36
+ end
37
+ end
38
+ 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