wayfarer 0.4.0 → 0.4.1

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/Gemfile.lock +20 -15
  4. data/docs/cookbook/user_agent.md +1 -1
  5. data/docs/guides/browser_automation/capybara.md +64 -1
  6. data/docs/guides/browser_automation/custom_adapters.md +100 -0
  7. data/docs/guides/browser_automation/ferrum.md +3 -3
  8. data/docs/guides/browser_automation/selenium.md +7 -5
  9. data/docs/guides/callbacks.md +117 -10
  10. data/docs/guides/configuration.md +16 -10
  11. data/docs/guides/error_handling.md +9 -5
  12. data/docs/guides/networking.md +77 -3
  13. data/docs/index.md +9 -1
  14. data/docs/reference/api/base.md +4 -4
  15. data/docs/reference/configuration_keys.md +42 -0
  16. data/docs/reference/environment_variables.md +25 -27
  17. data/lib/wayfarer/base.rb +7 -17
  18. data/lib/wayfarer/callbacks.rb +71 -0
  19. data/lib/wayfarer/cli/base.rb +5 -1
  20. data/lib/wayfarer/cli/job.rb +7 -3
  21. data/lib/wayfarer/cli/route.rb +2 -2
  22. data/lib/wayfarer/cli/route_printer.rb +7 -7
  23. data/lib/wayfarer/config/capybara.rb +10 -0
  24. data/lib/wayfarer/config/ferrum.rb +11 -0
  25. data/lib/wayfarer/config/networking.rb +26 -0
  26. data/lib/wayfarer/config/redis.rb +14 -0
  27. data/lib/wayfarer/config/root.rb +11 -0
  28. data/lib/wayfarer/config/selenium.rb +21 -0
  29. data/lib/wayfarer/config/strconv.rb +45 -0
  30. data/lib/wayfarer/config/struct.rb +72 -0
  31. data/lib/wayfarer/gc.rb +3 -7
  32. data/lib/wayfarer/middleware/fetch.rb +7 -3
  33. data/lib/wayfarer/middleware/router.rb +2 -2
  34. data/lib/wayfarer/middleware/worker.rb +12 -9
  35. data/lib/wayfarer/networking/capybara.rb +28 -0
  36. data/lib/wayfarer/networking/context.rb +36 -0
  37. data/lib/wayfarer/networking/ferrum.rb +17 -52
  38. data/lib/wayfarer/networking/http.rb +34 -0
  39. data/lib/wayfarer/networking/pool.rb +15 -10
  40. data/lib/wayfarer/networking/result.rb +1 -1
  41. data/lib/wayfarer/networking/selenium.rb +20 -47
  42. data/lib/wayfarer/networking/strategy.rb +38 -0
  43. data/lib/wayfarer/page.rb +2 -3
  44. data/lib/wayfarer/redis/pool.rb +3 -1
  45. data/lib/wayfarer/routing/dsl.rb +8 -8
  46. data/lib/wayfarer/routing/matchers/custom.rb +23 -0
  47. data/lib/wayfarer/routing/matchers/host.rb +19 -0
  48. data/lib/wayfarer/routing/matchers/path.rb +48 -0
  49. data/lib/wayfarer/routing/matchers/query.rb +63 -0
  50. data/lib/wayfarer/routing/matchers/scheme.rb +17 -0
  51. data/lib/wayfarer/routing/matchers/suffix.rb +17 -0
  52. data/lib/wayfarer/routing/matchers/url.rb +17 -0
  53. data/lib/wayfarer/routing/route.rb +1 -1
  54. data/lib/wayfarer.rb +9 -9
  55. data/spec/base_spec.rb +14 -0
  56. data/spec/callbacks_spec.rb +102 -0
  57. data/spec/cli/job_spec.rb +6 -6
  58. data/spec/config/capybara_spec.rb +18 -0
  59. data/spec/config/ferrum_spec.rb +24 -0
  60. data/spec/config/networking_spec.rb +73 -0
  61. data/spec/config/redis_spec.rb +32 -0
  62. data/spec/config/root_spec.rb +31 -0
  63. data/spec/config/selenium_spec.rb +56 -0
  64. data/spec/config/strconv_spec.rb +58 -0
  65. data/spec/config/struct_spec.rb +66 -0
  66. data/spec/gc_spec.rb +8 -6
  67. data/spec/middleware/fetch_spec.rb +20 -8
  68. data/spec/middleware/router_spec.rb +7 -0
  69. data/spec/middleware/worker_spec.rb +64 -27
  70. data/spec/networking/capybara_spec.rb +12 -0
  71. data/spec/networking/context_spec.rb +127 -0
  72. data/spec/networking/ferrum_spec.rb +6 -22
  73. data/spec/networking/http_spec.rb +12 -0
  74. data/spec/networking/pool_spec.rb +37 -12
  75. data/spec/networking/selenium_spec.rb +6 -22
  76. data/spec/networking/strategy.rb +170 -0
  77. data/spec/redis/pool_spec.rb +1 -1
  78. data/spec/routing/dsl_spec.rb +10 -10
  79. data/spec/routing/integration_spec.rb +22 -22
  80. data/spec/routing/{custom_matcher_spec.rb → matchers/custom_spec.rb} +4 -4
  81. data/spec/routing/{host_matcher_spec.rb → matchers/host_spec.rb} +6 -6
  82. data/spec/routing/{path_matcher_spec.rb → matchers/path_spec.rb} +6 -6
  83. data/spec/routing/{query_matcher_spec.rb → matchers/query_spec.rb} +15 -15
  84. data/spec/routing/{scheme_matcher_spec.rb → matchers/scheme_spec.rb} +4 -4
  85. data/spec/routing/{suffix_matcher_spec.rb → matchers/suffix_spec.rb} +4 -4
  86. data/spec/routing/{uri_matcher_spec.rb → matchers/uri_spec.rb} +4 -4
  87. data/spec/routing/path_finder_spec.rb +1 -1
  88. data/spec/routing/root_route_spec.rb +2 -2
  89. data/spec/routing/route_spec.rb +2 -2
  90. data/spec/spec_helpers.rb +13 -5
  91. data/spec/wayfarer_spec.rb +1 -1
  92. data/wayfarer.gemspec +8 -7
  93. metadata +74 -33
  94. data/lib/wayfarer/config.rb +0 -67
  95. data/lib/wayfarer/networking/healer.rb +0 -21
  96. data/lib/wayfarer/networking/net_http.rb +0 -52
  97. data/lib/wayfarer/routing/custom_matcher.rb +0 -21
  98. data/lib/wayfarer/routing/host_matcher.rb +0 -23
  99. data/lib/wayfarer/routing/path_matcher.rb +0 -46
  100. data/lib/wayfarer/routing/query_matcher.rb +0 -67
  101. data/lib/wayfarer/routing/scheme_matcher.rb +0 -21
  102. data/lib/wayfarer/routing/suffix_matcher.rb +0 -21
  103. data/lib/wayfarer/routing/url_matcher.rb +0 -21
  104. data/spec/config_spec.rb +0 -144
  105. data/spec/networking/adapter.rb +0 -135
  106. data/spec/networking/healer_spec.rb +0 -46
  107. data/spec/networking/net_http_spec.rb +0 -37
