wayfarer 0.4.7 → 0.4.8

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.env +17 -0
  3. data/.github/workflows/lint.yaml +8 -6
  4. data/.github/workflows/release.yaml +4 -3
  5. data/.github/workflows/tests.yaml +5 -14
  6. data/.gitignore +2 -2
  7. data/.rubocop.yml +31 -0
  8. data/.vale.ini +6 -3
  9. data/Dockerfile +3 -2
  10. data/Gemfile +21 -0
  11. data/Gemfile.lock +233 -128
  12. data/Rakefile +7 -0
  13. data/docker-compose.yml +13 -14
  14. data/docs/guides/callbacks.md +3 -1
  15. data/docs/guides/configuration.md +10 -35
  16. data/docs/guides/development.md +67 -0
  17. data/docs/guides/handlers.md +7 -7
  18. data/docs/guides/jobs.md +54 -11
  19. data/docs/guides/networking/custom_adapters.md +31 -10
  20. data/docs/guides/pages.md +24 -22
  21. data/docs/guides/routing.md +116 -34
  22. data/docs/guides/tasks.md +30 -10
  23. data/docs/guides/tutorial.md +23 -17
  24. data/docs/guides/user_agents.md +11 -9
  25. data/lib/wayfarer/base.rb +9 -8
  26. data/lib/wayfarer/batch_completion.rb +18 -14
  27. data/lib/wayfarer/callbacks.rb +14 -14
  28. data/lib/wayfarer/cli/route_printer.rb +78 -96
  29. data/lib/wayfarer/cli.rb +12 -30
  30. data/lib/wayfarer/gc.rb +6 -1
  31. data/lib/wayfarer/kv.rb +28 -0
  32. data/lib/wayfarer/middleware/chain.rb +7 -1
  33. data/lib/wayfarer/middleware/content_type.rb +20 -15
  34. data/lib/wayfarer/middleware/dedup.rb +9 -3
  35. data/lib/wayfarer/middleware/dispatch.rb +7 -2
  36. data/lib/wayfarer/middleware/normalize.rb +4 -12
  37. data/lib/wayfarer/middleware/router.rb +1 -1
  38. data/lib/wayfarer/middleware/uri_parser.rb +4 -3
  39. data/lib/wayfarer/networking/context.rb +12 -1
  40. data/lib/wayfarer/networking/ferrum.rb +1 -4
  41. data/lib/wayfarer/networking/follow.rb +2 -1
  42. data/lib/wayfarer/networking/pool.rb +12 -7
  43. data/lib/wayfarer/networking/selenium.rb +15 -7
  44. data/lib/wayfarer/page.rb +0 -2
  45. data/lib/wayfarer/parsing/xml.rb +1 -1
  46. data/lib/wayfarer/parsing.rb +2 -5
  47. data/lib/wayfarer/redis/barrier.rb +15 -2
  48. data/lib/wayfarer/redis/counter.rb +1 -2
  49. data/lib/wayfarer/routing/dsl.rb +166 -31
  50. data/lib/wayfarer/routing/hash_stack.rb +33 -0
  51. data/lib/wayfarer/routing/matchers/custom.rb +8 -5
  52. data/lib/wayfarer/routing/matchers/{suffix.rb → empty_params.rb} +2 -6
  53. data/lib/wayfarer/routing/matchers/host.rb +15 -9
  54. data/lib/wayfarer/routing/matchers/path.rb +11 -33
  55. data/lib/wayfarer/routing/matchers/query.rb +41 -17
  56. data/lib/wayfarer/routing/matchers/result.rb +12 -0
  57. data/lib/wayfarer/routing/matchers/scheme.rb +13 -5
  58. data/lib/wayfarer/routing/matchers/url.rb +13 -5
  59. data/lib/wayfarer/routing/path_consumer.rb +130 -0
  60. data/lib/wayfarer/routing/path_finder.rb +151 -23
  61. data/lib/wayfarer/routing/result.rb +1 -1
  62. data/lib/wayfarer/routing/root_route.rb +14 -2
  63. data/lib/wayfarer/routing/route.rb +71 -14
  64. data/lib/wayfarer/routing/serializable.rb +28 -0
  65. data/lib/wayfarer/routing/sub_route.rb +53 -0
  66. data/lib/wayfarer/routing/target_route.rb +17 -1
  67. data/lib/wayfarer/stringify.rb +1 -2
  68. data/lib/wayfarer/task.rb +3 -5
  69. data/lib/wayfarer/uri/normalization.rb +120 -0
  70. data/lib/wayfarer.rb +50 -10
  71. data/mise.toml +2 -0
  72. data/mkdocs.yml +8 -17
  73. data/rake/lint.rake +0 -96
  74. data/rake/release.rake +5 -11
  75. data/rake/tests.rake +8 -4
  76. data/requirements.txt +1 -1
  77. data/spec/factories/job.rb +8 -0
  78. data/spec/factories/middleware.rb +2 -2
  79. data/spec/factories/path_finder.rb +11 -0
  80. data/spec/factories/redis.rb +19 -0
  81. data/spec/factories/task.rb +39 -1
  82. data/spec/spec_helpers.rb +50 -57
  83. data/spec/support/active_job_helpers.rb +8 -0
  84. data/spec/support/integration_helpers.rb +21 -0
  85. data/spec/support/redis_helpers.rb +9 -0
  86. data/spec/support/test_app.rb +64 -43
  87. data/spec/{base_spec.rb → wayfarer/base_spec.rb} +32 -36
  88. data/spec/wayfarer/batch_completion_spec.rb +142 -0
  89. data/spec/wayfarer/cli/job_spec.rb +88 -0
  90. data/spec/wayfarer/cli/routing_spec.rb +322 -0
  91. data/spec/{cli → wayfarer/cli}/version_spec.rb +1 -1
  92. data/spec/wayfarer/gc_spec.rb +29 -0
  93. data/spec/{handler_spec.rb → wayfarer/handler_spec.rb} +1 -3
  94. data/spec/{integration → wayfarer/integration}/callbacks_spec.rb +9 -6
  95. data/spec/wayfarer/integration/content_type_spec.rb +37 -0
  96. data/spec/wayfarer/integration/custom_routing_spec.rb +51 -0
  97. data/spec/{integration → wayfarer/integration}/gc_spec.rb +9 -13
  98. data/spec/{integration → wayfarer/integration}/handler_spec.rb +9 -10
  99. data/spec/{integration → wayfarer/integration}/page_spec.rb +8 -6
  100. data/spec/{integration → wayfarer/integration}/params_spec.rb +4 -4
  101. data/spec/{integration → wayfarer/integration}/parsing_spec.rb +7 -33
  102. data/spec/wayfarer/integration/retry_spec.rb +112 -0
  103. data/spec/{integration → wayfarer/integration}/stage_spec.rb +5 -5
  104. data/spec/{middleware → wayfarer/middleware}/batch_completion_spec.rb +4 -5
  105. data/spec/{middleware → wayfarer/middleware}/chain_spec.rb +20 -15
  106. data/spec/{middleware → wayfarer/middleware}/content_type_spec.rb +18 -21
  107. data/spec/{middleware → wayfarer/middleware}/controller_spec.rb +22 -20
  108. data/spec/wayfarer/middleware/dedup_spec.rb +66 -0
  109. data/spec/wayfarer/middleware/normalize_spec.rb +32 -0
  110. data/spec/{middleware → wayfarer/middleware}/router_spec.rb +18 -20
  111. data/spec/{middleware → wayfarer/middleware}/stage_spec.rb +11 -10
  112. data/spec/wayfarer/middleware/uri_parser_spec.rb +63 -0
  113. data/spec/{middleware → wayfarer/middleware}/user_agent_spec.rb +34 -32
  114. data/spec/wayfarer/networking/capybara_spec.rb +13 -0
  115. data/spec/{networking → wayfarer/networking}/context_spec.rb +46 -38
  116. data/spec/wayfarer/networking/ferrum_spec.rb +13 -0
  117. data/spec/{networking → wayfarer/networking}/follow_spec.rb +9 -4
  118. data/spec/wayfarer/networking/http_spec.rb +12 -0
  119. data/spec/{networking → wayfarer/networking}/pool_spec.rb +11 -9
  120. data/spec/wayfarer/networking/selenium_spec.rb +12 -0
  121. data/spec/{networking → wayfarer/networking}/strategy.rb +33 -54
  122. data/spec/{page_spec.rb → wayfarer/page_spec.rb} +3 -3
  123. data/spec/{parsing → wayfarer/parsing}/json_spec.rb +1 -1
  124. data/spec/{parsing/xml_spec.rb → wayfarer/parsing/xml_parse_spec.rb} +4 -3
  125. data/spec/{redis → wayfarer/redis}/barrier_spec.rb +5 -4
  126. data/spec/wayfarer/redis/counter_spec.rb +34 -0
  127. data/spec/{redis → wayfarer/redis}/pool_spec.rb +3 -2
  128. data/spec/{routing → wayfarer/routing}/dsl_spec.rb +12 -22
  129. data/spec/wayfarer/routing/hash_stack_spec.rb +63 -0
  130. data/spec/wayfarer/routing/integration_spec.rb +101 -0
  131. data/spec/wayfarer/routing/matchers/custom_spec.rb +39 -0
  132. data/spec/wayfarer/routing/matchers/host_spec.rb +56 -0
  133. data/spec/wayfarer/routing/matchers/matcher.rb +17 -0
  134. data/spec/wayfarer/routing/matchers/path_spec.rb +43 -0
  135. data/spec/wayfarer/routing/matchers/query_spec.rb +123 -0
  136. data/spec/wayfarer/routing/matchers/scheme_spec.rb +45 -0
  137. data/spec/wayfarer/routing/matchers/url_spec.rb +33 -0
  138. data/spec/wayfarer/routing/path_consumer_spec.rb +123 -0
  139. data/spec/wayfarer/routing/path_finder_spec.rb +409 -0
  140. data/spec/wayfarer/routing/root_route_spec.rb +51 -0
  141. data/spec/wayfarer/routing/route_spec.rb +74 -0
  142. data/spec/wayfarer/routing/sub_route_spec.rb +103 -0
  143. data/spec/wayfarer/uri/normalization_spec.rb +98 -0
  144. data/spec/wayfarer_spec.rb +2 -2
  145. data/wayfarer.gemspec +17 -28
  146. metadata +768 -246
  147. data/.rbenv-gemsets +0 -1
  148. data/.ruby-version +0 -1
  149. data/RELEASING.md +0 -17
  150. data/docs/cookbook/user_agent.md +0 -7
  151. data/docs/design.md +0 -36
  152. data/docs/guides/jobs/error_handling.md +0 -40
  153. data/docs/reference/configuration.md +0 -36
  154. data/spec/batch_completion_spec.rb +0 -104
  155. data/spec/cli/job_spec.rb +0 -74
  156. data/spec/cli/routing_spec.rb +0 -101
  157. data/spec/fixtures/dummy_job.rb +0 -9
  158. data/spec/gc_spec.rb +0 -17
  159. data/spec/integration/content_type_spec.rb +0 -145
  160. data/spec/integration/routing_spec.rb +0 -18
  161. data/spec/middleware/dedup_spec.rb +0 -71
  162. data/spec/middleware/dispatch_spec.rb +0 -59
  163. data/spec/middleware/normalize_spec.rb +0 -60
  164. data/spec/middleware/uri_parser_spec.rb +0 -53
  165. data/spec/networking/capybara_spec.rb +0 -12
  166. data/spec/networking/ferrum_spec.rb +0 -12
  167. data/spec/networking/http_spec.rb +0 -12
  168. data/spec/networking/selenium_spec.rb +0 -12
  169. data/spec/redis/counter_spec.rb +0 -44
  170. data/spec/routing/integration_spec.rb +0 -110
  171. data/spec/routing/matchers/custom_spec.rb +0 -31
  172. data/spec/routing/matchers/host_spec.rb +0 -49
  173. data/spec/routing/matchers/path_spec.rb +0 -43
  174. data/spec/routing/matchers/query_spec.rb +0 -137
  175. data/spec/routing/matchers/scheme_spec.rb +0 -25
  176. data/spec/routing/matchers/suffix_spec.rb +0 -41
  177. data/spec/routing/matchers/uri_spec.rb +0 -27
  178. data/spec/routing/path_finder_spec.rb +0 -33
  179. data/spec/routing/root_route_spec.rb +0 -29
  180. data/spec/routing/route_spec.rb +0 -43
  181. data/docs/{reference → guides}/cli.md +0 -0
  182. data/spec/{stringify_spec.rb → wayfarer/stringify_spec.rb} +2 -2
  183. /data/spec/{task_spec.rb → wayfarer/task_spec.rb} +0 -0
