wallflower 0.4.0 → 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.
- checksums.yaml +4 -4
- data/README.md +15 -7
- data/app/jobs/wallflower/run_job.rb +6 -3
- data/app/models/wallflower/task.rb +2 -2
- data/app/views/wallflower/tasks/index.html.erb +1 -1
- data/app/views/wallflower/tasks/show.html.erb +1 -1
- data/lib/generators/wallflower/install/templates/create_wallflower_tasks.rb.erb +1 -1
- data/lib/generators/wallflower/install/templates/rename_wallflower_tasks_kind_to_task_type.rb.erb +5 -0
- data/lib/generators/wallflower/update/update_generator.rb +4 -0
- data/lib/wallflower/engine.rb +1 -1
- data/lib/wallflower/task_types.rb +31 -0
- data/lib/wallflower/version.rb +1 -1
- data/lib/wallflower.rb +3 -3
- data/the_local/agents/wallflower-develop.md +11 -10
- data/the_local/agents/wallflower-info.md +4 -4
- data/the_local/interface.yml +2 -2
- metadata +3 -2
- data/lib/wallflower/kinds.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bee05b46afa4694efc995d6ab7befa843e299fe7badc2cbb3525a81c243bc585
|
|
4
|
+
data.tar.gz: 27e1317e521c02f6fe23577cea0ce20e32351e187b79882994d75c774ce56e3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d356ca88a0600f1310e9997e05d79b74665cece49b6a09f40f228e0b670a46c8f11e216e605dcd7650c7a596178333237456fb2c64dde46b2feaaca8fdbc100
|
|
7
|
+
data.tar.gz: 917d6dd960077ff4ad47d478c429534e1d974a110fac430149108b0ac7b26a2332ed97b59c862a372f572b0fa7162e621cbd8bff55c5b6e3584e286228c2aa65
|
data/README.md
CHANGED
|
@@ -42,12 +42,12 @@ Wallflower.configure do |config|
|
|
|
42
42
|
end
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## Task types and runners
|
|
46
46
|
|
|
47
|
-
A
|
|
47
|
+
A task type is a type of task the app offers, such as an export. Register each one in an initializer with its key, its title and the name of its runner class:
|
|
48
48
|
|
|
49
49
|
```ruby
|
|
50
|
-
Wallflower.
|
|
50
|
+
Wallflower.register_task_type :export_transactions, title: "Export transactions", runner: "ExportTransactionsRunner"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
An app whose registered runner class does not exist fails to boot, with a message naming the class.
|
|
@@ -91,7 +91,7 @@ A finished task's page then offers a Download button. The file is sent through W
|
|
|
91
91
|
## Starting a task
|
|
92
92
|
|
|
93
93
|
```ruby
|
|
94
|
-
task = Wallflower.start(
|
|
94
|
+
task = Wallflower.start(task_type: :export_transactions, person: current_user, account: current_account, params: { "month" => "2026-10" })
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
The task is returned queued and its job is enqueued. When the job runs, the task is marked running, its runner is called, and when the runner returns the task is marked finished with the time it finished.
|
|
@@ -110,7 +110,15 @@ With no `on_finish` set, tasks still run to the end.
|
|
|
110
110
|
|
|
111
111
|
## When a task fails
|
|
112
112
|
|
|
113
|
-
A
|
|
113
|
+
A task type runs one try by default. A task type registered with `tries:` runs again after a raised error until it has used them all, waiting `retry_wait:` seconds between tries, 60 by default:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Wallflower.register_task_type :send_price, title: "Send a price", runner: "SendPriceRunner", tries: 3, retry_wait: 60
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Between tries the task stays running and `on_finish` does not run. Each try starts its progress and refusals over, so a runner with more than one try must be safe to run again.
|
|
120
|
+
|
|
121
|
+
A runner that raises on its last try marks its task failed. The task keeps the error's message, its page shows it beside how far the task got, the error is reported through `Rails.error`, and `on_finish` runs once with the failed task.
|
|
114
122
|
|
|
115
123
|
A task still running with no update for longer than `stall_after`, one hour by default, is marked failed by `Wallflower::SweepJob`, with a reason saying it stalled. Schedule the sweep the way the app schedules any recurring job, for example in Solid Queue's `config/recurring.yml`:
|
|
116
124
|
|
|
@@ -132,11 +140,11 @@ With `keep_for` set, the same sweep deletes each finished or failed task that en
|
|
|
132
140
|
|
|
133
141
|
## The task list
|
|
134
142
|
|
|
135
|
-
The engine's root lists the tasks the signed-in person started in the current account, newest first. Each row shows the
|
|
143
|
+
The engine's root lists the tasks the signed-in person started in the current account, newest first. Each row shows the task type's title, the status as a coloured badge, the progress, the time left while it runs and when it was started, links to the task's page, and updates while the task runs.
|
|
136
144
|
|
|
137
145
|
## The task page
|
|
138
146
|
|
|
139
|
-
A task's page lives at `tasks/:id` under the engine's mount path. The person who started the task sees its
|
|
147
|
+
A task's page lives at `tasks/:id` under the engine's mount path. The person who started the task sees its task type's title, its status and, once the runner has set a total, a progress bar of done against total. While the task runs and is at least five percent done, an estimate of the time left sits under the bar, worked out from how fast the task has gone since it started running. The page listens for changes to its task over Turbo Streams, so its status, progress and time left update without a reload, in a browser and in the Hotwire Native iOS app alike. Anyone else, and the same person viewing from another account, gets a not-found response.
|
|
140
148
|
|
|
141
149
|
The task page and the task list reload in place when they are shown again after being hidden, such as when the iOS app returns from the background, so they show changes broadcast while they could not receive them. A page that stays visible makes no extra request. The copied controller is registered by the host's own Stimulus setup, as `wallflower--catch-up`.
|
|
142
150
|
|
|
@@ -4,16 +4,19 @@ module Wallflower
|
|
|
4
4
|
class RunJob < ActiveJob::Base
|
|
5
5
|
def perform(task)
|
|
6
6
|
run(task)
|
|
7
|
-
Wallflower.configuration.on_finish&.call(task)
|
|
7
|
+
Wallflower.configuration.on_finish&.call(task) if task.status.in?(%w[finished failed])
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
12
|
def run(task)
|
|
13
|
-
task.
|
|
14
|
-
|
|
13
|
+
task.refusals.destroy_all
|
|
14
|
+
task.update!(status: "running", started_at: Time.current, done: 0)
|
|
15
|
+
Wallflower.task_type(task.task_type).runner.constantize.new.call(task)
|
|
15
16
|
task.update!(status: "finished", finished_at: Time.current)
|
|
16
17
|
rescue StandardError => error
|
|
18
|
+
return retry_job(wait: Wallflower.task_type(task.task_type).retry_wait) if executions < Wallflower.task_type(task.task_type).tries
|
|
19
|
+
|
|
17
20
|
task.update!(status: "failed", error_message: error.message, finished_at: Time.current)
|
|
18
21
|
Rails.error.report(error, handled: true, context: { wallflower_task_id: task.id })
|
|
19
22
|
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<% @tasks.each do |task| %>
|
|
5
5
|
<div data-task-row>
|
|
6
6
|
<%= turbo_stream_from task %>
|
|
7
|
-
<%= link_to task.
|
|
7
|
+
<%= link_to task.task_type_title, task_path(task) %>
|
|
8
8
|
<%= render "wallflower/tasks/live", task: task %>
|
|
9
9
|
<p>Started <%= task.created_at.strftime("%b %-d, %Y") %></p>
|
|
10
10
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= turbo_stream_from @task %>
|
|
2
2
|
<%= tag.div(data: { controller: "wallflower--catch-up" }) %>
|
|
3
3
|
<%= ui_page(max_width: :md) do %>
|
|
4
|
-
<h1><%= @task.
|
|
4
|
+
<h1><%= @task.task_type_title %></h1>
|
|
5
5
|
<%= render "wallflower/tasks/live", task: @task %>
|
|
6
6
|
<% if @task.status == "finished" %>
|
|
7
7
|
<p id="<%= dom_id(@task, :outcome) %>"><%= @task.changed_count %> changed, <%= @task.refusals.size %> refused</p>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class CreateWallflowerTasks < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
3
|
create_table :wallflower_tasks do |t|
|
|
4
|
-
t.string :
|
|
4
|
+
t.string :task_type, null: false
|
|
5
5
|
t.string :status, null: false, default: "queued"
|
|
6
6
|
t.json :params, null: false, default: {}
|
|
7
7
|
t.integer :total
|
|
@@ -28,6 +28,10 @@ module Wallflower
|
|
|
28
28
|
migration_template "add_started_at_to_wallflower_tasks.rb.erb", "db/migrate/add_started_at_to_wallflower_tasks.rb"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def copy_task_type_migration
|
|
32
|
+
migration_template "rename_wallflower_tasks_kind_to_task_type.rb.erb", "db/migrate/rename_wallflower_tasks_kind_to_task_type.rb"
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
def copy_catch_up_controller
|
|
32
36
|
source = File.expand_path("../../../../app/javascript/wallflower/catch_up_controller.js", __dir__)
|
|
33
37
|
create_file "app/javascript/controllers/wallflower/catch_up_controller.js", File.read(source)
|
data/lib/wallflower/engine.rb
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wallflower
|
|
4
|
+
TaskType = Struct.new(:key, :title, :runner, :tries, :retry_wait, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
class MissingRunner < StandardError; end
|
|
7
|
+
|
|
8
|
+
def self.register_task_type(key, title:, runner:, tries: 1, retry_wait: 60)
|
|
9
|
+
task_types[key.to_sym] = TaskType.new(key: key.to_sym, title: title, runner: runner.to_s, tries: tries, retry_wait: retry_wait)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.task_type(key)
|
|
13
|
+
task_types.fetch(key.to_sym)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.task_types
|
|
17
|
+
@task_types ||= {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.check_task_types!
|
|
21
|
+
task_types.each_value do |task_type|
|
|
22
|
+
next if task_type.runner.safe_constantize
|
|
23
|
+
|
|
24
|
+
raise MissingRunner, "Wallflower task_type #{task_type.key} names the runner #{task_type.runner}, which does not exist."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.reset_task_types!
|
|
29
|
+
@task_types = {}
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/wallflower/version.rb
CHANGED
data/lib/wallflower.rb
CHANGED
|
@@ -2,11 +2,11 @@ require "keystone_ui"
|
|
|
2
2
|
require "turbo-rails"
|
|
3
3
|
require "wallflower/version"
|
|
4
4
|
require "wallflower/configuration"
|
|
5
|
-
require "wallflower/
|
|
5
|
+
require "wallflower/task_types"
|
|
6
6
|
require "wallflower/engine"
|
|
7
7
|
|
|
8
8
|
module Wallflower
|
|
9
|
-
def self.start(
|
|
10
|
-
Task.create!(
|
|
9
|
+
def self.start(task_type:, person:, account: nil, params: {})
|
|
10
|
+
Task.create!(task_type: task_type.to_s, person: person, account: account, params: params).tap { |task| RunJob.perform_later(task) }
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallflower-develop
|
|
3
|
-
description: Use PROACTIVELY for adding long-running work that a person starts and follows in a Rails app, such as an export or a report, including registering the
|
|
3
|
+
description: Use PROACTIVELY for adding long-running work that a person starts and follows in a Rails app, such as an export or a report, including registering the task type, writing its runner, starting it, reporting progress, attaching a result file, linking a result page and refusing rows — MUST BE USED instead of hand-rolling a background job with its own status column, progress tracking, status page or file download.
|
|
4
4
|
tools: Bash, Read, Write, Edit, Grep
|
|
5
5
|
scope: background tasks — starting long-running work, following its status and progress, and getting its result
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
This local implements background tasks with Wallflower. It always registers a
|
|
8
|
+
This local implements background tasks with Wallflower. It always registers a task type, writes the task type's runner, and starts the task through `Wallflower.start`, asking the developer each decision listed below before writing code.
|
|
9
9
|
|
|
10
10
|
## What wallflower is
|
|
11
11
|
|
|
@@ -13,8 +13,8 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
|
|
|
13
13
|
|
|
14
14
|
## Interface
|
|
15
15
|
|
|
16
|
-
- `Wallflower.
|
|
17
|
-
- `Wallflower.start` — `Wallflower.start(
|
|
16
|
+
- `Wallflower.register_task_type` — `Wallflower.register_task_type(key, title:, runner:, tries: 1, retry_wait: 60)` registers a type of task, with its key, the title people see, the name of its runner class, how many tries it gets and the seconds between them.
|
|
17
|
+
- `Wallflower.start` — `Wallflower.start(task_type:, person:, account: nil, params: {})` creates a queued task, enqueues its job, and returns the task.
|
|
18
18
|
- `set_total` — `task.set_total(total)` saves the number of rows the task will work through.
|
|
19
19
|
- `advance` — `task.advance(by = 1)` adds to the rows done, saving at most once a second.
|
|
20
20
|
- `attach_result` — `task.attach_result(io:, filename:)` attaches a file as the task's result.
|
|
@@ -24,17 +24,17 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
|
|
|
24
24
|
## How to use it
|
|
25
25
|
|
|
26
26
|
1. Ask the developer these before writing code:
|
|
27
|
-
- What is the
|
|
27
|
+
- What is the task type's key, its title, and its runner's class name?
|
|
28
28
|
- Who is the person who starts the task, and does it belong to an account?
|
|
29
29
|
- What are the task's params?
|
|
30
30
|
- Does the task work through rows, and can a row be refused?
|
|
31
31
|
- Is the result a file, a page in the app, both, or nothing?
|
|
32
32
|
- Where does the person go after starting the task?
|
|
33
33
|
|
|
34
|
-
2. Register the
|
|
34
|
+
2. Register the task type in an initializer:
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
37
|
-
Wallflower.
|
|
37
|
+
Wallflower.register_task_type :export_transactions, title: "Export transactions", runner: "ExportTransactionsRunner"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
3. Write the runner as a class in the app. Wallflower calls `new` on it with no arguments, then `call` with the task. The app fails to boot, with a message naming the class, if the registered class does not exist.
|
|
@@ -67,10 +67,10 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
|
|
|
67
67
|
6. Start the task from the app's own code, passing params as a hash with string keys, since they are stored in a JSON column:
|
|
68
68
|
|
|
69
69
|
```ruby
|
|
70
|
-
task = Wallflower.start(
|
|
70
|
+
task = Wallflower.start(task_type: :export_transactions, person: current_user, account: current_account, params: { "month" => "2026-10" })
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`person` is required and `account` is optional. `start` does not check that the
|
|
73
|
+
`person` is required and `account` is optional. `start` does not check that the task type is registered. The runner runs when the job runs, not during this call.
|
|
74
74
|
|
|
75
75
|
7. Send the person to the task. Its page is at `tasks/:id` under the path where the engine is mounted, and the list of tasks is at the root of that path.
|
|
76
76
|
|
|
@@ -79,6 +79,7 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
|
|
|
79
79
|
- Pass the same person the app treats as the current person, since the task list, the task page and the download answer only that person.
|
|
80
80
|
- When the app has a current account configured, pass that same account, since those pages then show only tasks in that account and a task started with another account or none does not appear.
|
|
81
81
|
- The job sets the task's status, so a runner never sets it. The task is marked running before the runner is called and finished when the runner returns.
|
|
82
|
-
-
|
|
82
|
+
- A task type with more than one try runs its runner again after a `StandardError`. Between tries the task stays running and each try starts its progress and refusals over, so that runner must be safe to run again.
|
|
83
|
+
- To fail a task, let a `StandardError` leave the runner on its last try. The task is marked failed, its message is stored and shown to the person on the task's page, and the error is reported through `Rails.error`. Write that message for the person reading it, and do not rescue an error the person needs to see, since a runner that returns marks the task finished.
|
|
83
84
|
- A running task with no update for longer than the app's stall time, one hour by default, can be marked failed as stalled. Call `advance` or `set_total` often enough that a slow step does not reach that time.
|
|
84
85
|
- Installing the gem, setting up its pages, its configuration, code that runs when a task finishes, and scheduling stalled-task checks and old-task deletion are out of scope. They belong to the install local.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallflower-info
|
|
3
|
-
description: Use to learn what wallflower offers — background tasks,
|
|
3
|
+
description: Use to learn what wallflower offers — background tasks, task types and runners, task statuses and progress, results, and the task pages.
|
|
4
4
|
tools: Read
|
|
5
5
|
scope: background tasks — starting long-running work, following its status and progress, and getting its result
|
|
6
6
|
---
|
|
@@ -15,17 +15,17 @@ Reach for it when an app has work that runs in a background job and the person w
|
|
|
15
15
|
|
|
16
16
|
## Interface
|
|
17
17
|
|
|
18
|
-
This local documents no commands, because wallflower declares none for it. The install local owns adding the gem to an app: its generators, mounting its engine, its settings and scheduling its recurring sweep. The develop local owns building on it: registering
|
|
18
|
+
This local documents no commands, because wallflower declares none for it. The install local owns adding the gem to an app: its generators, mounting its engine, its settings and scheduling its recurring sweep. The develop local owns building on it: registering task types, writing runners, starting tasks, and reporting progress and results from a runner.
|
|
19
19
|
|
|
20
20
|
## How to use it
|
|
21
21
|
|
|
22
22
|
- To hook wallflower into an app or update it there, use `wallflower-install`.
|
|
23
|
-
- To add a
|
|
23
|
+
- To add a task type or change what a task does, use `wallflower-develop`.
|
|
24
24
|
|
|
25
25
|
## Conventions
|
|
26
26
|
|
|
27
27
|
- **Task**: one run of long-running work, stored with the params, person and account it was started with.
|
|
28
|
-
- **
|
|
28
|
+
- **Task type**: a type of task the app offers, such as an export, with a key, a title and the name of its runner class.
|
|
29
29
|
- **Runner**: a plain class whose `call` receives the task and does the work.
|
|
30
30
|
- **Person**: the record that started a task. Only that person can see the task.
|
|
31
31
|
- **Account**: the optional record a task belongs to. When the app names a current account, a person sees only the tasks in that account.
|
data/the_local/interface.yml
CHANGED
|
@@ -8,7 +8,7 @@ install:
|
|
|
8
8
|
- Wallflower::SweepJob
|
|
9
9
|
|
|
10
10
|
develop:
|
|
11
|
-
- Wallflower.
|
|
11
|
+
- Wallflower.register_task_type
|
|
12
12
|
- Wallflower.start
|
|
13
13
|
- set_total
|
|
14
14
|
- advance
|
|
@@ -19,7 +19,7 @@ develop:
|
|
|
19
19
|
sources:
|
|
20
20
|
- lib/wallflower.rb
|
|
21
21
|
- lib/wallflower/configuration.rb
|
|
22
|
-
- lib/wallflower/
|
|
22
|
+
- lib/wallflower/task_types.rb
|
|
23
23
|
- lib/wallflower/engine.rb
|
|
24
24
|
- app/models/wallflower/task.rb
|
|
25
25
|
- app/models/wallflower/refusal.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wallflower
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
@@ -109,11 +109,12 @@ files:
|
|
|
109
109
|
- lib/generators/wallflower/install/templates/add_started_at_to_wallflower_tasks.rb.erb
|
|
110
110
|
- lib/generators/wallflower/install/templates/create_wallflower_refusals.rb.erb
|
|
111
111
|
- lib/generators/wallflower/install/templates/create_wallflower_tasks.rb.erb
|
|
112
|
+
- lib/generators/wallflower/install/templates/rename_wallflower_tasks_kind_to_task_type.rb.erb
|
|
112
113
|
- lib/generators/wallflower/update/update_generator.rb
|
|
113
114
|
- lib/wallflower.rb
|
|
114
115
|
- lib/wallflower/configuration.rb
|
|
115
116
|
- lib/wallflower/engine.rb
|
|
116
|
-
- lib/wallflower/
|
|
117
|
+
- lib/wallflower/task_types.rb
|
|
117
118
|
- lib/wallflower/version.rb
|
|
118
119
|
- the_local/agents/wallflower-develop.md
|
|
119
120
|
- the_local/agents/wallflower-info.md
|
data/lib/wallflower/kinds.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Wallflower
|
|
4
|
-
Kind = Struct.new(:key, :title, :runner, keyword_init: true)
|
|
5
|
-
|
|
6
|
-
class MissingRunner < StandardError; end
|
|
7
|
-
|
|
8
|
-
def self.register_kind(key, title:, runner:)
|
|
9
|
-
kinds[key.to_sym] = Kind.new(key: key.to_sym, title: title, runner: runner.to_s)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.kind(key)
|
|
13
|
-
kinds.fetch(key.to_sym)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.kinds
|
|
17
|
-
@kinds ||= {}
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.check_kinds!
|
|
21
|
-
kinds.each_value do |kind|
|
|
22
|
-
next if kind.runner.safe_constantize
|
|
23
|
-
|
|
24
|
-
raise MissingRunner, "Wallflower kind #{kind.key} names the runner #{kind.runner}, which does not exist."
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.reset_kinds!
|
|
29
|
-
@kinds = {}
|
|
30
|
-
end
|
|
31
|
-
end
|