@@ -6,6 +6,7 @@ module Wayfarer
6
6
  def self.included(base)
7
7
  base.include(Wayfarer::Redis::Connection)
8
8
  base.include(Wayfarer::Middleware::Stage::API)
9
+ base.include(Wayfarer::Callbacks)
9
10
  base.include(InstanceMethods)
10
11
  base.extend(ClassMethods)
11
12
  end
@@ -21,25 +22,27 @@ module Wayfarer
21
22
  module InstanceMethods
22
23
  extend Forwardable
23
24
 
24
- attr_accessor :task
25
-
26
- delegate %i[params adapter] => "task.metadata"
27
- delegate %i[browser capybara] => :adapter
25
+ delegate %i[params agent] => "task.metadata"
28
26
 
29
27
  def call(task)
30
- self.task = task
31
- public_send(task.metadata.action)
32
- yield if block_given?
28
+ run_callbacks :action do
29
+ public_send(task.metadata.action)
30
+ yield if block_given? # TODO: Should be excluded from callback block
31
+ end
33
32
  end
34
33
 
35
34
  def chain
36
- Wayfarer::Middleware::Chain.new([*Wayfarer.core_middleware, self])
35
+ Wayfarer::Middleware::Chain.new([*Wayfarer.middleware, self])
36
+ end
37
+
38
+ def browser
39
+ agent.instance
37
40
  end
38
41
 
39
42
  def page(live: false)
40
43
  return task.metadata.page unless live
41
44
 