@@ -5,19 +5,22 @@ module Wayfarer
5
5
  module Matchers
6
6
  class Custom
7
7
  include Stringify
8
+ include EmptyParams
8
9
 
9
10
  attr_reader :delegate
10
11
 
11
- def initialize(delegate = proc)
12
+ def initialize(delegate)
12
13
  @delegate = delegate
13
14
  end
14
15
 
15
- def match(url)
16
- !!delegate.call(url)
16
+ def evaluate(path_finder)
17
+ Wayfarer::Routing::RootRoute.new.tap do |route|
18
+ delegate.call(route, path_finder.uri, path_finder.task)
19
+ end
17
20
  end
18
21
 
19
- def params(_)
20
- {}
22
+ def to_h
23
+ { custom: delegate.class.name }
21
24
  end
22
25
  end
23
26
  end
@@ -3,13 +3,9 @@
3
3
  module Wayfarer
4
4
  module Routing
5
5
  module Matchers
6
- Suffix = Struct.new(:suffix) do
7
- def match(url)
8
- url.path.end_with?(suffix)
9
- end
10
-
6
+ module EmptyParams
11
7
  def params(_)
12
- {}
8
+ Wayfarer::Routing::Route::EMPTY_PARAMS
13
9
  end
14
10
  end
15
11
  end
