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.
- checksums.yaml +4 -4
- data/.env +17 -0
- data/.github/workflows/lint.yaml +8 -6
- data/.github/workflows/release.yaml +4 -3
- data/.github/workflows/tests.yaml +5 -14
- data/.gitignore +2 -2
- data/.rubocop.yml +31 -0
- data/.vale.ini +6 -3
- data/Dockerfile +3 -2
- data/Gemfile +21 -0
- data/Gemfile.lock +233 -128
- data/Rakefile +7 -0
- data/docker-compose.yml +13 -14
- data/docs/guides/callbacks.md +3 -1
- data/docs/guides/configuration.md +10 -35
- data/docs/guides/development.md +67 -0
- data/docs/guides/handlers.md +7 -7
- data/docs/guides/jobs.md +54 -11
- data/docs/guides/networking/custom_adapters.md +31 -10
- data/docs/guides/pages.md +24 -22
- data/docs/guides/routing.md +116 -34
- data/docs/guides/tasks.md +30 -10
- data/docs/guides/tutorial.md +23 -17
- data/docs/guides/user_agents.md +11 -9
- data/lib/wayfarer/base.rb +9 -8
- data/lib/wayfarer/batch_completion.rb +18 -14
- data/lib/wayfarer/callbacks.rb +14 -14
- data/lib/wayfarer/cli/route_printer.rb +78 -96
- data/lib/wayfarer/cli.rb +12 -30
- data/lib/wayfarer/gc.rb +6 -1
- data/lib/wayfarer/kv.rb +28 -0
- data/lib/wayfarer/middleware/chain.rb +7 -1
- data/lib/wayfarer/middleware/content_type.rb +20 -15
- data/lib/wayfarer/middleware/dedup.rb +9 -3
- data/lib/wayfarer/middleware/dispatch.rb +7 -2
- data/lib/wayfarer/middleware/normalize.rb +4 -12
- data/lib/wayfarer/middleware/router.rb +1 -1
- data/lib/wayfarer/middleware/uri_parser.rb +4 -3
- data/lib/wayfarer/networking/context.rb +12 -1
- data/lib/wayfarer/networking/ferrum.rb +1 -4
- data/lib/wayfarer/networking/follow.rb +2 -1
- data/lib/wayfarer/networking/pool.rb +12 -7
- data/lib/wayfarer/networking/selenium.rb +15 -7
- data/lib/wayfarer/page.rb +0 -2
- data/lib/wayfarer/parsing/xml.rb +1 -1
- data/lib/wayfarer/parsing.rb +2 -5
- data/lib/wayfarer/redis/barrier.rb +15 -2
- data/lib/wayfarer/redis/counter.rb +1 -2
- data/lib/wayfarer/routing/dsl.rb +166 -31
- data/lib/wayfarer/routing/hash_stack.rb +33 -0
- data/lib/wayfarer/routing/matchers/custom.rb +8 -5
- data/lib/wayfarer/routing/matchers/{suffix.rb → empty_params.rb} +2 -6
- data/lib/wayfarer/routing/matchers/host.rb +15 -9
- data/lib/wayfarer/routing/matchers/path.rb +11 -33
- data/lib/wayfarer/routing/matchers/query.rb +41 -17
- data/lib/wayfarer/routing/matchers/result.rb +12 -0
- data/lib/wayfarer/routing/matchers/scheme.rb +13 -5
- data/lib/wayfarer/routing/matchers/url.rb +13 -5
- data/lib/wayfarer/routing/path_consumer.rb +130 -0
- data/lib/wayfarer/routing/path_finder.rb +151 -23
- data/lib/wayfarer/routing/result.rb +1 -1
- data/lib/wayfarer/routing/root_route.rb +14 -2
- data/lib/wayfarer/routing/route.rb +71 -14
- data/lib/wayfarer/routing/serializable.rb +28 -0
- data/lib/wayfarer/routing/sub_route.rb +53 -0
- data/lib/wayfarer/routing/target_route.rb +17 -1
- data/lib/wayfarer/stringify.rb +1 -2
- data/lib/wayfarer/task.rb +3 -5
- data/lib/wayfarer/uri/normalization.rb +120 -0
- data/lib/wayfarer.rb +50 -10
- data/mise.toml +2 -0
- data/mkdocs.yml +8 -17
- data/rake/lint.rake +0 -96
- data/rake/release.rake +5 -11
- data/rake/tests.rake +8 -4
- data/requirements.txt +1 -1
- data/spec/factories/job.rb +8 -0
- data/spec/factories/middleware.rb +2 -2
- data/spec/factories/path_finder.rb +11 -0
- data/spec/factories/redis.rb +19 -0
- data/spec/factories/task.rb +39 -1
- data/spec/spec_helpers.rb +50 -57
- data/spec/support/active_job_helpers.rb +8 -0
- data/spec/support/integration_helpers.rb +21 -0
- data/spec/support/redis_helpers.rb +9 -0
- data/spec/support/test_app.rb +64 -43
- data/spec/{base_spec.rb → wayfarer/base_spec.rb} +32 -36
- data/spec/wayfarer/batch_completion_spec.rb +142 -0
- data/spec/wayfarer/cli/job_spec.rb +88 -0
- data/spec/wayfarer/cli/routing_spec.rb +322 -0
- data/spec/{cli → wayfarer/cli}/version_spec.rb +1 -1
- data/spec/wayfarer/gc_spec.rb +29 -0
- data/spec/{handler_spec.rb → wayfarer/handler_spec.rb} +1 -3
- data/spec/{integration → wayfarer/integration}/callbacks_spec.rb +9 -6
- data/spec/wayfarer/integration/content_type_spec.rb +37 -0
- data/spec/wayfarer/integration/custom_routing_spec.rb +51 -0
- data/spec/{integration → wayfarer/integration}/gc_spec.rb +9 -13
- data/spec/{integration → wayfarer/integration}/handler_spec.rb +9 -10
- data/spec/{integration → wayfarer/integration}/page_spec.rb +8 -6
- data/spec/{integration → wayfarer/integration}/params_spec.rb +4 -4
- data/spec/{integration → wayfarer/integration}/parsing_spec.rb +7 -33
- data/spec/wayfarer/integration/retry_spec.rb +112 -0
- data/spec/{integration → wayfarer/integration}/stage_spec.rb +5 -5
- data/spec/{middleware → wayfarer/middleware}/batch_completion_spec.rb +4 -5
- data/spec/{middleware → wayfarer/middleware}/chain_spec.rb +20 -15
- data/spec/{middleware → wayfarer/middleware}/content_type_spec.rb +18 -21
- data/spec/{middleware → wayfarer/middleware}/controller_spec.rb +22 -20
- data/spec/wayfarer/middleware/dedup_spec.rb +66 -0
- data/spec/wayfarer/middleware/normalize_spec.rb +32 -0
- data/spec/{middleware → wayfarer/middleware}/router_spec.rb +18 -20
- data/spec/{middleware → wayfarer/middleware}/stage_spec.rb +11 -10
- data/spec/wayfarer/middleware/uri_parser_spec.rb +63 -0
- data/spec/{middleware → wayfarer/middleware}/user_agent_spec.rb +34 -32
- data/spec/wayfarer/networking/capybara_spec.rb +13 -0
- data/spec/{networking → wayfarer/networking}/context_spec.rb +46 -38
- data/spec/wayfarer/networking/ferrum_spec.rb +13 -0
- data/spec/{networking → wayfarer/networking}/follow_spec.rb +9 -4
- data/spec/wayfarer/networking/http_spec.rb +12 -0
- data/spec/{networking → wayfarer/networking}/pool_spec.rb +11 -9
- data/spec/wayfarer/networking/selenium_spec.rb +12 -0
- data/spec/{networking → wayfarer/networking}/strategy.rb +33 -54
- data/spec/{page_spec.rb → wayfarer/page_spec.rb} +3 -3
- data/spec/{parsing → wayfarer/parsing}/json_spec.rb +1 -1
- data/spec/{parsing/xml_spec.rb → wayfarer/parsing/xml_parse_spec.rb} +4 -3
- data/spec/{redis → wayfarer/redis}/barrier_spec.rb +5 -4
- data/spec/wayfarer/redis/counter_spec.rb +34 -0
- data/spec/{redis → wayfarer/redis}/pool_spec.rb +3 -2
- data/spec/{routing → wayfarer/routing}/dsl_spec.rb +12 -22
- data/spec/wayfarer/routing/hash_stack_spec.rb +63 -0
- data/spec/wayfarer/routing/integration_spec.rb +101 -0
- data/spec/wayfarer/routing/matchers/custom_spec.rb +39 -0
- data/spec/wayfarer/routing/matchers/host_spec.rb +56 -0
- data/spec/wayfarer/routing/matchers/matcher.rb +17 -0
- data/spec/wayfarer/routing/matchers/path_spec.rb +43 -0
- data/spec/wayfarer/routing/matchers/query_spec.rb +123 -0
- data/spec/wayfarer/routing/matchers/scheme_spec.rb +45 -0
- data/spec/wayfarer/routing/matchers/url_spec.rb +33 -0
- data/spec/wayfarer/routing/path_consumer_spec.rb +123 -0
- data/spec/wayfarer/routing/path_finder_spec.rb +409 -0
- data/spec/wayfarer/routing/root_route_spec.rb +51 -0
- data/spec/wayfarer/routing/route_spec.rb +74 -0
- data/spec/wayfarer/routing/sub_route_spec.rb +103 -0
- data/spec/wayfarer/uri/normalization_spec.rb +98 -0
- data/spec/wayfarer_spec.rb +2 -2
- data/wayfarer.gemspec +17 -28
- metadata +768 -246
- data/.rbenv-gemsets +0 -1
- data/.ruby-version +0 -1
- data/RELEASING.md +0 -17
- data/docs/cookbook/user_agent.md +0 -7
- data/docs/design.md +0 -36
- data/docs/guides/jobs/error_handling.md +0 -40
- data/docs/reference/configuration.md +0 -36
- data/spec/batch_completion_spec.rb +0 -104
- data/spec/cli/job_spec.rb +0 -74
- data/spec/cli/routing_spec.rb +0 -101
- data/spec/fixtures/dummy_job.rb +0 -9
- data/spec/gc_spec.rb +0 -17
- data/spec/integration/content_type_spec.rb +0 -145
- data/spec/integration/routing_spec.rb +0 -18
- data/spec/middleware/dedup_spec.rb +0 -71
- data/spec/middleware/dispatch_spec.rb +0 -59
- data/spec/middleware/normalize_spec.rb +0 -60
- data/spec/middleware/uri_parser_spec.rb +0 -53
- data/spec/networking/capybara_spec.rb +0 -12
- data/spec/networking/ferrum_spec.rb +0 -12
- data/spec/networking/http_spec.rb +0 -12
- data/spec/networking/selenium_spec.rb +0 -12
- data/spec/redis/counter_spec.rb +0 -44
- data/spec/routing/integration_spec.rb +0 -110
- data/spec/routing/matchers/custom_spec.rb +0 -31
- data/spec/routing/matchers/host_spec.rb +0 -49
- data/spec/routing/matchers/path_spec.rb +0 -43
- data/spec/routing/matchers/query_spec.rb +0 -137
- data/spec/routing/matchers/scheme_spec.rb +0 -25
- data/spec/routing/matchers/suffix_spec.rb +0 -41
- data/spec/routing/matchers/uri_spec.rb +0 -27
- data/spec/routing/path_finder_spec.rb +0 -33
- data/spec/routing/root_route_spec.rb +0 -29
- data/spec/routing/route_spec.rb +0 -43
- data/docs/{reference → guides}/cli.md +0 -0
- data/spec/{stringify_spec.rb → wayfarer/stringify_spec.rb} +2 -2
- /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
|
12
|
+
def initialize(delegate)
|
12
13
|
@delegate = delegate
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
|
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
|
20
|
-
{}
|
22
|
+
def to_h
|
23
|
+
{ custom: delegate.class.name }
|
21
24
|
end
|
22
25
|
end
|
23
26
|
end
|
@@ -3,17 +3,23 @@
|
|
3
3
|
module Wayfarer
|
4
4
|
module Routing
|
5
5
|
module Matchers
|
6
|
-
Host
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
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
|
-
|
8
|
-
:route,
|
9
|
-
:peeking,
|
10
|
-
:matcher
|
7
|
+
PATTERN_TYPE = "sinatra"
|
11
8
|
|
12
|
-
|
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
|
-
|
27
|
-
|
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
|
34
|
-
|
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
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
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
|
7
|
-
|
8
|
-
|
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
|
-
|
14
|
-
|
18
|
+
# TODO: Move query parameter parsing to PathFinder
|
19
|
+
parsed_query = CGI.parse(query)
|
15
20
|
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
35
|
+
def params(path_finder)
|
36
|
+
return {} unless evaluate(path_finder)
|
25
37
|
|
26
|
-
|
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
|
-
|
40
|
+
parsed_query
|
41
|
+
.select { |k, _| fields.keys.include?(k.to_sym) }
|
42
|
+
.transform_values(&:last)
|
31
43
|
end
|
32
|
-
|
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
|
|
@@ -3,13 +3,21 @@
|
|
3
3
|
module Wayfarer
|
4
4
|
module Routing
|
5
5
|
module Matchers
|
6
|
-
Scheme
|
7
|
-
|
8
|
-
|
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
|
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
|
7
|
-
|
8
|
-
|
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
|
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
|
-
|
7
|
-
|
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.
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
28
|
-
return
|
131
|
+
return false if stopped? || !match_history.last
|
132
|
+
return true unless route.leaf? && path_consumer.valid?
|
29
133
|
|
30
|
-
|
31
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
@@ -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
|
7
|
-
|
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
|