42
- task.metadata.page = adapter.live(task.metadata.page)
45
+ task.metadata.page = agent.live&.page || task.metadata.page
43
46
  end
44
47
  end
45
48
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Networking
5
+ class Capybara
6
+ include Strategy
7
+
8
+ def create
9
+ ::Capybara::Session.new(Wayfarer.config.capybara.driver, nil)
10
+ end
11
+
12
+ def destroy(instance)
13
+ instance.quit
14
+ end
15
+
16
+ def navigate(instance, url)
17
+ instance.visit(url)
18
+ end
19
+
20
+ def live(instance)
21
+ success(url: instance.current_url,
22
+ body: instance.html,
23
+ status_code: instance.status_code,
24
+ headers: instance.response_headers)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Networking
5
+ Context = Struct.new(:strategy) do
6
+ def fetch(url)
7
+ supervise { strategy.fetch(instance, url) }
8
+ end
9
+
10
+ def live
11
+ supervise { strategy.live(instance) }
12
+ end
13
+
14
+ def renew
15
+ strategy.destroy(instance)
16
+ ensure
17
+ @instance = nil
18
+ end
19
+
20
+ def instance
21
+ @instance ||= strategy.create
22
+ end
23
+
24
+ private
25
+
26
+ def supervise
27
+ yield
28
+ rescue *strategy.renew_on => e
29
+ renew
30
+ ensure
31
+ # If renewing raises, re-raise the originally caught exception
32
+ raise e if e
33
+ end
34
+ end
35
+ end
36
+ end
@@ -3,67 +3,32 @@
3
3
  module Wayfarer
4
4
  module Networking
5
5
  class Ferrum
6
- def self.renew_on
7
- [::Ferrum::DeadBrowserError]
8
- end
9
-
10
- attr_reader :browser
11
-
12
- def initialize
13
- @browser = instantiate_browser
14
- end
15
-
16
- def fetch(url)
17
- browser.goto(url)
18
- Result::Success.new(live(nil))
19
- end
20
-
21
- def live(_)
22
- Wayfarer::Page.new(url: browser.current_url,
23
- body: body,
24
- status_code: browser.network.response.status,
25
- headers: browser.network.response.headers)
26
- end
6
+ include Strategy
27
7
 
28
- def body
29
- browser.body
8
+ def renew_on
9
+ [::Ferrum::DeadBrowserError]
30
10
  end
31
11
 
32
- def renew
33
- free
34
- @browser = instantiate_browser
12
+ def create
13
+ ::Ferrum::Browser.new(Wayfarer.config.ferrum.options).tap do |browser|
14
+ browser.headers.set(Wayfarer.config.network.http_headers)
15
+ end
35
16
  end
36
17
 
37
- def free
38
- browser&.reset
39
- browser&.quit
40
- @browser = nil
41
- @capybara = nil
18
+ def destroy(instance)
19
+ instance.reset
20
+ instance.quit
42
21
  end
43
22
 
44
- def capybara
45
- @capybara ||= instantiate_capybara_driver
23
+ def navigate(instance, url)
24
+ instance.goto(url)
46
25
  end
47
26
 
48
- private
49
-
50
- def instantiate_browser
51
- ::Ferrum::Browser.new(Wayfarer.config.ferrum_options).tap do |browser|
52
- browser.headers.set(Wayfarer.config.http_headers)
53
- end
54
- end
55
-
56
- def instantiate_capybara_driver
57
- Capybara.run_server = false
58
- Capybara.current_driver = :cuprite
59
-
60
- capybara_driver = Capybara::Cuprite::Driver.new(nil)
61
- capybara_driver.instance_variable_set(:@capybara, browser)
62
-
63
- session = Capybara::Session.new(:cuprite, nil)
64
- session.instance_variable_set(:@driver, capybara_driver)
65
-
66
- session
27
+ def live(instance)
28
+ success(url: instance.current_url,
29
+ body: instance.body,
30
+ status_code: instance.network.response.status,
31
+ headers: instance.network.response.headers)
67
32
  end
68
33
  end