@@ -3,17 +3,23 @@
3
3
  module Wayfarer
4
4
  module Routing
5
5
  module Matchers
6
- Host = Struct.new(:host) do
7
- # rubocop:disable Style/CaseEquality
8
- def match(url)
9
- # url.host excludes the port
10
- # TODO: Test case, docs
11
- host === url.authority
6
+ class Host
7
+ include EmptyParams
8
+
9
+ attr_reader :host
10
+
11
+ def initialize(host)
12
+ @host = host
13
+ end
14
+
15
+ def evaluate(path_finder)
16
+ # rubocop:disable Style/CaseEquality -- String and Regexp matching
17
+ @host === path_finder.uri.host
18
+ # rubocop:enable Style/CaseEquality
12
19
  end
13
- # rubocop:enable Style/CaseEquality
14
20
 
15
- def params(_)
16
- {}
21
+ def to_h
22
+ { name: host }
17
23
  end
18
24
  end
19
25
  end
@@ -4,46 +4,24 @@ module Wayfarer
4
4
  module Routing
5
5
  module Matchers
6
6
  class Path
7
- attr_reader :path,
8
- :route,
9
- :peeking,
10
- :matcher
7
+ PATTERN_TYPE = "sinatra"
11
8
 
12
- def initialize(path, route)
13
- @path = path
14
- @route = route
15
- @peeking = false
16
- @matcher = Mustermann.new(path, type: "sinatra") # TODO: Add type constant
17
- end
18
-
19
- def match(url)
20
- # TODO: Ditch this and add a parameter to the path DSL#path
21
- #
22
- # If the route's branch contains other path matchers in child routes,
23
- # match the beginning of the path (peeking), instead of the whole path.
24
- route.accept(self)
9
+ attr_reader :pattern
25
10
 
