wayfarer 0.4.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/actions/setup-mise/action.yaml +22 -0
  3. data/.github/workflows/push.yaml +89 -0
  4. data/.github/workflows/release.yaml +2 -2
  5. data/.gitignore +1 -0
  6. data/AGENTS.md +265 -0
  7. data/CLAUDE.md +1 -0
  8. data/Dockerfile +1 -0
  9. data/Gemfile.lock +5 -1
  10. data/docs/cookbook/batch_routing.md +3 -1
  11. data/docs/cookbook/consent_screen.md +3 -1
  12. data/docs/cookbook/executing_javascript.md +9 -3
  13. data/docs/cookbook/navigation.md +9 -3
  14. data/docs/cookbook/querying_html.md +9 -3
  15. data/docs/cookbook/screenshots.md +6 -2
  16. data/docs/guides/cli.md +1 -1
  17. data/docs/guides/debugging.md +1 -1
  18. data/docs/guides/development.md +18 -26
  19. data/docs/guides/handlers.md +1 -1
  20. data/docs/guides/jobs.md +13 -9
  21. data/docs/guides/middleware.md +134 -0
  22. data/docs/guides/navigation.md +6 -3
  23. data/docs/guides/networking/capybara.md +3 -1
  24. data/docs/guides/networking/custom_adapters.md +3 -3
  25. data/docs/guides/networking/ferrum.md +3 -1
  26. data/docs/guides/networking/http.md +4 -2
  27. data/docs/guides/networking/selenium.md +7 -3
  28. data/docs/guides/pages.md +4 -2
  29. data/docs/guides/routing.md +16 -8
  30. data/docs/guides/tasks.md +1 -1
  31. data/docs/guides/tutorial.md +73 -1
  32. data/docs/guides/user_agents.md +10 -10
  33. data/docs/index.md +1 -1
  34. data/lib/wayfarer/base.rb +6 -10
  35. data/lib/wayfarer/cli.rb +19 -1
  36. data/lib/wayfarer/gc.rb +0 -1
  37. data/lib/wayfarer/handler.rb +7 -5
  38. data/lib/wayfarer/logging.rb +3 -4
  39. data/lib/wayfarer/middleware/controller.rb +9 -8
  40. data/lib/wayfarer/middleware/dependency_graph.rb +181 -0
  41. data/lib/wayfarer/routing/matchers/query.rb +3 -2
  42. data/lib/wayfarer.rb +23 -2
  43. data/mise.toml +2 -0
  44. data/mkdocs.yml +1 -0
  45. data/rake/lint.rake +16 -0
  46. data/spec/spec_helpers.rb +1 -1
  47. data/spec/wayfarer/cli/routing_spec.rb +36 -6
  48. data/spec/wayfarer/integration/dependency_graph_spec.rb +111 -0
  49. data/spec/wayfarer/middleware/controller_spec.rb +34 -9
  50. data/spec/wayfarer/middleware/dependency_graph_spec.rb +367 -0
  51. data/spec/wayfarer/routing/integration_spec.rb +18 -0
  52. data/spec/wayfarer/routing/matchers/query_spec.rb +18 -0
  53. data/wayfarer.gemspec +1 -1
  54. metadata +11 -3
  55. data/.github/workflows/lint.yaml +0 -27
  56. data/.github/workflows/tests.yaml +0 -21
@@ -2,21 +2,11 @@
2
2
 
3
3
  ## Release Procedure
4
4
 
5
- 1. Ensure `Wayfarer::VERSION` was bumped appropriately.
6
- 2. Ensure the version in wayfarer.gemspec matches.
7
- 3. Open a release Pull Request develop -> master branch
8
- 4. Merge the Pull Request
9
- 5. Publish RubyGem and git tag as follows:
10
-
11
- ```
12
- git checkout master
13
- git pull origin master --rebase
14
- bundle exec rake build
15
- gem push build/wayfarer-*.gem
16
- bundle exec rake clean
17
- git tag <VERSION>
18
- git push origin <VERSION>
19
- ```
5
+ 1. Bump versions:
6
+ * `Wayfarer::VERSION` in `lib/wayfarer.rb`
7
+ * RubyGem version in `wayfarer.gemspec`
8
+ * Run `bundle install` to regenerate `Gemfile.lock`
9
+ 2. Push to `master` or `develop` and run the manual `release` worflow
20
10
 