69
34
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Networking
5
+ class HTTP
6
+ include Strategy
7
+
8
+ CONNECTION_NAME = "wayfarer"
9
+
10
+ def create
11
+ Net::HTTP::Persistent.new(name: CONNECTION_NAME).tap do |conn|
12
+ Wayfarer.config.network.http_headers.each do |key, val|
13
+ conn.override_headers[key] = val
14
+ end
15
+ end
16
+ end
17
+
18
+ def destroy(instance)
19
+ instance.shutdown
20
+ end
21
+
22
+ def fetch(instance, url)
23
+ res = instance.request(URI(url))
24
+
25
+ return redirect(res["location"]) if res.is_a?(Net::HTTPRedirection)
26
+
27
+ success(url: url,
28
+ status_code: res.code.to_i,
29
+ body: res.body,
30
+ headers: res.to_hash.transform_values(&:first))
31
+ end
32
+ end
33
+ end
34
+ end
@@ -6,28 +6,33 @@ module Wayfarer
6
6
  include Singleton
7
7
  extend Forwardable
8
8
 
9
+ cattr_accessor :registry, default: { http: HTTP,
10
+ ferrum: Ferrum,
11
+ selenium: Selenium,
12
+ capybara: Capybara }
13
+
9
14
  attr_reader :pool
10
15
 
11
16
  def initialize
12
- @pool = ConnectionPool.new(size: Wayfarer.config.adapter_pool_size,
13
- timeout: Wayfarer.config.adapter_pool_timeout,
14
- &method(:instantiate_adapter))
17
+ @pool = ConnectionPool.new(size: Wayfarer.config.network.pool_size,
18
+ timeout: Wayfarer.config.network.pool_timeout,
19
+ &method(:context))
15
20
  end
16
21
 
17
22
  delegate with: :pool
18
23
 
19
24
  def free
20
- pool.shutdown(&:free)
25
+ pool.shutdown(&:renew)
21
26
  end
22
27
 
23
28
  private
24
29
 
25
- def instantiate_adapter
26
- Healer.new(case Wayfarer.config.adapter
27
- when :net_http then Wayfarer::Networking::NetHTTP.instance
28
- when :selenium then Wayfarer::Networking::Selenium.new
29
- when :ferrum then Wayfarer::Networking::Ferrum.new
30
- end)
30
+ def context
31
+ Wayfarer::Networking::Context.new(strategy)
32
+ end
33
+
34
+ def strategy
35
+ self.class.registry[Wayfarer.config.network.agent].new
31
36
  end
32
37
  end
33
38
  end
@@ -12,7 +12,7 @@ module Wayfarer
12
12
  # Signals that a URL resulted in response with 3xx.
13
13
  # @!attribute [rw] redirect_url
14
14
  # @return [URI] Where to go next.
15
- Redirect = Struct.new(:request_url, :redirect_url)
15
+ Redirect = Struct.new(:redirect_url)
16
16
  end
17
17
  end
18
18
  end
@@ -3,67 +3,40 @@
3
3
  module Wayfarer
4
4
  module Networking
5
5
  class Selenium
6
- def self.renew_on
7
- [] # TODO: Figure out when to renew
8
- end
9
-
10
- FAKE_STATUS_CODE = 200
11
- FAKE_RESPONSE_HEADERS = {}.freeze
12
-
13
- attr_reader :browser
6
+ include Strategy
14
7
 
15
- def initialize
16
- @browser = instantiate_browser
17
- end
18
-
19
- def fetch(url)
20
- browser.navigate.to(url)
21
- Result::Success.new(live(nil))
22
- end
8
+ MOCK_STATUS_CODE = 200
9
+ MOCK_RESPONSE_HEADERS = {}.freeze
23
10
 
24
- def live(_)
25
- Wayfarer::Page.new(url: browser.current_url,
26
- body: browser.page_source,
27
- status_code: FAKE_STATUS_CODE,
28
- headers: FAKE_RESPONSE_HEADERS)
11
+ def create
12
+ ::Selenium::WebDriver.for(Wayfarer.config.selenium.driver, **options)
29
13
  end
30
14
 
31
- def body
32
- browser.page_source
15
+ def destroy(instance)
16
+ instance.quit
33
17
  end
34
18
 
35
- def renew
36
- free
37
- @browser = instantiate_browser
19
+ def navigate(instance, url)
20
+ instance.navigate.to(url)
38
21
  end
39
22
 
40
- def free
41
- browser&.quit
42
- @browser = nil
43
- @capybara = nil
44
- end
45
-
46
- def capybara
47
- @capybara ||= instantiate_capybara_driver
23
+ def live(instance)
24
+ success(url: instance.current_url,
25
+ body: instance.page_source,
26
+ status_code: MOCK_STATUS_CODE,
27
+ headers: MOCK_RESPONSE_HEADERS)
48
28
  end
