wayfarer 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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