26
- !!(if peeking
27
- matcher.peek(url.path)
28
- else
29
- matcher.match(url.path)
30
- end)
11
+ def initialize(pattern)
12
+ @pattern = Mustermann.new(pattern, type: PATTERN_TYPE)
31
13
  end
32
14
 
33
- def params(url)
34
- return {} unless match(url)
35
-
36
- matcher.params(url.path) || {}
15
+ def evaluate(path_finder)
16
+ path_finder.path_consumer.current_state.valid?
37
17
  end
38
18
 
39
- def visit(route)
40
- return true if route == self.route
41
- return true if route.is_a?(TargetRoute)
42
-
43
- return unless route.matcher.is_a?(self.class)
19
+ def params(path_finder)
20
+ path_finder.path_consumer.current_state.params
21
+ end
44
22
 
45
- @peeking = true
46
- false
23
+ def to_h
24
+ { pattern: pattern.to_s }
47
25
  end
48
26
  end
49
27
  end
@@ -3,33 +3,56 @@
3
3
  module Wayfarer
4
4
  module Routing
5
5
  module Matchers
6
- Query = Struct.new(:fields) do
7
- def match(url)
8
- query = url.query
6
+ class Query
7
+ attr_reader :fields
8
+
9
+ def initialize(fields)
10
+ @fields = fields
11
+ end
12
+
13
+ def evaluate(path_finder)
14
+ query = path_finder.uri.query
9
15
 