49
29
 
50
30
  private
51
31
 
52
- def instantiate_browser
53
- ::Selenium::WebDriver.for(*Wayfarer.config.selenium_argv)
32
+ def options
33
+ Wayfarer.config.selenium.options.merge(http_client: http_client)
54
34
  end
55
35
 
56
- def instantiate_capybara_driver
57
- Capybara.run_server = false
58
- Capybara.current_driver = :selenium
59
-
60
- capybara_driver = Capybara::Selenium::Driver.new(nil)
61
- capybara_driver.instance_variable_set(:@capybara, browser)
62
-
63
- session = Capybara::Session.new(:selenium, nil)
64
- session.instance_variable_set(:@driver, capybara_driver)
65
-
66
- session
36
+ def http_client
37
+ ::Selenium::WebDriver::Remote::Http::Default.new.tap do |client|
38
+ client.read_timeout = Wayfarer.config.selenium.client_timeout
39
+ end
67
40
  end
68
41
  end
69
42
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Networking
5
+ module Strategy
6
+ def renew_on
7
+ []
8
+ end
9
+
10
+ def fetch(instance, url)
11
+ navigate(instance, url)
12
+ live(instance)
13
+ end
14
+
15
+ def navigate(_instance, _url)
16
+ raise NoMethodError
17
+ end
18
+
19
+ def live(_instance); end
20
+
21
+ def create
22
+ raise NoMethodError
23
+ end
24
+
25
+ def destroy(_instance); end
26
+
27
+ private
28
+
29
+ def success(...)
30
+ Wayfarer::Networking::Result::Success.new(Wayfarer::Page.new(...))
31
+ end
32
+
33
+ def redirect(...)
34
+ Wayfarer::Networking::Result::Redirect.new(...)
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/wayfarer/page.rb CHANGED
@@ -4,11 +4,10 @@ module Wayfarer
4
4
  class Page
5
5
  attr_reader :url,
6
6
  :status_code,
7
+ :body,
7
8
  :headers
8
9
 
9
- attr_accessor :body
10
-
11
- def initialize(url:, status_code:, headers:, body:)
10
+ def initialize(url:, status_code:, body:, headers:)
12
11
  @url = url
13
12
  @status_code = status_code
14
13
  @body = body
@@ -9,7 +9,9 @@ module Wayfarer
9
9
  attr_reader :pool
10
10
 
11
11
  def initialize
12
- @pool = ConnectionPool.new(&Wayfarer.config.redis_factory)
12
+ @pool = ConnectionPool.new do
13
+ Wayfarer.config.redis.factory.call(Wayfarer.config.redis)
14
+ end
13
15
  end
14
16
 
15
17
  delegate with: :pool
@@ -4,40 +4,40 @@ module Wayfarer
4
4
  module Routing
5
5
  module DSL
6
6
  def url(url, options = {}, &block)
7
- add_child_route(URLMatcher.new(url), path_offset, options, &block)
7
+ add_child_route(Matchers::URL.new(url), path_offset, options, &block)
8
8
  end
9
9
 
10
10
  def host(host, options = {}, &block)
11
- add_child_route(HostMatcher.new(host), path_offset, options, &block)
11
+ add_child_route(Matchers::Host.new(host), path_offset, options, &block)
12
12
  end
13
13
 
14
14
  def path(path, options = {}, &block)
15
15
  offset = File.join(path_offset, path)
16
16
  add_child_route(nil, offset, options, &block).tap do |route|
17
- route.matcher = PathMatcher.new(offset, route)
17
+ route.matcher = Matchers::Path.new(offset, route)
18
18
  end
19
19
  end
20
20
 
21
21
  def query(fields, options = {}, &block)
22
- add_child_route(QueryMatcher.new(fields), path_offset, options, &block)
22
+ add_child_route(Matchers::Query.new(fields), path_offset, options, &block)
23
23
  end
24
24
 
25
25
  def scheme(scheme, options = {}, &block)
26
- add_child_route(SchemeMatcher.new(scheme), path_offset, options, &block)
26
+ add_child_route(Matchers::Scheme.new(scheme), path_offset, options, &block)
27
27
  end
28
28
 
29
29
  def suffix(suffix, options = {}, &block)
30
- add_child_route(SuffixMatcher.new(suffix), path_offset, options, &block)
30
+ add_child_route(Matchers::Suffix.new(suffix), path_offset, options, &block)
31
31
  end
