hanami-router 2.0.0.alpha3 → 2.0.0

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,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Middleware
5
+ class BodyParser
6
+ # Body parser abstract class
7
+ #
8
+ # @since 2.0.0
9
+ class Parser
10
+ DEFAULT_MIME_TYPES = [].freeze
11
+
12
+ # Return supported mime types
13
+ #
14
+ # @return [Array<String>] supported MIME types
15
+ #
16
+ # @abstract
17
+ # @since 2.0.0
18
+ #
19
+ # @example
20
+ # require "hanami/middleware/body_parser"
21
+ #
22
+ # class XMLParser < Hanami::Middleware::BodyParser::Parser
23
+ # def self.mime_types
24
+ # ["application/xml", "text/xml"]
25
+ # end
26
+ # end
27
+ attr_reader :mime_types
28
+
29
+ # @api private
30
+ def initialize(mime_types: DEFAULT_MIME_TYPES)
31
+ @mime_types = self.class.mime_types + mime_types
32
+ end
33
+
34
+ # Parse raw HTTP request body
35
+ #
36
+ # @param body [String] HTTP request body
37
+ #
38
+ # @return [Hash] the result of the parsing
39
+ #
40
+ # @raise [Hanami::Middleware::BodyParser::BodyParsingError] the error
41
+ # that must be raised if the parsing cannot be accomplished
42
+ #
43
+ # @abstract
44
+ # @since 2.0.0
45
+ #
46
+ # @example
47
+ # require "hanami/middleware/body_parser"
48
+ #
49
+ # class XMLParser < Hanami::Middleware::BodyParser::Parser
50
+ # def parse(body)
51
+ # # XML parsing
52
+ # # ...
53
+ # rescue => exception
54
+ # raise Hanami::Middleware::BodyParser::BodyParsingError.new(exception.message)
55
+ # end
56
+ # end
57
+ def parse(body) # rubocop:disable Lint/UnusedMethodArgument
58
+ raise NoMethodError
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,20 +2,23 @@
2
2
 
3
3
  require "hanami/router/params"
4
4
  require "hanami/middleware/error"
5
- require_relative "body_parser/class_interface"
5
+ require_relative "../router/constants"
6
6
 
7
7
  module Hanami
8
8
  module Middleware
9
9
  # @since 1.3.0
10
10
  # @api private
11
11
  class BodyParser
12
+ require_relative "body_parser/class_interface"
13
+ require_relative "body_parser/parser"
14
+
12
15
  # @since 1.3.0
13
16
  # @api private
14
17
  CONTENT_TYPE = "CONTENT_TYPE"
15
18
 
16
19
  # @since 1.3.0
17
20
  # @api private
18
- MEDIA_TYPE_MATCHER = /\s*[;,]\s*/.freeze
21
+ MEDIA_TYPE_MATCHER = /\s*[;,]\s*/
19
22
 
20
23
  # @since 1.3.0
21
24
  # @api private
@@ -25,9 +28,6 @@ module Hanami
25
28
  # @api private
26
29
  ROUTER_PARAMS = "router.params"
27
30
 
28
- # @api private
29
- ROUTER_PARSED_BODY = "router.parsed_body"
30
-
31
31
  # @api private
32
32
  FALLBACK_KEY = "_"
33
33
 
@@ -35,7 +35,7 @@ module Hanami
35
35
 
36
36
  def initialize(app, parsers)
37
37
  @app = app
38
- @parsers = build_parsers(parsers)
38
+ @parsers = parsers
39
39
  end
40
40
 
41
41
  def call(env)
@@ -45,8 +45,8 @@ module Hanami
45
45
  env[RACK_INPUT].rewind # somebody might try to read this stream
46
46
 
47
47
  if (parser = @parsers[media_type(env)])
48
- env[ROUTER_PARSED_BODY] = parser.parse(body)
49
- env[ROUTER_PARAMS] = _symbolize(env[ROUTER_PARSED_BODY])
48
+ env[Router::ROUTER_PARSED_BODY] = parser.parse(body)
49
+ env[ROUTER_PARAMS] = _symbolize(env[Router::ROUTER_PARSED_BODY])
50
50
  end
51
51
 
52
52
  @app.call(env)
@@ -54,25 +54,12 @@ module Hanami
54
54
 
55
55
  private
56
56
 
57
- def build_parsers(parser_names)
58
- parser_names = Array(parser_names)
59
- return {} if parser_names.empty?
60
-
61
- parser_names.each_with_object({}) do |name, parsers|
62
- parser = self.class.for(name)
63
-
64
- parser.mime_types.each do |mime|
65
- parsers[mime] = parser
66
- end
67
- end
68
- end
69
-
70
57
  # @api private
71
58
  def _symbolize(body)
72
59
  if body.is_a?(::Hash)