21
11
  ## Conventions and guidelines
22
12
 
@@ -27,10 +17,11 @@ git push origin <VERSION>
27
17
 
28
18
  ### Navigate the web along URL patterns
29
19
 
30
- URLs are less prone to change than served markup.
31
- One reason for this is that changes to a URL's path can have negative
32
- consequences for its page ranking in search engines. Websites naturally implement
33
- architectural URL patterns like REST or expose surrogate keys.
20
+ URLs are less prone to change than served markup. There are also SEO
21
+ incentives to keep their paths stable. Since websites naturally implement
22
+ architectural URL patterns like REST on the application layer, URL structure
23
+ reflects internal domain concepts necessarily, for which Wayfarer's URL-based
24
+ routing is designed.
34
25
 
35
26
  ### Follow URLs verbatim as they appear in responses
36
27
 
@@ -49,17 +40,18 @@ is ephemeral.
49
40
 
50
41
  There are two core features that depend on Redis. First, per-batch acylicity is
51
42
  achieved by maintaining the set of processed URLs per batch in Redis.
52
- There's no option to follow links in a cyclic manner. Second, batch completion
53
- requires updating an integer value in Redis, and batch completion is a very
54
- useful feature, since most crawls should end eventually, and often you want to
55
- know when.
43
+ There's no configuration option to follow links in a cyclic manner. Second,
44
+ batch completion requires updating an integer value in Redis, and batch completion
45
+ is a very useful feature, since most crawls should end eventually, and often you want
46
+ to know when.
56
47
 
57
48
  ### No configuration files
58
49
 
59
- Wayfarer can be configured through `Wayfarer.config` only, because `Wayfarer.config`
60
- may contain Ruby objects that don't de/serialize well, such as `Proc`s or `Set`s.
50
+ Wayfarer can be configured through `Wayfarer.config` in Ruby code only,
51
+ because `Wayfarer.config` may contain Ruby objects that don't de/serialize well,
52
+ such as `Proc`s or `Set`s.
61
53
 
62
- ### Features out of scope
54
+ ### Other features that are out of scope
63
55
 
64
56
  Wayfarer won't provide:
65
57
 
@@ -1,6 +1,6 @@
1
1
  # Handlers
2
2
 
3
- Handlers are like [jobs](/jobs) but they don't inherit from `ActiveJob::Base`
3
+ Handlers are like [jobs](jobs.md) but they don't inherit from `ActiveJob::Base`
4
4
  which is why they can't affect the message queue directly themselves.
5
5
  Instead, jobs and handlers can route tasks to other handlers. Handlers
6
6
  themselves have routes, but they can be bypassed.
data/docs/guides/jobs.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Jobs
2
2
 