32
32
 
33
33
  def to(action, options = {}, &block)
34
- add_child_route(CustomMatcher.new { true }, path_offset, TargetRoute, options, &block).tap do |route|
34
+ add_child_route(Matchers::Custom.new { true }, path_offset, TargetRoute, options, &block).tap do |route|
35
35
  route.action = action
36
36
  end
37
37
  end
38
38
 
39
39
  def custom(delegate, options = {}, &block)
40
- add_child_route(CustomMatcher.new(delegate), path_offset, options, &block)
40
+ add_child_route(Matchers::Custom.new(delegate), path_offset, options, &block)
41
41
  end
42
42
 
43
43
  private
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ class Custom
7
+ attr_reader :delegate
8
+
9
+ def initialize(delegate = proc)
10
+ @delegate = delegate
11
+ end
12
+
13
+ def match(url)
14
+ !!delegate.call(url)
15
+ end
16
+
17
+ def params(_)
18
+ {}
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ Host = Struct.new(:host) do
7
+ # rubocop:disable Style/CaseEquality
8
+ def match(url)
9
+ host === url.host
10
+ end
11
+ # rubocop:enable Style/CaseEquality
12
+
13
+ def params(_)
14
+ {}
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ class Path
7
+ attr_reader :path,
8
+ :route,
9
+ :peeking,
10
+ :matcher
11
+
12
+ def initialize(path, route)
13
+ @path = path
14
+ @route = route
15
+ @peeking = false
16
+ @matcher = Mustermann.new(path, type: "sinatra")
17
+ end
18
+
19
+ def match(url)
20
+ route.accept(self)
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
+ !!(if peeking
25
+ matcher.peek(url.path)
26
+ else
27
+ matcher.match(url.path)
28
+ end)
29
+ end
30
+
31
+ def params(url)
32
+ return {} unless match(url)
33
+
34
+ matcher.params(url.path) || {}
35
+ end
36
+
37
+ def visit(route)
38
+ return true if route == self.route
39
+
40
+ return unless route.matcher.is_a?(self.class)
41
+
42
+ @peeking = true
43
+ false
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ Query = Struct.new(:fields) do
7
+ def match(url)
8
+ query = url.query
9
+
10
+ # CGI::parse throws a NoMethodError if the query is an empty string
11
+ return false if query.nil? || query.empty?
12
+
13
+ CGI.parse(query).none? { |field, vals| violates?(field, vals) }
14
+ end
15
+
16
+ def params(url)
17
+ return {} unless match(url)
18
+
19
+ CGI.parse(url.query)
20
+ .select { |(k, _)| fields.keys.include?(k.to_sym) }
21
+ .transform_values(&:last)
22
+ end
23
+
24
+ private
25
+
26
+ # rubocop:disable Lint/AssignmentInCondition
27
+ def violates?(field, vals)
28
+ return false unless constraint = fields[field.to_sym]
29
+
30
+ violates_constraint?(constraint, vals)
31
+ end
32
+ # rubocop:enable Lint/AssignmentInCondition
33
+
34
+ def violates_constraint?(constraint, vals)
35
+ case constraint
36
+ when String then violates_string?(constraint, vals)
37
+ when Integer then violates_integer?(constraint, vals)
38
+ when Regexp then violates_regexp?(constraint, vals)
39
+ when Range then violates_range?(constraint, vals)
40
+ end
41
+ end
42
+
43
+ def violates_string?(str, vals)
44
+ vals.none? { |val| str == val }
45
+ end
46
+
47
+ def violates_integer?(int, vals)
48
+ vals.none? { |val| int == Integer(val) }
49
+ rescue ArgumentError
50
+ true
51
+ end
52
+
53
+ def violates_regexp?(regexp, vals)
54
+ vals.none? { |val| regexp.match(val) }
55
+ end
56
+
57
+ def violates_range?(range, vals)
58
+ vals.none? { |val| range.include?(val.to_i) }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ Scheme = Struct.new(:scheme) do
7
+ def match(url)
8
+ url.scheme == scheme.to_s
9
+ end
10
+
11
+ def params(_)
12
+ {}
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfarer
4
+ module Routing
5
+ module Matchers
6
+ Suffix = Struct.new(:suffix) do
7
+ def match(url)
8
+ url.path.end_with?(suffix)
9
+ end
10
+
11
+ def params(_)
12
+ {}
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end