10
- # CGI::parse throws a NoMethodError if the query is an empty string
11
16
  return false if query.nil? || query.empty?
12
17
 
13
- CGI.parse(query).none? { |field, vals| violates?(field, vals) }
14
- end
18
+ # TODO: Move query parameter parsing to PathFinder
19
+ parsed_query = CGI.parse(query)
15
20
 
16
- def params(url)
17
- return {} unless match(url)
21
+ # For every expected field in `fields`,
22
+ # check that it is present and does not violate the constraint.
23
+ fields.all? do |(field_key, constraint)|
24
+ param_vals = parsed_query[field_key.to_s]
18
25
 
19
- CGI.parse(url.query)
20
- .select { |(k, _)| fields.keys.include?(k.to_sym) }
21
- .transform_values(&:last)
26
+ # If the param is absent, mismatch
27
+ if param_vals.nil? || param_vals.empty?
28
+ false
29
+ else
30
+ !violates_constraint?(constraint, param_vals)
31
+ end
32
+ end
22
33
  end
23
34
 
24
- private
35
+ def params(path_finder)
36
+ return {} unless evaluate(path_finder)
25
37
 
26
- # rubocop:disable Lint/AssignmentInCondition
27
- def violates?(field, vals)
28
- return false unless constraint = fields[field.to_sym]
38
+ parsed_query = CGI.parse(path_finder.uri.query)
29
39
 
30
- violates_constraint?(constraint, vals)
40
+ parsed_query
41
+ .select { |k, _| fields.keys.include?(k.to_sym) }
42
+ .transform_values(&:last)
31
43
  end
32
- # rubocop:enable Lint/AssignmentInCondition
44
+
45
+ def to_h
46
+ fields.transform_values do |val|
47
+ case val
48
+ when Regexp then val.inspect
49
+ when Range then { min: val.min, max: val.max }
50
+ else val
51
+ end
52
+ end
53
+ end
54
+
55
+ private
33
56
 
34
57
  def violates_constraint?(constraint, vals)
35
58
  case constraint
@@ -37,6 +60,7 @@ module Wayfarer
37
60
  when Integer then violates_integer?(constraint, vals)
38
61
  when Regexp then violates_regexp?(constraint, vals)
39
62
  when Range then violates_range?(constraint, vals)
63
+ else false
40
64
  end
41
65
  end
42
66
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ module Result
7
+ Match = Class.new.include(Singleton)
8
+ Mismatch = Class.new.include(Singleton)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -3,13 +3,21 @@
3
3
  module Wayfarer
4
4
  module Routing
5
5
  module Matchers
6
- Scheme = Struct.new(:scheme) do
7
- def match(url)
8
- url.scheme == scheme.to_s
6
+ class Scheme
7
+ include EmptyParams
8
+
9
+ attr_reader :scheme
10
+
11
+ def initialize(scheme)
12
+ @scheme = scheme
13
+ end
14
+
15
+ def evaluate(path_finder)
16
+ path_finder.uri.scheme == scheme.to_s
9
17
  end