73
60
  Router::Params.deep_symbolize(body)
74
61
  else
75
- { FALLBACK_KEY => body }
62
+ {FALLBACK_KEY => body}
76
63
  end
77
64
  end
78
65
 
@@ -5,7 +5,7 @@ module Hanami
5
5
  #
6
6
  # @since 1.3.0
7
7
  module Middleware
8
- unless defined?(Error)
8
+ unless defined?(::Hanami::Middleware::Error)
9
9
  # Base error for Rack middleware
10
10
  #
11
11
  # @since 2.0.0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Middleware
5
+ # Trie node to register scopes with custom Rack middleware
6
+ #
7
+ # @api private
8
+ # @since 2.0.0
9
+ class Node
10
+ # @api private
11
+ # @since 2.0.0
12
+ attr_reader :app
13
+
14
+ # @api private
15
+ # @since 2.0.0
16
+ def initialize
17
+ @app = nil
18
+ @children = {}
19
+ end
20
+
21
+ # @api private
22
+ # @since 2.0.0
23
+ def freeze
24
+ @children.freeze
25
+ super
26
+ end
27
+
28
+ # @api private
29
+ # @since 2.0.0
30
+ def put(segment)
31
+ @children[segment] ||= self.class.new
32
+ end
33
+
34
+ # @api private
35
+ # @since 2.0.0
36
+ def get(segment)
37
+ @children.fetch(segment) { self if leaf? }
38
+ end
39
+
40
+ # @api private
41
+ # @since 2.0.0
42
+ def app!(app)
43
+ @app = app
44
+ end
45
+
46
+ # @api private
47
+ # @since 2.0.0
48
+ def app?
49
+ !@app.nil?
50
+ end
51
+
52
+ # @api private
53
+ # @since 2.0.0
54
+ def leaf?
55
+ @children.empty?
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./node"
4
+
5
+ module Hanami
6
+ module Middleware
7
+ # Trie to register scopes with custom Rack middleware
8
+ #
9
+ # @api private
10
+ # @since 2.0.0
11
+ class Trie
12
+ # @api private
13
+ # @since 2.0.0
14
+ def initialize(app)
15
+ @app = app
16
+ @root = Node.new
17
+ end
18
+
19
+ # @api private
20
+ # @since 2.0.0
21
+ def freeze
22
+ @root.freeze
23
+ super
24
+ end
25
+
26
+ # @api private
27
+ # @since 2.0.0
28
+ def add(path, app)
29
+ node = @root
30
+ for_each_segment(path) do |segment|
31
+ node = node.put(segment)
32
+ end
33
+
34
+ node.app!(app)
35
+ end
36
+
37
+ # @api private
38
+ # @since 2.0.0
39
+ def find(path)
40
+ node = @root
41
+
42
+ for_each_segment(path) do |segment|
43
+ break unless node
44
+
45
+ node = node.get(segment)
46
+ end
47
+
48
+ return node.app if node&.app?
49
+
50
+ @root.app || @app
51
+ end
52
+
53
+ # @api private
54
+ # @since 2.0.0
55
+ def empty?
56
+ @root.leaf?
57
+ end
58
+
59
+ private
60
+
61
+ # @api private
62
+ # @since 2.0.0
63
+ def for_each_segment(path, &blk)
64
+ _, *segments = path.split(/\//)
65
+ segments.each(&blk)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -35,7 +35,7 @@ module Hanami
35
35
  if value
36
36
  @status = value
37
37
  else
38
- @status ||= 200
38
+ @status ||= Router::HTTP_STATUS_OK
39
39
  end
40
40
  end
41
41
 
@@ -59,7 +59,7 @@ module Hanami
59
59
  #
60
60
  # @since 2.0.0
61
61
  def params
62
- env["router.params"]
62
+ env[Router::PARAMS]
63
63
  end
64
64
 
65
65
  # @api private
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Router
5
+ # @api private
6
+ # @since 2.0.0
7
+ ROUTER_PARSED_BODY = "router.parsed_body"
8
+ end
9
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Router
5
+ # Base class for all Hanami::Router errors.
6
+ #
7
+ # @since 0.5.0
8
+ # @api public
9
+ class Error < StandardError
10
+ end
11
+
12
+ # Error raised when no endpoint is specified for a route.
13
+ #
14
+ # Endpoints must be specified by `to:` or a block.
15
+ #
16
+ # @since 2.0.0
17
+ # @api public
18
+ class MissingEndpointError < Error
19
+ # @since 2.0.0
20
+ # @api private
21
+ def initialize(path)
22
+ super("missing endpoint for #{path.inspect}")
23
+ end
24
+ end
25
+
26
+ # Error raised when a named route could not be found.
27
+ #
28
+ # @see Hanami::Router#path
29
+ # @see Hanami::Router#url
30
+ #
31
+ # @since 2.0.0
32
+ # @api public
33
+ class MissingRouteError < Error
34
+ # @since 2.0.0
35
+ # @api private
36
+ def initialize(name)
37
+ super("No route could be found with name #{name.inspect}")
38
+ end
39
+ end
40
+
41
+ # Error raised when variables given for route cannot be expanded into a full path.
42
+ #
43
+ # @see Hanami::Router#path
44
+ # @see Hanami::Router#url
45
+ #
46
+ # @since 2.0.0
47
+ # @api public
48
+ class InvalidRouteExpansionError < Error
49
+ # @since 2.0.0
50
+ # @api private
51
+ def initialize(name, message)
52
+ super("No route could be generated for `#{name.inspect}': #{message}")
53
+ end
54
+ end
55
+
56
+ # Error raised when an unknown HTTP status code is given.
57
+ #
58
+ # @see Hanami::Router#redirect
59
+ #
60
+ # @since 2.0.0
61
+ # @api public
62
+ class UnknownHTTPStatusCodeError < Error
63
+ # @since 2.0.0
64
+ # @api private
65
+ def initialize(code)
66
+ super("Unknown HTTP status code: #{code.inspect}")
67
+ end
68
+ end
69
+
70
+ # Error raised when a recognized route is called but has no callable endpoint.
71
+ #
72
+ # @see Hanami::Router#recognize
73
+ # @see Hanami::Router::RecognizedRoute#call
74
+ #
75
+ # @since 0.5.0
76
+ # @api public
77
+ class NotRoutableEndpointError < Error
78
+ # @since 0.5.0
79
+ # @api private
80
+ def initialize(env)
81
+ super %(Cannot find routable endpoint for: #{env[::Rack::REQUEST_METHOD]} #{env[::Rack::PATH_INFO]})
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Hanami
6
+ class Router
7
+ # Renders a CSV representation of the routes
8
+ #
9
+ # You can forward [CSV generation
10
+ # options](https://ruby-doc.org/stdlib-3.1.0/libdoc/csv/rdoc/CSV.html#class-CSV-label-Options+for+Generating]
11
+ # when calling it:
12
+ #
13
+ # ```
14
+ # require "hanami/router/inspector"
15
+ # require "hanami/router/formatter/csv"
16
+ #
17
+ # Hanami::Router::Inspector.new(
18
+ # routes: Router.routes,
19
+ # formatter: Hanami::Router::Formatter::CSV.new
20
+ # ).call(write_headers: false)
21
+ # ```
22
+ #
23
+ # @api private
24
+ # @since 2.0.0
25
+ module Formatter
26
+ class CSV
27
+ # @api private
28
+ # @since 2.0.0
29
+ DEFAULT_OPTIONS = {
30
+ write_headers: true
31
+ }.freeze
32
+
33
+ # @api private
34
+ # @since 2.0.0
35
+ HEADERS = %w[METHOD PATH TO AS CONSTRAINTS].freeze
36
+
37
+ # @api private
38
+ # @since 2.0.0
39
+ def call(routes, **csv_opts)
40
+ ::CSV.generate(**DEFAULT_OPTIONS.merge(csv_opts)) do |csv|
41
+ csv << HEADERS if csv.write_headers?
42
+ routes.reduce(csv) do |acc, route|
43
+ route.head? ? acc : acc << row(route)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def row(route)
51
+ [
52
+ route.http_method.to_s,
53
+ route.path,
54
+ route.inspect_to,
55
+ route.as? ? route.inspect_as : "",
56
+ route.constraints? ? route.inspect_constraints : ""
57
+ ]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Router
5
+ # Renders a human friendly representation of the routes
6
+ #
7
+ # @api private
8
+ # @since 2.0.0
9
+ module Formatter
10
+ class HumanFriendly
11
+ # @api private
12
+ # @since 2.0.0
13
+ NEW_LINE = $/
14
+ private_constant :NEW_LINE
15
+
16
+ # @api private
17
+ # @since 2.0.0
18
+ SMALL_STRING_JUSTIFY_AMOUNT = 8
19
+ private_constant :SMALL_STRING_JUSTIFY_AMOUNT
20
+
21
+ # @api private
22
+ # @since 2.0.0
23
+ MEDIUM_STRING_JUSTIFY_AMOUNT = 20
24
+ private_constant :MEDIUM_STRING_JUSTIFY_AMOUNT
25
+
26
+ # @api private
27
+ # @since 2.0.0
28
+ LARGE_STRING_JUSTIFY_AMOUNT = 30
29
+ private_constant :LARGE_STRING_JUSTIFY_AMOUNT
30
+
31
+ # @api private
32
+ # @since 2.0.0
33
+ EXTRA_LARGE_STRING_JUSTIFY_AMOUNT = 40
34
+ private_constant :EXTRA_LARGE_STRING_JUSTIFY_AMOUNT
35
+
36
+ # @api private
37
+ # @since 2.0.0
38
+ def call(routes)
39
+ routes.filter_map(&method(:format_route_unless_head)).join(NEW_LINE)
40
+ end
41
+
42
+ private
43
+
44
+ def format_route_unless_head(route)
45
+ !route.head? &&
46
+ [
47
+ route.http_method.to_s.ljust(SMALL_STRING_JUSTIFY_AMOUNT),
48
+ route.path.ljust(LARGE_STRING_JUSTIFY_AMOUNT),
49
+ route.inspect_to.ljust(LARGE_STRING_JUSTIFY_AMOUNT),
50
+ route.as? ? "as #{route.inspect_as}".ljust(MEDIUM_STRING_JUSTIFY_AMOUNT) : "",
51
+ route.constraints? ? "(#{route.inspect_constraints})".ljust(EXTRA_LARGE_STRING_JUSTIFY_AMOUNT) : ""
52
+ ].join
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/router/formatter/human_friendly"
4
+
5
+ module Hanami
6
+ class Router
7
+ # Builds a representation of an array of routes according to a given formatter.
8
+ #
9
+ # @see Router.new
10
+ #
11
+ # @since 2.0.0
12
+ # @api private
13
+ class Inspector
14
+ # @param routes [Array<Hanami::Route>]
15
+ # @param formatter [#call] routes formatter, taking routes as an argument and returning its
16
+ # own representation (typically a string). Defaults to {Formatter::HumanFriendly}.
17
+ #
18
+ # @since 2.0.0
19
+ # @api public
20
+ def initialize(routes: [], formatter: Formatter::HumanFriendly.new)
21
+ @routes = routes
22
+ @formatter = formatter
23
+ end
24
+
25
+ # Adds a route to be inspected.
26
+ #
27
+ # @param route [Route]
28
+ #
29
+ # @since 2.0.0
30
+ # @api public
31
+ def add_route(route)
32
+ @routes.push(route)
33
+ end
34
+
35
+ # Calls the formatter for all added routes.
36
+ #
37
+ # @return [Any] Formatted routes
38
+ #
39
+ # @since 2.0.0
40
+ # @api public
41
+ def call(...)
42
+ @formatter.call(@routes, ...)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -36,8 +36,7 @@ module Hanami
36
36
  # @api private
37
37
  # @since 2.0.0
38
38
  #
39
- # rubocop:disable Metrics/MethodLength
40
- def get(segment)
39
+ def get(segment) # rubocop:disable Metrics/PerceivedComplexity
41
40
  return unless @variable || @fixed
42
41
 
43
42
  found = nil
@@ -55,7 +54,6 @@ module Hanami
55
54
 
56
55
  [found, captured&.named_captures]
57
56
  end
58
- # rubocop:enable Metrics/MethodLength
59
57
 
60
58
  # @api private
61
59
  # @since 2.0.0
@@ -74,7 +72,7 @@ module Hanami
74
72
  # @api private
75
73
  # @since 2.0.0
76
74
  def variable?(segment)
77
- /:/.match?(segment)
75
+ Router::ROUTE_VARIABLE_MATCHER.match?(segment)
78
76
  end
79
77
 
80
78
  # @api private
@@ -15,7 +15,7 @@ module Hanami
15
15
  #
16
16
  # @api private
17
17
  # @since 2.0.0
18
- def self.deep_symbolize(params) # rubocop:disable Metrics/MethodLength
18
+ def self.deep_symbolize(params)
19
19
  params.each_with_object({}) do |(key, value), output|
20
20
  output[key.to_sym] =
21
21
  case value
@@ -25,7 +25,7 @@ module Hanami
25
25
  # @api private
26
26
  def relative_join(path, separator = DEFAULT_SEPARATOR)
27
27
  _join(path.to_s)
28
- .gsub(DEFAULT_SEPARATOR_REGEXP, separator)[1..-1]
28
+ .gsub(DEFAULT_SEPARATOR_REGEXP, separator)[1..]
29
29
  end
30
30
 
31
31
  # @since 2.0.0
@@ -48,11 +48,11 @@ module Hanami
48
48
 
49
49
  # @since 2.0.0
50
50
  # @api private
51
- DEFAULT_SEPARATOR_REGEXP = /\//.freeze
51
+ DEFAULT_SEPARATOR_REGEXP = /\//
52
52
 
53
53
  # @since 2.0.0
54
54
  # @api private
55
- DOUBLE_DEFAULT_SEPARATOR_REGEXP = /[\/]{2,}/.freeze
55
+ DOUBLE_DEFAULT_SEPARATOR_REGEXP = /\/{2,}/
56
56
 
57
57
  # @since 2.0.0
58
58
  # @api private