roast-ai 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.
- checksums.yaml +4 -4
- data/.claude/commands/docs/write-comments.md +36 -0
- data/.github/CODEOWNERS +1 -1
- data/.github/workflows/ci.yaml +10 -6
- data/.gitignore +0 -1
- data/.rubocop.yml +7 -1
- data/CLAUDE.md +2 -2
- data/CONTRIBUTING.md +2 -0
- data/Gemfile +18 -18
- data/Gemfile.lock +46 -57
- data/README.md +118 -1432
- data/README_LEGACY.md +1464 -0
- data/Rakefile +39 -4
- data/dev.yml +29 -0
- data/dsl/agent_sessions.rb +20 -0
- data/dsl/async_cogs.rb +49 -0
- data/dsl/async_cogs_complex.rb +67 -0
- data/dsl/call.rb +44 -0
- data/dsl/collect_from.rb +72 -0
- data/dsl/demo/Gemfile +4 -0
- data/dsl/demo/Gemfile.lock +120 -0
- data/dsl/demo/cogs/local.rb +15 -0
- data/dsl/demo/simple_external_cog.rb +17 -0
- data/dsl/json_output.rb +28 -0
- data/dsl/map.rb +55 -0
- data/dsl/map_reduce.rb +37 -0
- data/dsl/map_with_index.rb +49 -0
- data/dsl/next_break.rb +40 -0
- data/dsl/next_break_parallel.rb +44 -0
- data/dsl/outputs.rb +39 -0
- data/dsl/outputs_bang.rb +36 -0
- data/dsl/parallel_map.rb +37 -0
- data/dsl/plugin-gem-example/.gitignore +8 -0
- data/dsl/plugin-gem-example/Gemfile +13 -0
- data/dsl/plugin-gem-example/Gemfile.lock +178 -0
- data/dsl/plugin-gem-example/lib/other.rb +17 -0
- data/dsl/plugin-gem-example/lib/plugin_gem_example.rb +5 -0
- data/dsl/plugin-gem-example/lib/simple.rb +15 -0
- data/dsl/plugin-gem-example/lib/version.rb +10 -0
- data/dsl/plugin-gem-example/plugin-gem-example.gemspec +28 -0
- data/dsl/prompts/simple_prompt.md.erb +3 -0
- data/dsl/prototype.rb +10 -4
- data/dsl/repeat_loop_results.rb +53 -0
- data/dsl/ruby_cog.rb +72 -0
- data/dsl/simple_agent.rb +18 -0
- data/dsl/simple_chat.rb +26 -0
- data/dsl/simple_repeat.rb +29 -0
- data/dsl/skip.rb +36 -0
- data/dsl/step_communication.rb +10 -5
- data/dsl/targets_and_params.rb +57 -0
- data/dsl/temperature.rb +17 -0
- data/dsl/temporary_directory.rb +22 -0
- data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
- data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
- data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
- data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
- data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
- data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
- data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
- data/dsl/tutorial/03_targets_and_params/README.md +230 -0
- data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
- data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
- data/dsl/tutorial/04_configuration_options/README.md +209 -0
- data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
- data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
- data/dsl/tutorial/05_control_flow/README.md +156 -0
- data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
- data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
- data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
- data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
- data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
- data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
- data/dsl/tutorial/07_processing_collections/README.md +152 -0
- data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
- data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
- data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
- data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
- data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
- data/dsl/tutorial/09_async_cogs/README.md +197 -0
- data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
- data/dsl/tutorial/README.md +222 -0
- data/dsl/working_directory.rb +16 -0
- data/exe/roast +1 -1
- data/internal/documentation/architectural-notes.md +115 -0
- data/internal/documentation/doc-comments-external.md +686 -0
- data/internal/documentation/doc-comments-internal.md +342 -0
- data/internal/documentation/doc-comments.md +211 -0
- data/lib/roast/dsl/cog/config.rb +280 -4
- data/lib/roast/dsl/cog/input.rb +73 -0
- data/lib/roast/dsl/cog/output.rb +313 -0
- data/lib/roast/dsl/cog/registry.rb +71 -0
- data/lib/roast/dsl/cog/stack.rb +3 -2
- data/lib/roast/dsl/cog/store.rb +11 -8
- data/lib/roast/dsl/cog.rb +108 -31
- data/lib/roast/dsl/cog_input_context.rb +44 -0
- data/lib/roast/dsl/cog_input_manager.rb +156 -0
- data/lib/roast/dsl/cogs/agent/config.rb +465 -0
- data/lib/roast/dsl/cogs/agent/input.rb +81 -0
- data/lib/roast/dsl/cogs/agent/output.rb +59 -0
- data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
- data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
- data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
- data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
- data/lib/roast/dsl/cogs/agent.rb +75 -0
- data/lib/roast/dsl/cogs/chat/config.rb +453 -0
- data/lib/roast/dsl/cogs/chat/input.rb +92 -0
- data/lib/roast/dsl/cogs/chat/output.rb +64 -0
- data/lib/roast/dsl/cogs/chat/session.rb +68 -0
- data/lib/roast/dsl/cogs/chat.rb +81 -0
- data/lib/roast/dsl/cogs/cmd.rb +291 -27
- data/lib/roast/dsl/cogs/ruby.rb +171 -0
- data/lib/roast/dsl/command_runner.rb +191 -0
- data/lib/roast/dsl/config_context.rb +2 -47
- data/lib/roast/dsl/config_manager.rb +143 -0
- data/lib/roast/dsl/control_flow.rb +41 -0
- data/lib/roast/dsl/execution_context.rb +9 -0
- data/lib/roast/dsl/execution_manager.rb +267 -0
- data/lib/roast/dsl/nil_assertions.rb +23 -0
- data/lib/roast/dsl/system_cog/params.rb +32 -0
- data/lib/roast/dsl/system_cog.rb +36 -0
- data/lib/roast/dsl/system_cogs/call.rb +162 -0
- data/lib/roast/dsl/system_cogs/map.rb +448 -0
- data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
- data/lib/roast/dsl/workflow.rb +123 -0
- data/lib/roast/dsl/workflow_context.rb +20 -0
- data/lib/roast/dsl/workflow_params.rb +24 -0
- data/lib/roast/sorbet_runtime_stub.rb +154 -0
- data/lib/roast/tools/apply_diff.rb +1 -3
- data/lib/roast/tools/cmd.rb +4 -3
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/update_files.rb +1 -1
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +4 -0
- data/lib/roast/workflow/step_loader.rb +14 -2
- data/lib/roast-ai.rb +4 -0
- data/lib/roast.rb +60 -22
- data/{roast.gemspec → roast-ai.gemspec} +10 -13
- data/sorbet/config +1 -0
- data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
- data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
- data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
- data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
- data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
- data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
- data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
- data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
- data/sorbet/rbi/gems/marcel@1.1.0.rbi +239 -0
- data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
- data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +5703 -0
- data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
- data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1197 -0
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +314 -2
- data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +498 -0
- data/sorbet/tapioca/config.yml +6 -0
- data/sorbet/tapioca/require.rb +2 -0
- metadata +198 -34
- data/dsl/less_simple.rb +0 -112
- data/dsl/simple.rb +0 -8
- data/lib/roast/dsl/cog_execution_context.rb +0 -29
- data/lib/roast/dsl/cogs/graph.rb +0 -53
- data/lib/roast/dsl/cogs.rb +0 -65
- data/lib/roast/dsl/executor.rb +0 -82
- data/lib/roast/dsl/workflow_execution_context.rb +0 -47
- data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
- data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
- data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
- data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
- data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
- data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
- data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
- data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
- data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
- data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
- data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
- data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
- data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
- data/sorbet/rbi/gems/rack@2.2.18.rbi +0 -5659
- data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +0 -2170
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
- data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +0 -11
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
# The canonical way to execute shell commands in Roast.
|
|
7
|
+
#
|
|
8
|
+
# CommandRunner is the standard command execution interface for DSL cogs
|
|
9
|
+
# and should be used for all command invocations in this project.
|
|
10
|
+
#
|
|
11
|
+
# Features:
|
|
12
|
+
# - Separate stdout/stderr capture (using Async fibers for concurrency)
|
|
13
|
+
# - Line-by-line streaming callbacks for custom handling
|
|
14
|
+
# - Optional timeout support with automatic process cleanup
|
|
15
|
+
# - Direct command execution (no shell by default for safety)
|
|
16
|
+
#
|
|
17
|
+
# Note: Currently executes commands directly without shell features.
|
|
18
|
+
# Shell support (pipes, redirects, etc.) will be added in a future version.
|
|
19
|
+
class CommandRunner
|
|
20
|
+
class CommandRunnerError < StandardError; end
|
|
21
|
+
|
|
22
|
+
class NoCommandProvidedError < CommandRunnerError; end
|
|
23
|
+
|
|
24
|
+
class TimeoutError < CommandRunnerError; end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Execute a command with optional stream handlers
|
|
28
|
+
#
|
|
29
|
+
# @param args [Array<String>] Command and arguments as an array
|
|
30
|
+
# @param timeout [Integer, nil] Timeout in seconds (default: nil, no timeout)
|
|
31
|
+
# @param stdout_handler [Proc, nil] Called for each stdout line
|
|
32
|
+
# @param stderr_handler [Proc, nil] Called for each stderr line
|
|
33
|
+
# @return [Array<String, String, Process::Status>] stdout, stderr, status
|
|
34
|
+
#
|
|
35
|
+
# @example Basic usage
|
|
36
|
+
# stdout, stderr, status = CommandRunner.execute(["echo", "hello"])
|
|
37
|
+
#
|
|
38
|
+
# @example With handlers for streaming output
|
|
39
|
+
# CommandRunner.execute(
|
|
40
|
+
# ["ls", "-la"],
|
|
41
|
+
# stdout_handler: ->(line) { puts "[OUT] #{line}" }
|
|
42
|
+
# )
|
|
43
|
+
#
|
|
44
|
+
# @example With explicit timeout
|
|
45
|
+
# CommandRunner.execute(["sleep", "5"], timeout: 2) # Will timeout after 2 seconds
|
|
46
|
+
#: (
|
|
47
|
+
#| Array[String],
|
|
48
|
+
#| ?working_directory: (Pathname | String)?,
|
|
49
|
+
#| ?timeout: (Integer | Float)?,
|
|
50
|
+
#| ?stdin_content: String?,
|
|
51
|
+
#| ?stdout_handler: (^(String) -> void)?,
|
|
52
|
+
#| ?stderr_handler: (^(String) -> void)?,
|
|
53
|
+
#| ) -> [String, String, Process::Status]
|
|
54
|
+
def execute(
|
|
55
|
+
args,
|
|
56
|
+
working_directory: nil,
|
|
57
|
+
timeout: nil,
|
|
58
|
+
stdin_content: nil,
|
|
59
|
+
stdout_handler: nil,
|
|
60
|
+
stderr_handler: nil
|
|
61
|
+
)
|
|
62
|
+
args.compact!
|
|
63
|
+
raise NoCommandProvidedError if args.blank?
|
|
64
|
+
|
|
65
|
+
stdin, stdout, stderr, wait_thread = Open3 #: as untyped
|
|
66
|
+
.popen3(
|
|
67
|
+
{ "PWD" => working_directory&.to_s }.compact,
|
|
68
|
+
*args,
|
|
69
|
+
{ chdir: working_directory }.compact,
|
|
70
|
+
)
|
|
71
|
+
stdin.puts stdin_content if stdin_content.present?
|
|
72
|
+
stdin.close
|
|
73
|
+
pid = wait_thread.pid
|
|
74
|
+
|
|
75
|
+
# If timeout is specified, start a timer in a separate fiber
|
|
76
|
+
timeout_task = if timeout
|
|
77
|
+
Async do |task|
|
|
78
|
+
task.annotate("CommandRunner Timeout Monitor")
|
|
79
|
+
sleep(timeout)
|
|
80
|
+
kill_process(pid) if pid
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Read stdout and stderr concurrently
|
|
85
|
+
stdout_content, stderr_content = Sync do |sync_task|
|
|
86
|
+
sync_task.annotate("CommandRunner Process Handler")
|
|
87
|
+
stdout_task = Async do |task|
|
|
88
|
+
task.annotate("CommandRunner Standard Output Reader")
|
|
89
|
+
buffer = "" #: String
|
|
90
|
+
stdout.each_line do |line|
|
|
91
|
+
buffer += line
|
|
92
|
+
begin
|
|
93
|
+
stdout_handler&.call(line)
|
|
94
|
+
rescue => e
|
|
95
|
+
Roast::Helpers::Logger.debug("stdout_handler raised: #{e.class} - #{e.message}")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
buffer
|
|
99
|
+
rescue IOError
|
|
100
|
+
buffer
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
stderr_task = Async do |task|
|
|
104
|
+
task.annotate("CommandRunner Standard Error Reader")
|
|
105
|
+
buffer = "" #: String
|
|
106
|
+
stderr.each_line do |line|
|
|
107
|
+
buffer += line
|
|
108
|
+
begin
|
|
109
|
+
stderr_handler&.call(line)
|
|
110
|
+
rescue => e
|
|
111
|
+
Roast::Helpers::Logger.debug("stderr_handler raised: #{e.class} - #{e.message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
buffer
|
|
115
|
+
rescue IOError
|
|
116
|
+
buffer
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
[stdout_task.wait, stderr_task.wait]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Wait for the process to complete
|
|
123
|
+
status = wait_thread.value
|
|
124
|
+
|
|
125
|
+
# Cancel the timeout task if it's still running
|
|
126
|
+
timeout_task&.stop
|
|
127
|
+
|
|
128
|
+
# Check if the process was killed due to timeout
|
|
129
|
+
if timeout && status.signaled? && (status.termsig == 15 || status.termsig == 9)
|
|
130
|
+
raise TimeoutError, "Command timed out after #{timeout} seconds"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
[stdout_content, stderr_content, status]
|
|
134
|
+
ensure
|
|
135
|
+
# Clean up resources
|
|
136
|
+
begin
|
|
137
|
+
[stdout, stderr].compact.each(&:close)
|
|
138
|
+
rescue
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
# If we haven't waited for the process yet, kill it
|
|
142
|
+
if pid && wait_thread&.alive?
|
|
143
|
+
Async do |task|
|
|
144
|
+
task.annotate("CommandRunner Process Killer")
|
|
145
|
+
kill_process(pid)
|
|
146
|
+
end.wait
|
|
147
|
+
wait_thread.join(1) # Give it a second to finish
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
#: (Integer) -> void
|
|
154
|
+
def kill_process(pid)
|
|
155
|
+
return unless process_running?(pid)
|
|
156
|
+
|
|
157
|
+
# First try TERM signal
|
|
158
|
+
Process.kill("TERM", pid)
|
|
159
|
+
|
|
160
|
+
# Give process a short time to terminate gracefully
|
|
161
|
+
5.times do
|
|
162
|
+
sleep(0.02)
|
|
163
|
+
return unless process_running?(pid)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# If still running, use KILL signal
|
|
167
|
+
Process.kill("KILL", pid) if process_running?(pid)
|
|
168
|
+
|
|
169
|
+
# Also try to kill the process group to ensure child processes are killed
|
|
170
|
+
begin
|
|
171
|
+
Process.kill("-KILL", pid)
|
|
172
|
+
rescue
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
rescue Errno::ESRCH
|
|
176
|
+
# Process already terminated
|
|
177
|
+
rescue Errno::EPERM
|
|
178
|
+
Roast::Helpers::Logger.debug("Could not kill process #{pid}: Permission denied")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#: (Integer) -> bool
|
|
182
|
+
def process_running?(pid)
|
|
183
|
+
Process.getpgid(pid)
|
|
184
|
+
true
|
|
185
|
+
rescue Errno::ESRCH
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -3,52 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
module Roast
|
|
5
5
|
module DSL
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@cogs = cogs
|
|
9
|
-
@executor_scoped_configs = {}
|
|
10
|
-
@cog_scoped_configs = {}
|
|
11
|
-
@config_proc = config_proc
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def fetch_merged_config(cog_class, name = nil)
|
|
15
|
-
# All configs have an entry, even if it's empty.
|
|
16
|
-
configs = fetch_execution_scope(cog_class)
|
|
17
|
-
instance_configs = fetch_cog_config(cog_class, name) unless name.nil?
|
|
18
|
-
configs = configs.merge(instance_configs) if instance_configs
|
|
19
|
-
configs
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def prepare!
|
|
23
|
-
bind_default_cogs
|
|
24
|
-
instance_eval(&@config_proc)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
#: () -> void
|
|
28
|
-
def bind_default_cogs
|
|
29
|
-
bind_cog(Cogs::Cmd, :cmd)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def fetch_cog_config(cog_class, name)
|
|
33
|
-
@cog_scoped_configs[cog_class][name]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def fetch_or_create_cog_config(cog_class, name)
|
|
37
|
-
@cog_scoped_configs[cog_class][name] = cog_class.config_class.new unless @cog_scoped_configs.key?(name)
|
|
38
|
-
@cog_scoped_configs[cog_class][name]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def fetch_execution_scope(cog_class)
|
|
42
|
-
@executor_scoped_configs[cog_class]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def bind_cog(cog_class, method_name)
|
|
46
|
-
@cog_scoped_configs[cog_class] = {}
|
|
47
|
-
@executor_scoped_configs[cog_class] = cog_class.config_class.new
|
|
48
|
-
instance_eval do
|
|
49
|
-
define_singleton_method(method_name, &cog_class.on_config)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
6
|
+
# Context in which the `config` blocks of a workflow definition are evaluated
|
|
7
|
+
class ConfigContext; end
|
|
53
8
|
end
|
|
54
9
|
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
class ConfigManager
|
|
7
|
+
class ConfigManagerError < Roast::Error; end
|
|
8
|
+
class ConfigManagerNotPreparedError < ConfigManagerError; end
|
|
9
|
+
class ConfigManagerAlreadyPreparedError < ConfigManagerError; end
|
|
10
|
+
class IllegalCogNameError < ConfigManagerError; end
|
|
11
|
+
|
|
12
|
+
#: (Cog::Registry, Array[^() -> void]) -> void
|
|
13
|
+
def initialize(cog_registry, config_procs)
|
|
14
|
+
@cog_registry = cog_registry
|
|
15
|
+
@config_procs = config_procs
|
|
16
|
+
@config_context = ConfigContext.new #: ConfigContext
|
|
17
|
+
@global_config = Cog::Config.new #: Cog::Config
|
|
18
|
+
@general_configs = {} #: Hash[singleton(Cog), Cog::Config]
|
|
19
|
+
@regexp_scoped_configs = {} #: Hash[singleton(Cog), Hash[Regexp, Cog::Config]]
|
|
20
|
+
@name_scoped_configs = {} #: Hash[singleton(Cog), Hash[Symbol, Cog::Config]]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: () -> void
|
|
24
|
+
def prepare!
|
|
25
|
+
raise ConfigManagerAlreadyPreparedError if preparing? || prepared?
|
|
26
|
+
|
|
27
|
+
@preparing = true
|
|
28
|
+
bind_global
|
|
29
|
+
bind_registered_cogs
|
|
30
|
+
@config_procs.each { |cp| @config_context.instance_eval(&cp) }
|
|
31
|
+
@prepared = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: () -> bool
|
|
35
|
+
def preparing?
|
|
36
|
+
@preparing ||= false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#: () -> bool
|
|
40
|
+
def prepared?
|
|
41
|
+
@prepared ||= false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: (singleton(Cog), ?Symbol?) -> Cog::Config
|
|
45
|
+
def config_for(cog_class, name = nil)
|
|
46
|
+
raise ConfigManagerNotPreparedError unless prepared?
|
|
47
|
+
|
|
48
|
+
# All cogs will always have a config; empty by default if the cog was never explicitly configured
|
|
49
|
+
config = cog_class.config_class.new(@global_config.instance_variable_get(:@values).deep_dup)
|
|
50
|
+
config = config.merge(fetch_general_config(cog_class))
|
|
51
|
+
@regexp_scoped_configs.fetch(cog_class, {}).select do |pattern, _|
|
|
52
|
+
pattern.match?(name.to_s) unless name.nil?
|
|
53
|
+
end.values.each { |cfg| config = config.merge(cfg) }
|
|
54
|
+
unless name.nil?
|
|
55
|
+
name_scoped_config = fetch_name_scoped_config(cog_class, name)
|
|
56
|
+
config = config.merge(name_scoped_config)
|
|
57
|
+
end
|
|
58
|
+
config.validate!
|
|
59
|
+
config
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
#: (singleton(Cog)) -> Cog::Config
|
|
65
|
+
def fetch_general_config(cog_class)
|
|
66
|
+
@general_configs[cog_class] ||= cog_class.config_class.new
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: (singleton(Cog), Regexp) -> Cog::Config
|
|
70
|
+
def fetch_regexp_scoped_config(cog_class, pattern)
|
|
71
|
+
regexp_scoped_configs_for_cog = @regexp_scoped_configs[cog_class] ||= {}
|
|
72
|
+
regexp_scoped_configs_for_cog[pattern] ||= cog_class.config_class.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#: (singleton(Cog), Symbol) -> Cog::Config
|
|
76
|
+
def fetch_name_scoped_config(cog_class, name)
|
|
77
|
+
name_scoped_configs_for_cog = @name_scoped_configs[cog_class] ||= {}
|
|
78
|
+
name_scoped_configs_for_cog[name] ||= cog_class.config_class.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#: () -> void
|
|
82
|
+
def bind_registered_cogs
|
|
83
|
+
@cog_registry.cogs.each { |cog_method_name, cog_class| bind_cog(cog_method_name, cog_class) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
#: (Symbol, singleton(Cog)) -> void
|
|
87
|
+
def bind_cog(cog_method_name, cog_class)
|
|
88
|
+
on_config_method = method(:on_config)
|
|
89
|
+
cog_method = proc do |cog_name_or_pattern = nil, &cog_config_proc|
|
|
90
|
+
on_config_method.call(cog_class, cog_name_or_pattern, cog_config_proc)
|
|
91
|
+
end
|
|
92
|
+
@config_context.instance_eval do
|
|
93
|
+
raise IllegalCogNameError, cog_method_name if respond_to?(cog_method_name, true)
|
|
94
|
+
|
|
95
|
+
define_singleton_method(cog_method_name, cog_method)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: (singleton(Cog), (Symbol | Regexp)?, ^() -> void ) -> void
|
|
100
|
+
def on_config(cog_class, cog_name_or_pattern, cog_config_proc)
|
|
101
|
+
# Called when the cog method is invoked in the workflow's 'config' block.
|
|
102
|
+
# This allows configuration parameters to be set for the cog generally or for a specific named instance
|
|
103
|
+
|
|
104
|
+
# NOTE: cast to untyped is to intentional handling the 'unreachable' else case here.
|
|
105
|
+
# This method takes user input directly so additional validation with a clearer exception message will be helpful
|
|
106
|
+
cog_name_or_pattern = cog_name_or_pattern #: untyped
|
|
107
|
+
config_object = case cog_name_or_pattern
|
|
108
|
+
when NilClass
|
|
109
|
+
fetch_general_config(cog_class)
|
|
110
|
+
when Regexp
|
|
111
|
+
fetch_regexp_scoped_config(cog_class, cog_name_or_pattern)
|
|
112
|
+
when Symbol
|
|
113
|
+
fetch_name_scoped_config(cog_class, cog_name_or_pattern)
|
|
114
|
+
else
|
|
115
|
+
raise ArgumentError, "Invalid type '#{cog_name_or_pattern.class}' for cog_name_or_pattern"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# NOTE: Sorbet expects the proc passed to instance_exec to be declared as taking an argument
|
|
119
|
+
# but our cog_config_proc does not get an argument
|
|
120
|
+
cog_config_proc = cog_config_proc #: as ^(untyped) -> void
|
|
121
|
+
config_object.instance_exec(&cog_config_proc) if cog_config_proc
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def bind_global
|
|
126
|
+
on_global_method = method(:on_global)
|
|
127
|
+
method_to_bind = proc do |&global_proc|
|
|
128
|
+
on_global_method.call(global_proc)
|
|
129
|
+
end
|
|
130
|
+
@config_context.instance_eval do
|
|
131
|
+
define_singleton_method(:global, method_to_bind)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#: (^() -> void ) -> void
|
|
136
|
+
def on_global(global_config_proc)
|
|
137
|
+
global_config_proc = global_config_proc #: as ^(untyped) -> void
|
|
138
|
+
@global_config.instance_exec(&global_config_proc) if global_config_proc
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
# Errors that can be raised to control workflow execution
|
|
7
|
+
module ControlFlow
|
|
8
|
+
class Base < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Raised in a cog's input block or execute method to terminate the cog and mark it as 'skipped'
|
|
11
|
+
# without triggering any failure handling
|
|
12
|
+
class SkipCog < Base; end
|
|
13
|
+
|
|
14
|
+
# Raised in a cog's input block or execute method to terminate the cog and mark it as 'failed'
|
|
15
|
+
# without terminating the workflow. The workflow may abort anyway if the cog is configured to abort the
|
|
16
|
+
# workflow on failure.
|
|
17
|
+
class FailCog < Base; end
|
|
18
|
+
|
|
19
|
+
# Raised in a cog's input block or execute method to terminate the current loop iteration
|
|
20
|
+
# and start the next iteration immediately. The current cog will be marked as 'skipped',
|
|
21
|
+
# and every subsequent cog in the current iteration will not run. Any async cogs currently running in the
|
|
22
|
+
# current scope will be stopped.
|
|
23
|
+
#
|
|
24
|
+
# If this exception is raised outside of a loop (e.g, via the `call` cog, or in the top-level executor),
|
|
25
|
+
# this exception will just terminate that executor as described above without starting a 'next' iteration.
|
|
26
|
+
class Next < Base; end
|
|
27
|
+
|
|
28
|
+
# Raised in a cog's input block or execute method to terminate the current loop iteration immediately
|
|
29
|
+
# and skip all subsequent loop iterations. The current cog will be marked as 'skipped',
|
|
30
|
+
# and every subsequent cog in the current iteration will not run. Any async cogs currently running in the
|
|
31
|
+
# current scope will be stopped.
|
|
32
|
+
#
|
|
33
|
+
# If this exception is raised outside of a loop (e.g, via the `call` cog, or in the top-level executor),
|
|
34
|
+
# this exception will just terminate that executor as described above without starting a 'next' iteration.
|
|
35
|
+
#
|
|
36
|
+
# If this exception is raised inside a `map`, this exception will prevent any subsequent executor invocations
|
|
37
|
+
# within that map and will stop any async invocations running in parallel.
|
|
38
|
+
class Break < Base; end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|