10
18
 
11
- def params(_)
12
- {}
19
+ def to_h
20
+ { scheme: scheme }
13
21
  end
14
22
  end
15
23
  end
@@ -3,13 +3,21 @@
3
3
  module Wayfarer
4
4
  module Routing
5
5
  module Matchers
6
- URL = Struct.new(:url) do
7
- def match(url)
8
- url == Addressable::URI.parse(self.url)
6
+ class URL
7
+ include EmptyParams
8
+
9
+ attr_reader :url
10
+
11
+ def initialize(url)
12
+ @url = Addressable::URI.parse(url)
13
+ end
14
+
15
+ def evaluate(path_finder)
16
+ path_finder.uri == url
9
17
  end
10
18
 
11
- def params(_)
12
- {}
19
+ def to_h
20
+ { url: url.to_s }
13
21
  end
14
22
  end
15
23
  end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ # Tracks path consumption during route matching.
6
+ #
7
+ # @api private
8
+ class PathConsumer
9
+ # The result of consuming part of a path.
10
+ #
11
+ # @!attribute offset
12
+ # @return [Integer, nil] index into the path string
13
+ # @!attribute params
14
+ # @return [Hash] collected path parameters
15
+ # @!attribute valid?
16
+ # @return [Boolean] whether the current state is valid
17
+ ConsumptionState = Struct.new(:offset, :params, :valid?, keyword_init: true)
18
+
19
+ INITIAL_OFFSET = nil
20
+
21
+ # Initial valid state, before any path is consumed.
22
+ INITIAL_STATE = ConsumptionState.new(
23
+ offset: INITIAL_OFFSET,
24
+ params: Wayfarer::Routing::Route::EMPTY_PARAMS,
25
+ valid?: true
26
+ ).freeze
27
+
28
+ # @return [String] full path being consumed
29
+ attr_reader :path
30
+
31
+ def initialize(path)
32
+ @path = File.join(File::SEPARATOR, path)
33
+ @states = [INITIAL_STATE]
34
+ end
35
+
36
+ # Whether the path was fully consumed or not consumed at all.
37
+ #
38
+ # @return [Boolean]
39
+ def valid?
40
+ current_state.valid? && consumed?
41
+ end
42
+
43
+ # The most recent consumption state.
44
+ #
45
+ # @return [ConsumptionState]
46
+ def current_state
47
+ states.last
48
+ end
49
+
50
+ # Attempts to consume the path if the route has a {Matchers::Path}
51
+ # and pushes a new state as a result.
52
+ #
53
+ # @param route [Wayfarer::Routing::Route]
54
+ def push(route)
55
+ states.push(next_state(route))
56
+ end
57
+
58
+ # Reverts to the previous state.
59
+ #
60
+ # @return [ConsumptionState] the popped state
61
+ def pop
62
+ states.pop
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :states
68
+
69
+ def current_offset
70
+ current_state.offset || 0
71
+ end
72
+
73
+ def consumed?
74
+ nothing_consumed? || fully_consumed?
75
+ end
76
+
77
+ def nothing_consumed?
78
+ current_state.offset == INITIAL_OFFSET
79
+ end
80
+
81
+ def fully_consumed?
82
+ current_offset >= path.length || trailing_slash?
83
+ end
84
+
85
+ def trailing_slash?
86
+ current_offset + 1 == path.length && path[-1] == File::SEPARATOR
87
+ end
88
+
89
+ def sub_path
90
+ return File::SEPARATOR if (offset = current_offset) >= path.length
91
+
92
+ File.join(File::SEPARATOR, path[offset..])
93
+ end
94
+
95
+ def next_state(route)
96
+ return unchanged_state unless route.matcher.is_a?(Wayfarer::Routing::Matchers::Path)
97
+
98
+ consumed_state(route.matcher.pattern)
99
+ end
100
+
101
+ def consumed_state(pattern)
102
+ return unchanged_state unless current_state.valid?
103
+
104
+ match_data = pattern.peek_match(sub_path) or return invalid_state
105
+ matched_path = match_data.to_s
106
+
107
+ new_offset = current_offset + matched_path.length
108
+ new_params = current_state.params.merge(match_data.named_captures)
109
+
110
+ ConsumptionState.new(
111
+ offset: new_offset,
112
+ params: new_params,
113
+ valid?: true
114
+ )
115
+ end
116
+
117
+ def unchanged_state
118
+ current_state.dup
119
+ end
120
+
121
+ def invalid_state
122
+ ConsumptionState.new(
123
+ offset: current_offset,
124
+ params: current_state.params,
125
+ valid?: false
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
@@ -2,44 +2,172 @@
2
2
 
3
3
  module Wayfarer
4
4
  module Routing
5
+ # Encapsulates all state needed to route a URL.
6
+ #
7
+ # @api private
5
8
  class PathFinder
6
- def self.result(route, url)
7
- finder = new(url)
9
+ include KV
10
+
11
+ # The result of traversing the route with a new {PathFinder} for `url`.
12
+ #
13
+ # @param route [Wayfarer::Routing::Route]
14
+ # @param task [Wayfarer::Task]
15
+ # @param callback [Proc]
16
+ # @return [Result::Match, Result::Mismatch]
17
+ def self.result(route, task, &)
18
+ accept_finder(route, new(task, &))
19
+ end
20
+
21
+ # The result of traversing the route with an existing {PathFinder}.
22
+ #
23
+ # @param route [Wayfarer::Routing::Route]
24
+ # @param path_finder [PathFinder]
25
+ # @return [Result::Match, Result::Mismatch]
26
+ def self.sub_result(route, path_finder)
27
+ accept_finder(route, path_finder)
28
+ end
29
+
30
+ # @param route [Wayfarer::Routing::Route]
31
+ # @param finder [PathFinder]
32
+ # @return [Result::Match, Result::Mismatch]
33
+ private_class_method def self.accept_finder(route, finder)
8
34
  route.accept(finder)
9
- return Result::Mismatch.new if finder.path.none?
35
+ return Result::Mismatch.instance unless finder.found?
10
36
 
11
37
  Result::Match.new(finder.action, finder.params)
12
38
  end
13
39
 
14
- attr_reader :url,
15
- :path,
16
- :action,
17
- :params
40
+ # @return [Wayfarer::Task]
41
+ attr_reader :task
42
+
43
+ # @return [Addressable::URI]
44
+ attr_reader :uri
45
+
46
+ # @return [String]
47
+ attr_reader :path
48
+
49
+ # @return [Wayfarer::Routing::PathConsumer]
50
+ attr_reader :path_consumer
51
+
52
+ # @return [Array<Wayfarer::Routing::Route>]
53
+ attr_reader :current_path, :found_path
54
+
55
+ # @return [Object, nil]
56
+ attr_reader :action
57
+
58
+ # @return [Hash]
59
+ attr_reader :params
60
+
61
+ # @return [Wayfarer::Routing::HashStack]
62
+ attr_reader :params_stack
63
+
64
+ # @param task [Wayfarer::Task]
65
+ # @param path_consumer [PathConsumer] internal use only
66
+ # @param params_stack [HashStack] internal use only
67
+ # @param stop_when_found [Boolean] whether traversal halts on match
68
+ def initialize(
69
+ task,
70
+ path_consumer: initial_path_consumer(task[:uri]),
71
+ params_stack: Wayfarer::Routing::HashStack.empty,
72
+ stop_when_found: true,
73
+ &callback
74
+ )
75
+ @task = task
76
+ @uri = task[:uri]
77
+ @current_path = []
78
+ @actions = []
79
+ @path_consumer = path_consumer
80
+ @params_stack = params_stack
81
+ @kv = kv
82
+ @callback = callback
83
+ @stop_when_found = stop_when_found
84
+ @match_history = []
85
+ end
86
+
87
+ # Whether a route has matched and consumed the URL.
88
+ #
89
+ # @return [Boolean]
90
+ def found?
91
+ !!found_path
92
+ end
93
+
94
+ # @return [Wayfarer::Routing::Route, nil]
95
+ def current_route
96
+ current_path.last
97
+ end
98
+
99
+ # Enters a route node, updating state.
100
+ #
101
+ # @param route [Wayfarer::Routing::Route]
102
+ # @return [void]
103
+ def enter(route)
104
+ return if stopped?
105
+
106
+ current_path.push(route)
107
+ path_consumer.push(route)
108
+ params_stack.push(route.params(self))
109
+ actions.prepend(route.action(self))
110
+ match_history.push(match(route))
111
+ end
112
+
113
+ # Leaves the current route, restoring previous state.
114
+ #
115
+ # @return [void]
116
+ def leave
117
+ return if found? && stop_when_found?
18
118
 
19
- def initialize(url)
20
- @url = url
21
- @path = []
22
- @action = nil
23
- @params = {}
119
+ actions.pop
120
+ params_stack.pop
121
+ path_consumer.pop
122
+ current_path.pop
123
+ match_history.pop
24
124
  end
25
125
 
126
+ # Visits a {Route}.
127
+ #
128
+ # @param route [Wayfarer::Routing::Route]
129
+ # @return [Boolean] whether to continue traversal
26
130
  def visit(route)
27
- return false if path.any?
28
- return false unless route.match(url)
131
+ return false if stopped? || !match_history.last
132
+ return true unless route.leaf? && path_consumer.valid?
29
133
 
30
- follow(route) if route.children.none?
31
- true
134
+ found! if !found? && match_history.all?
135
+
136
+ !stop_when_found?
137
+ end
138
+
139
+ # Whether traversal should stop.
140
+ #
141
+ # @return [Boolean]
142
+ def stopped?
143
+ found? && stop_when_found?
32
144
  end
33
145
 
34
146
  private
35
147
 
36
- # Follows the route back to the root. Collects parameters and action on
37
- # the way.
38
- def follow(route)
39
- path.unshift(route)
40
- @params.merge!(route.matcher.params(url))
41
- @action = route.action unless action
42
- follow(route.parent) if route.parent
148
+ attr_reader :actions, :callback, :match_history
149
+ attr_writer :found_path, :action, :params
150
+
151
+ def found!
152
+ self.params = params_stack.to_h
153
+ self.action = actions.find(&:present?)
154
+ self.found_path = current_path.clone.freeze
155
+ end
156
+
157
+ def stop_when_found?
158
+ !!@stop_when_found
159
+ end
160
+
161
+ def match(route)
162
+ route.match(self).tap { |result| callback&.call(route, result, self) }
163
+ end
164
+
165
+ def initial_path_consumer(uri)
166
+ Wayfarer::Routing::PathConsumer.new(url_path(uri))
167
+ end
168
+
169
+ def url_path(uri)
170
+ uri.host ? uri.path : ""
43
171
  end
44
172
  end
45
173
  end
@@ -4,7 +4,7 @@ module Wayfarer
4
4
  module Routing
5
5
  module Result
6
6
  Match = Struct.new(:action, :params)
7
- Mismatch = Class.new
7
+ Mismatch = Class.new.include(Singleton)
8
8
  end
9
9
  end
10
10
  end
@@ -2,9 +2,21 @@
2
2
 
3
3
  module Wayfarer
4
4
  module Routing
5
+ # Routing tree root.
5
6
  class RootRoute < Route
6
- def invoke(url)
7
- PathFinder.result(self, url)
7
+ def initialize
8
+ super(parent: nil)
9
+ end
10
+
11
+ # @param [url] Addressable::URI
12
+ # @return [Result::Match, Result::Mismatch]
13
+ def invoke(task)
14
+ PathFinder.result(self, task)
15
+ end
16
+
17
+ # @return [true, false]
18
+ def evaluate(path_finder)
19
+ path_finder.uri.absolute? && !leaf? # Don't route URLs without routes declared
8
20
  end
9
21
  end
10
22
  end