3
3
  Jobs are [Active Job](https://edgeguides.rubyonrails.org/active_job_basics.html)s
4
- that use a DSL to process [tasks](/guides/tasks) that they read from a message
4
+ that use a DSL to process [tasks](tasks.md) that they read from a message
5
5
  queue.
6
6
 
7
7
  Instead of implementing Active Job's `#perform` method yourself, you declare
8
- [routes](../routing) to instance methods, like web applications route incoming
8
+ [routes](routing.md) to instance methods, like web applications route incoming
9
9
  requests. Only URLs that match a route are retrieved and processed. All other
10
10
  URLs are considered successfully processed. The action has access to the
11
- retrieved [page](../pages), the [user agent](../user-agents) that retrieved the
11
+ retrieved [page](pages.md), the [user agent](user_agents.md) that retrieved the
12
12
  page and the current task:
13
13
 
14
14
  ```ruby
@@ -47,7 +47,7 @@ Instead of a generated UUID, you can also set your own batch:
47
47
  DummyJob.crawl("https://example.com", batch: "my-batch")
48
48
  ```
49
49
 
50
- You can also use Wayfarer's [CLI](../cli) to enqueue a task:
50
+ You can also use Wayfarer's [CLI](cli.md) to enqueue a task:
51
51
 
52
52
  ```sh
53
53
  wayfarer enqueue --batch my-batch DummyJob "https://example.com"
@@ -74,7 +74,7 @@ end
74
74
 
75
75
  ## Accessing the current task
76
76
 
77
- If the task's URL matched a [route](../routing), the URL is retrieved over the network,
77
+ If the task's URL matched a [route](routing.md), the URL is retrieved over the network,
78
78
  and the method that was routed to is called. The task is available as `#task`:
79
79
 
80
80
  ```ruby
@@ -92,7 +92,7 @@ end
92
92
 
93
93
  ## Accessing the current page
94
94
 
95
- You have access to the retrieved [page](../pages):
95
+ You have access to the retrieved [page](pages.md):
96
96
 
97
97
  ```ruby
98
98
  class DummyJob < ActiveJob::Base
@@ -184,7 +184,9 @@ Wayfarer relies on ActiveJob's [error handling methods](https://guides.rubyonrai
184
184
  * `retry_on` to retry jobs a number of times on certain errors:
185
185
 
186
186
  ```ruby
187
- class DummyJob < Wayfarer::Base
187
+ class DummyJob < ActiveJob::Base
188
+ include Wayfarer::Base
189
+
188
190
  retry_on MyError, attempts: 3 do |job, error|
189
191
  # This block runs once all 3 attempts have failed
190
192
  # (1 initial attempt + 2 retries)
@@ -195,7 +197,9 @@ Wayfarer relies on ActiveJob's [error handling methods](https://guides.rubyonrai
195
197
  * `discard_on` to throw away jobs on certain errors:
196
198
 
197
199
  ```ruby
198
- class DummyJob < Wayfarer::Base
200
+ class DummyJob < ActiveJob::Base
201
+ include Wayfarer::Base
202
+
199
203
  discard_on MyError do |job, error|
200
204
  # This block runs once and buries the job
201
205
  end
@@ -205,7 +209,7 @@ Wayfarer relies on ActiveJob's [error handling methods](https://guides.rubyonrai
205
209
  ## Recreating user agents on certain errors
206
210
 
207
211
  You can configure a list of exception classes upon which user agents
208
- get recreated (see [User agent API]()):
212
+ get recreated (see [User agent API](user_agents.md)):
209
213
 
210
214
  ```ruby
211
215
  Wayfarer.config[:network][:renew_on] = [MyIrrecoverableError]
@@ -0,0 +1,134 @@
1
+ # Middleware
2
+
3
+ Wayfarer processes [tasks](tasks.md) through a middleware chain, an ordered
4
+ pipeline of middleware classes. Each middleware receives a task, can modify it,
5
+ and yields to the next middleware in the chain. If a middleware doesn't yield,
6
+ the chain halts.
7
+
8
+ A middleware is a class that implements a `call` method. It receives a `task`
9
+ argument and must `yield` to continue the middleware chain.
10
+
11
+ ```ruby
12
+ class MyCustomMiddleware
13
+ def call(task)
14
+ # Pre-processing: optionally set task metadata
15
+ task[:started_at] = Time.now.utc
16
+
17
+ # Yield to continue the chain (or not)
18
+ # Downstream middleware can access `task[:started_at]`
19
+ yield if block_given?
20
+
21
+ # Post-processing
22
+ end
23
+ end
24
+ ```
25
+
26
+ !!! info "Task metadata `#task[]=` is ephemeral"
27
+
28
+ Anything you assign to a task at runtime isn't serialized to the message
29
+ queue. For example in the case of a retry, no task metadata can be restored.
30
+
31
+ A `DependencyGraph` manages the chain ordering by resolving `before:` and
32
+ `after:` constraints between middlewares.
33
+
34
+ ## Default middleware chain
35
+
36
+ Jobs that `#!ruby include Wayfarer::Base` get a default chain of 10 middlewares:
37
+
38
+ 1. `Redis`: connects to Redis
39
+ 2. `UriParser`: parses the task URL
40
+ 3. `Normalize`: normalizes the URL
41
+ 4. `Dedup`: deduplicates tasks
42
+ 5. `BatchCompletion`: tracks batch progress
43
+ 6. `Stage`: collects staged URLs
44
+ 7. `Router`: matches the URL to an action
45
+ 8. `UserAgent`: retrieves the page
46
+ 9. `ContentType`: filters by Content-Type
47
+ 10. `Dispatch`: calls the routed action
48
+
49
+ [Handlers](handlers.md) that `#!ruby include Wayfarer::Handler` get a shorter chain:
50
+
51
+ 1. `ContentType`
52
+ 2. `Router`
53
+ 3. `Dispatch`
54
+
55
+ `#!ruby Wayfarer.config[:middleware][:base]` and `#!ruby
56
+ Wayfarer.config[:middleware][:handler]` declare these defaults.
57
+
58
+ ## Adding middleware
59
+
60
+ Use the `use` class method to add a middleware to a job's chain. You can
61
+ declare ordering constraints with `before:` and `after:`:
62
+
63
+ ```ruby
64
+ class MyJob < ActiveJob::Base
65
+ include Wayfarer::Base
66
+
67
+ use MyCustomMiddleware, after: [Wayfarer::Middleware::Router]
68
+ end
69
+ ```
70
+
71
+ !!! info "Auto-registration"
72
+
73
+ If you reference a middleware in `before:` or `after:` that hasn't been
74
+ explicitly added, it's registered automatically with no constraints.
75
+
76
+ ## Removing middleware
77
+
78
+ Use `middleware.remove` to remove a middleware from the chain. All
79
+ `before:` and `after:` references to the removed middleware are automatically
80
+ cleaned up.
81
+
82
+ ```ruby
83
+ class NoDedupJob < ActiveJob::Base
84
+ include Wayfarer::Base
85
+
86
+ self.middleware = middleware.remove(Wayfarer::Middleware::Dedup)
87
+ end
88
+ ```
89
+
90
+ ## Replacing middleware
91
+
92
+ Use `middleware.replace` to swap a middleware. The replacement inherits
93
+ the exact same `before:` and `after:` constraints. The method updates all
94
+ references to the old middleware in other entries to point to the replacement.
95
+
96
+ ```ruby
97
+ class TestJob < ActiveJob::Base
98
+ include Wayfarer::Base
99
+
100
+ self.middleware = middleware.replace(
101
+ Wayfarer::Middleware::Redis,
102
+ MyInMemoryState
103
+ )
104
+ end
105
+ ```
106
+
107
+ !!! attention "`replace` inherits constraints"
108
+
109
+ If you need different constraints for the new middleware, use `remove`
110
+ followed by `add` instead.
111
+
112
+ ## The dependency graph
113
+
114
+ The dependency graph is immutable. Every operation (`add`, `remove`, `replace`)
115
+ returns a new instance which is why you always assign the result back:
116
+
117
+ ```ruby
118
+ self.middleware = middleware.add(MyMiddleware, after: [Wayfarer::Middleware::Router])
119
+ self.middleware = middleware.remove(Wayfarer::Middleware::Dedup)
120
+ self.middleware = middleware.replace(OldMiddleware, NewMiddleware)
121
+ ```
122
+
123
+ Circular dependencies raise a `#!ruby Wayfarer::Middleware::DependencyGraph::CyclicDependencyError`.
124
+
125
+ ## Modifying the global default
126
+
127
+ You can modify the default middleware chain globally in an initializer. Any job
128
+ class defined after this point uses the modified default:
129
+
130
+ ```ruby
131
+ # config/initializers/wayfarer.rb
132
+ Wayfarer.config[:middleware][:base] = Wayfarer.config[:middleware][:base]
133
+ .add(MyGlobalMiddleware, after: [Wayfarer::Middleware::Router])
134
+ ```
@@ -12,7 +12,9 @@ URL does not enqueue it immediately. Instead, the URL is added to a processing
12
12
  set first.
13
13
 
14
14
  ```ruby
15
- class DummyJob < Wayfarer::Base
15
+ class DummyJob < ActiveJob::Base
16
+ include Wayfarer::Base
17
+
16
18
  route.to :index
17
19
 
18
20
  def index
@@ -35,7 +37,6 @@ def index
35
37
  end
36
38
  ```
37
39
 
38
- See also: [Performance: Stage less URLs](/guides/performance)
39
40
 
40
41
  !!! attention "Failing action methods do not enqueue tasks"
41
42
 
@@ -59,7 +60,9 @@ job's routes do not match the URL. When the task gets consumed, the URL does not
59
60
  get fetched and the action method not called.
60
61
 
61
62
  ```ruby
62
- class DummyJob < Wayfarer::Base
63
+ class DummyJob < ActiveJob::Base
64
+ include Wayfarer::Base
65
+
63
66
  route do
64
67
  host "example.com", path: "/users/:user_id", to: :user
65
68
  end
@@ -7,7 +7,9 @@ applications which adds a nice API that also works well for web scraping.
7
7
  Wayfarer.config[:network][:agent] = :capybara
8
8
  # Wayfarer.config[:capybara][:driver] = ...
9
9
 
10
- class DummyJob < Wayfarer::Worker
10
+ class DummyJob < ActiveJob::Base
11
+ include Wayfarer::Base
12
+
11
13
  route.to :index
12
14
 
13
15
  def index
@@ -15,7 +15,7 @@ as they navigate to a URL
15
15
  Because spawning browser processes or instantiating HTTP clients is expensive,
16
16
  Wayfarer keeps user agents in a pool and reuses them across jobs. This means
17
17
  that browser state carries over between jobs, as each job checks out a previous
18
- job's user agent. When exceptions are raised, you must [handle them](/guides/jobs/#handling-errors).
18
+ job's user agent. When exceptions are raised, you must [handle them](../jobs.md#handling-errors).
19
19
 
20
20
  Only on certain irrecoverable errors are individual user agents
21
21
  destroyed and recreated. For example when a browser process crashes, it is
@@ -62,7 +62,7 @@ exception classes upon which an instance of the user agent should get recreated
62
62
  ### Stateless interface
63
63
 
64
64
  In addition to the base interface, stateless user agents implement `#fetch(instance, url)`
65
- which fetches [pages](../pages) or indicates redirects:
65
+ which fetches [pages](../pages.md) or indicates redirects:
66
66
 
67
67
  * `#create()` __(required)__
68
68
  * `#fetch(instance, url)` __(required)__: Called to retrieve a URL. Responses with a
@@ -93,7 +93,7 @@ instance callbacks:
93
93
  * `#create()` __(required)__
94
94
  * `#navigate(instance, url)` __(required)__: Navigates the user agent to the given URL.
95
95
  * `#live(instance)` __(required)__: Turns the current user agent state
96
- into a [page](../pages) by calling `success(url:, body:, status_code:, headers:)`.
96
+ into a [page](../pages.md) by calling `success(url:, body:, status_code:, headers:)`.
97
97
  * `#destroy(instance)` (optional)
98
98
  * `::renew_on` (optional)
99
99
 
@@ -13,7 +13,9 @@ so:
13
13
  ```ruby
14
14
  Wayfarer.config[:network][:agent] = :ferrum
15
15
 
16
- class DummyWorker < Wayfarer::Worker
16
+ class DummyJob < ActiveJob::Base
17
+ include Wayfarer::Base
18
+
17
19
  route.to :index
18
20
 
19
21
  def index
@@ -6,10 +6,12 @@ also alongside automated browsers.
6
6
  ## Ad-hoc GET requests
7
7
 
8
8
  When automating browsers, it can be useful to additionally retrieve another page
9
- or even the current page over plain HTTP. Jobs can fetch URLs to [pages](/pages) with `#http`:
9
+ or even the current page over plain HTTP. Jobs can fetch URLs to [pages](../pages.md) with `#http`:
10
10
 
11
11
  ```ruby
12
- class DummyJob < Wayfarer::Base
12
+ class DummyJob < ActiveJob::Base
13
+ include Wayfarer::Base
14
+
13
15
  route.to :index
14
16
 
15
17
  def index
@@ -9,7 +9,9 @@ so:
9
9
  ```ruby
10
10
  Wayfarer.config[:network][:agent] = :selenium
11
11
 
12
- class DummyWorker < Wayfarer::Worker
12
+ class DummyJob < ActiveJob::Base
13
+ include Wayfarer::Base
14
+
13
15
  route.to :index
14
16
 
15
17
  def index
@@ -29,7 +31,9 @@ process.
29
31
  ```ruby
30
32
  Wayfarer.config[:network][:agent] = :selenium
31
33
 
32
- class DummyJob < Wayfarer::Base
34
+ class DummyJob < ActiveJob::Base
35
+ include Wayfarer::Base
36
+
33
37
  route.to :index
34
38
 
35
39
  def index
@@ -39,7 +43,7 @@ process.
39
43
  end
40
44
  ```
41
45
 
42
- !!! note "Consider using [Ferrum](../ferrum) instead if Google Chrome suits your needs."
46
+ !!! note "Consider using [Ferrum](ferrum.md) instead if Google Chrome suits your needs."
43
47
  Use Ferrum if you want to automate Google Chrome. It provides superior
44
48
  stability and a richer feature set compared to Selenium drivers.
45
49
 
data/docs/guides/pages.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Pages
2
2
 
3
3
  A page is the immutable state of the contents behind a URL at a point in time,
4
- retrieved by a [user agent](user-agents.md). In other words, a page is an HTTP
4
+ retrieved by a [user agent](user_agents.md). In other words, a page is an HTTP
5
5
  response, or the state of a remotely controlled browser.
6
6
 
7
7
  ```ruby
@@ -82,7 +82,9 @@ To get a page that reflects the current browser state, set the `#!ruby :live`
82
82
  keyword:
83
83
 
84
84
  ```ruby
85
- class DummyJob < Wayfarer::Worker
85
+ class DummyJob < ActiveJob::Base
86
+ include Wayfarer::Base
87
+
86
88
  route.to :index
87
89
 
88
90
  def index
@@ -1,19 +1,27 @@
1
1
  # Routing
2
2
 
3
3
  Wayfarer equips jobs with a declarative routing DSL that maps URLs to actions.
4
- Actions are instance methods denoted by symbols, or [handlers](/guides/handlers).
5
- [Pages](/guides/pages) are only retrieved from URLs which map to an action.
4
+ Actions are instance methods denoted by symbols, or [handlers](handlers.md).
5
+ [Pages](pages.md) are only retrieved from URLs which map to an action.
6
6
 
7
- !!! info "Routed URLs are normalized"
7
+ !!! warning "Routed URLs are normalized"
8
8
 
9
- By default, Wayfarer [applies some transformations to each URL](../tasks/#url-normalization) to bring it
10
- into a canonical form. Routing happens based on this canonical form.
9
+ By default, Wayfarer [applies some transformations to each URL](tasks.md#url-normalization) to bring it
10
+ into a canonical form. __Routing happens based on this canonical form__,
11
+ not on the verbatim `task.url`.
11
12
 
12
- You can always access a task's raw string as it was enqueued with `task.url`.
13
+ This means that for example a route declaration doesn't match the URL
14
+ `https://www.example.com`, since the URL's hostname is normalized to `example.com`.
15
+
16
+ ```ruby
17
+ route.host "www.example.com"
18
+ ```
19
+ You may need to pass the `wayfarer route` CLI sub-command the `-r FILE` option
20
+ and point to the file that configures `Wayfarer.config`.
13
21
 
14
22
  A job's route declarations equate to a predicate tree.
15
23
  When a URL is routed, the predicate tree is searched depth-first. If a
16
- matching leaf predicate is found, the found path's action is dispatched.
24
+ matching leaf predicate is found, the downmost path's action is dispatched.
17
25
  You can extract data from URL path segments and query parameters and
18
26
  access it through `params` in jobs or handlers.
19
27
 
@@ -75,7 +83,7 @@ flowchart LR
75
83
  classDef unvisited fill:#BDBDBD,stroke:#BDBDBD,color:#616161
76
84
  ```
77
85
 
78
- ??? note "You can also visualise a job's routing tree with with the [`route` CLI subcommand](/guides/cli)"
86
+ ??? note "You can also visualise a job's routing tree with with the [`route` CLI subcommand](cli.md)"
79
87
 
80
88
  ```sh
81
89
  wayfarer route DummyJob -r dummy_job.rb http://localhost:9000/users/42/gallery
data/docs/guides/tasks.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Tasks
2
2
 
3
3
  Tasks are the immutable units of work read from a message queue and processed by
4
- [jobs](/guides/jobs). A task consists of two strings:
4
+ [jobs](jobs.md). A task consists of two strings:
5
5
 
6
6
  * The __URL__ to process
7
7
  * The __batch__ the task belongs to
@@ -60,7 +60,79 @@ bundle exec wayfarer perform -r dummy_job.rb DummyJob https://example.com
60
60
  ```
61
61
 
62
62
  If you don't provide a batch, Wayfarer uses a generated UUID instead.
63
- We could have also used `DummyJob.crawl
63
+ We could have also used `DummyJob.crawl("https://example.com")` in a Ruby script to enqueue the job programmatically.
64
64
 
65
+ ## Accessing page data
65
66
 
67
+ The `page` method returns an object representing the HTTP response or browser state.
68
+ Wayfarer automatically parses HTML, XML, and JSON responses.
66
69
 
70
+ For HTML pages, `page.doc` returns a [Nokogiri](https://nokogiri.org/) document, and `page.meta` returns a [MetaInspector](https://github.com/jaimeiniesta/metainspector) object for easy access to metadata.
71
+
72
+ Let's update `dummy_job.rb` to print the page title:
73
+
74
+ ```ruby title="dummy_job.rb"
75
+ require "activejob"
76
+ require "wayfarer"
77
+
78
+ class DummyJob < ActiveJob::Base
79
+ include Wayfarer::Base
80
+
81
+ route.to :index
82
+
83
+ def index
84
+ puts "Title: #{page.doc.title.strip}"
85
+ puts "H1: #{page.doc.at('h1')&.text&.strip}"
86
+ end
87
+ end
88
+ ```
89
+
90
+ Running `perform` again will now show the title of the page you crawled.
91
+
92
+ ## Crawling and Staging
93
+
94
+ To crawl more pages, we use the `stage` method. This adds new URLs to the current batch.
95
+ Wayfarer handles deduplication, ensuring that we don't get stuck in loops processing the same URL multiple times within the same batch.
96
+
97
+ We can use `page.meta.links.internal` to easily find all links on the current page that point to the same domain.
98
+
99
+ Update `dummy_job.rb` to crawl recursively:
100
+
101
+ ```ruby title="dummy_job.rb"
102
+ require "activejob"
103
+ require "wayfarer"
104
+
105
+ class DummyJob < ActiveJob::Base
106
+ include Wayfarer::Base
107
+
108
+ route.to :index
109
+
110
+ def index
111
+ puts "Crawling #{page.url}"
112
+
113
+ # Stage all internal links found on the page
114
+ stage page.meta.links.internal
115
+ end
116
+ end
117
+ ```
118
+
119
+ ## Executing the crawl
120
+
121
+ The `wayfarer perform` command we used earlier only executes a single job. To run a full crawl where staged jobs are picked up and processed, we use `wayfarer execute`.
122
+
123
+ This command starts an in-process async executor that will continue running until all jobs in the batch are complete.
124
+
125
+ ```sh
126
+ bundle exec wayfarer execute -r dummy_job.rb DummyJob https://example.com
127
+ ```
128
+
129
+ You will see output indicating that multiple pages are being crawled as Wayfarer follows the internal links.
130
+
131
+ ## Next steps
132
+
133
+ Now that you have a basic crawler running, you can explore more advanced features:
134
+
135
+ * **[Routing](routing.md)**: Learn how to route different URLs to different methods or handler classes.
136
+ * **[Pages](pages.md)**: Dive deeper into the `Page` object and custom response parsers.
137
+ * **[Networking](user_agents.md)**: Configure Wayfarer to use Headless Chrome (via Ferrum or Selenium) for pages that require JavaScript.
138
+ * **[Configuration](configuration.md)**: Customize concurrency, user agents, and request headers.
@@ -1,18 +1,18 @@
1
1
  # User agents
2
2
 
3
- User agents are used by [jobs](../jobs) to retrieve the contents behind a URL into a
4
- [page](../pages), for example a remotely controlled Firefox process or a Ruby HTTP client.
3
+ User agents are used by [jobs](jobs.md) to retrieve the contents behind a URL into a
4
+ [page](pages.md), for example a remotely controlled Firefox process or a Ruby HTTP client.
5
5
 
6
6
  User agents are kept in a connection pool and all user agents in the pool
7
7
  share the same type and configuration. You can add your own custom user agents by implementing
8
- the [user agent API](custom_user_agents.md).
8
+ the [user agent API](networking/custom_adapters.md).
9
9
 
10
10
  Wayfarer comes with the following built-in user agents:
11
11
 
12
- * [`:http`](http.md) (default)
13
- * [`:ferrum`](ferrum.md) to automate Google Chrome
14
- * [`:selenium`](selenium.md) to automate a variety of browsers
15
- * [`:capybara`](capybara.md) to use Capybara sessions
12
+ * [`:http`](networking/http.md) (default)
13
+ * [`:ferrum`](networking/ferrum.md) to automate Google Chrome
14
+ * [`:selenium`](networking/selenium.md) to automate a variety of browsers
15
+ * [`:capybara`](networking/capybara.md) to use Capybara sessions
16
16
 
17
17
  Configure the user agent with the global configuration option:
18
18
 
@@ -35,7 +35,7 @@ class DummyJob < ActiveJob::Base
35
35
  end
36
36
  ```
37
37
 
38
- You can also implement [custom user agents](custom_user_agents.md) to support
38
+ You can also implement [custom user agents](networking/custom_adapters.md) to support
39
39
  your own HTTP client or browser automation service/protocol.
40
40
 
41
41
  ### Ad-hoc HTTP requests
@@ -71,7 +71,7 @@ Wayfarer.config[:network][:http_headers] = { "User-Agent" => "MyCrawler" }
71
71
 
72
72
  Since user agents are expensive to create, especially in the case of browser
73
73
  processes, Wayfarer keeps user agents within a connection pool. When a job
74
- performs and needs to retrieve the [page](../pages) for its task URL, an agent
74
+ performs and needs to retrieve the [page](pages.md) for its task URL, an agent
75
75
  is checked out from the pool, and checked back in when the routed action method
76
76
  returns.
77
77
 
@@ -94,7 +94,7 @@ Wayfarer.config[:network][:pool][:size] = Sidekiq.options[:concurrency]
94
94
  The same browser session is used across jobs. This means that the browser
95
95
  is not closed between jobs, and that the browser's state carries over from
96
96
  job to job. You may account for this by resetting the browser's state
97
- according to your needs, for which you can use [callbacks](../callbacks).
97
+ according to your needs, for which you can use [callbacks](callbacks.md).
98
98
 
99
99
  ### `UserAgentTimeoutError`: avoiding pool contention
100
100
 
data/docs/index.md CHANGED
@@ -9,7 +9,7 @@ hide:
9
9
  ## Ruby web crawling framework built on [ActiveJob]() and [Redis]()
10
10
 
11
11
  <small>
12
- [Read the tutorial](/guides/tutorial){ .md-button .md-button--primary }
12
+ [Read the tutorial](guides/tutorial.md){ .md-button .md-button--primary }
13
13
  </small>
14
14
 
15
15
  === "Command line"
data/lib/wayfarer/base.rb CHANGED
@@ -116,16 +116,12 @@ module Wayfarer
116
116
  # chain included from {Controller}
117
117
  alias_method :perform, :call
118
118
 
119
- use Wayfarer::Middleware::Redis
120
- use Wayfarer::Middleware::UriParser
121
- use Wayfarer::Middleware::Normalize
122
- use Wayfarer::Middleware::Dedup
123
- use Wayfarer::Middleware::BatchCompletion
124
- use Wayfarer::Middleware::Stage
125
- use Wayfarer::Middleware::Router
126
- use Wayfarer::Middleware::UserAgent
127
- use Wayfarer::Middleware::ContentType
128
- use Wayfarer::Middleware::Dispatch
119
+ class_attribute :middleware,
120
+ default: Wayfarer.config[:middleware][:base],
121
+ instance_accessor: false,
122
+ instance_predicate: false
123
+
124
+ include middleware.api
129
125
  end
130
126
 
131
127
  class_methods do