roast-ai 0.1.7 → 0.2.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/.github/workflows/ci.yaml +1 -1
- data/CHANGELOG.md +40 -1
- data/CLAUDE.md +20 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +9 -6
- data/README.md +81 -14
- data/bin/roast +27 -0
- data/docs/ITERATION_SYNTAX.md +119 -0
- data/examples/conditional/README.md +161 -0
- data/examples/conditional/check_condition/prompt.md +1 -0
- data/examples/conditional/simple_workflow.yml +15 -0
- data/examples/conditional/workflow.yml +23 -0
- data/examples/dot_notation/README.md +37 -0
- data/examples/dot_notation/workflow.yml +44 -0
- data/examples/exit_on_error/README.md +50 -0
- data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
- data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
- data/examples/exit_on_error/workflow.yml +19 -0
- data/examples/grading/workflow.yml +5 -1
- data/examples/iteration/IMPLEMENTATION.md +88 -0
- data/examples/iteration/README.md +68 -0
- data/examples/iteration/analyze_complexity/prompt.md +22 -0
- data/examples/iteration/generate_recommendations/prompt.md +21 -0
- data/examples/iteration/generate_report/prompt.md +129 -0
- data/examples/iteration/implement_fix/prompt.md +25 -0
- data/examples/iteration/prioritize_issues/prompt.md +24 -0
- data/examples/iteration/prompts/analyze_file.md +28 -0
- data/examples/iteration/prompts/generate_summary.md +24 -0
- data/examples/iteration/prompts/update_report.md +29 -0
- data/examples/iteration/prompts/write_report.md +22 -0
- data/examples/iteration/read_file/prompt.md +9 -0
- data/examples/iteration/select_next_issue/prompt.md +25 -0
- data/examples/iteration/simple_workflow.md +39 -0
- data/examples/iteration/simple_workflow.yml +58 -0
- data/examples/iteration/update_fix_count/prompt.md +26 -0
- data/examples/iteration/verify_fix/prompt.md +29 -0
- data/examples/iteration/workflow.yml +42 -0
- data/examples/openrouter_example/workflow.yml +2 -2
- data/examples/workflow_generator/README.md +27 -0
- data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
- data/examples/workflow_generator/get_user_input/prompt.md +14 -0
- data/examples/workflow_generator/info_from_roast.rb +22 -0
- data/examples/workflow_generator/workflow.yml +35 -0
- data/lib/roast/errors.rb +9 -0
- data/lib/roast/factories/api_provider_factory.rb +61 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
- data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
- data/lib/roast/helpers/prompt_loader.rb +50 -1
- data/lib/roast/resources/base_resource.rb +7 -0
- data/lib/roast/resources.rb +6 -6
- data/lib/roast/tools/ask_user.rb +40 -0
- data/lib/roast/tools/cmd.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools.rb +11 -1
- data/lib/roast/value_objects/api_token.rb +49 -0
- data/lib/roast/value_objects/step_name.rb +39 -0
- data/lib/roast/value_objects/workflow_path.rb +77 -0
- data/lib/roast/value_objects.rb +5 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/api_configuration.rb +61 -0
- data/lib/roast/workflow/base_iteration_step.rb +165 -0
- data/lib/roast/workflow/base_step.rb +4 -24
- data/lib/roast/workflow/base_workflow.rb +76 -73
- data/lib/roast/workflow/command_executor.rb +88 -0
- data/lib/roast/workflow/conditional_executor.rb +50 -0
- data/lib/roast/workflow/conditional_step.rb +96 -0
- data/lib/roast/workflow/configuration.rb +35 -158
- data/lib/roast/workflow/configuration_loader.rb +78 -0
- data/lib/roast/workflow/configuration_parser.rb +13 -248
- data/lib/roast/workflow/context_path_resolver.rb +43 -0
- data/lib/roast/workflow/dot_access_hash.rb +198 -0
- data/lib/roast/workflow/each_step.rb +86 -0
- data/lib/roast/workflow/error_handler.rb +97 -0
- data/lib/roast/workflow/expression_utils.rb +36 -0
- data/lib/roast/workflow/file_state_repository.rb +3 -2
- data/lib/roast/workflow/interpolator.rb +34 -0
- data/lib/roast/workflow/iteration_executor.rb +85 -0
- data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
- data/lib/roast/workflow/output_handler.rb +35 -0
- data/lib/roast/workflow/output_manager.rb +77 -0
- data/lib/roast/workflow/parallel_executor.rb +49 -0
- data/lib/roast/workflow/repeat_step.rb +75 -0
- data/lib/roast/workflow/replay_handler.rb +123 -0
- data/lib/roast/workflow/resource_resolver.rb +77 -0
- data/lib/roast/workflow/session_manager.rb +6 -2
- data/lib/roast/workflow/state_manager.rb +97 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +205 -0
- data/lib/roast/workflow/step_executor_factory.rb +47 -0
- data/lib/roast/workflow/step_executor_registry.rb +79 -0
- data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
- data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
- data/lib/roast/workflow/step_finder.rb +97 -0
- data/lib/roast/workflow/step_loader.rb +154 -0
- data/lib/roast/workflow/step_orchestrator.rb +45 -0
- data/lib/roast/workflow/step_runner.rb +23 -0
- data/lib/roast/workflow/step_type_resolver.rb +117 -0
- data/lib/roast/workflow/workflow_context.rb +60 -0
- data/lib/roast/workflow/workflow_executor.rb +90 -209
- data/lib/roast/workflow/workflow_initializer.rb +112 -0
- data/lib/roast/workflow/workflow_runner.rb +87 -0
- data/lib/roast/workflow.rb +3 -0
- data/lib/roast.rb +96 -3
- data/roast.gemspec +2 -1
- data/schema/workflow.json +85 -0
- metadata +97 -4
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Finds step indices within workflow step arrays
|
6
|
+
# Handles various step formats: strings, hashes, parallel arrays
|
7
|
+
class StepFinder
|
8
|
+
attr_reader :steps
|
9
|
+
|
10
|
+
def initialize(steps = nil)
|
11
|
+
@steps = steps || []
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def find_index(steps_array, step_name)
|
16
|
+
new(steps_array).find_index(step_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Find the index of a step in the workflow steps array
|
21
|
+
# @param step_name [String] The name of the step to find
|
22
|
+
# @param steps_array [Array, nil] Optional steps array to search in
|
23
|
+
# @return [Integer, nil] The index of the step, or nil if not found
|
24
|
+
def find_index(step_name, steps_array = nil)
|
25
|
+
search_array = steps_array || @steps
|
26
|
+
# First, try direct search
|
27
|
+
index = find_by_direct_search(search_array, step_name)
|
28
|
+
return index if index
|
29
|
+
|
30
|
+
# Fall back to extracted name search
|
31
|
+
find_by_extracted_name(search_array, step_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Extract the name from a step definition
|
35
|
+
# @param step [String, Hash, Array] The step definition
|
36
|
+
# @return [String, Array] The step name(s)
|
37
|
+
def extract_name(step)
|
38
|
+
case step
|
39
|
+
when String
|
40
|
+
step
|
41
|
+
when Hash
|
42
|
+
step.keys.first
|
43
|
+
when Array
|
44
|
+
# For arrays, extract names from all contained steps
|
45
|
+
step.map { |s| extract_name(s) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def find_by_direct_search(steps_array, step_name)
|
52
|
+
steps_array.each_with_index do |step, index|
|
53
|
+
case step
|
54
|
+
when Hash
|
55
|
+
# Could be {name: command} or {name: {substeps}}
|
56
|
+
step_key = step.keys.first
|
57
|
+
return index if step_key == step_name
|
58
|
+
when Array
|
59
|
+
# This is a parallel step container, search inside it
|
60
|
+
if contains_step?(step, step_name)
|
61
|
+
return index
|
62
|
+
end
|
63
|
+
when String
|
64
|
+
return index if step == step_name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_by_extracted_name(steps_array, step_name)
|
71
|
+
steps_array.each_with_index do |step, index|
|
72
|
+
name = extract_name(step)
|
73
|
+
if name.is_a?(Array)
|
74
|
+
# For arrays (parallel steps), check if target is in the array
|
75
|
+
return index if name.flatten.include?(step_name)
|
76
|
+
elsif name == step_name
|
77
|
+
return index
|
78
|
+
end
|
79
|
+
end
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def contains_step?(parallel_steps, step_name)
|
84
|
+
parallel_steps.any? do |substep|
|
85
|
+
case substep
|
86
|
+
when Hash
|
87
|
+
substep.keys.first == step_name
|
88
|
+
when String
|
89
|
+
substep == step_name
|
90
|
+
else
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/value_objects/step_name"
|
4
|
+
require "roast/workflow/workflow_context"
|
5
|
+
require "roast/workflow/base_step"
|
6
|
+
require "roast/workflow/prompt_step"
|
7
|
+
|
8
|
+
module Roast
|
9
|
+
module Workflow
|
10
|
+
# Handles loading and instantiation of workflow steps
|
11
|
+
class StepLoader
|
12
|
+
DEFAULT_MODEL = "openai/gpt-4o-mini"
|
13
|
+
|
14
|
+
# Custom exception classes
|
15
|
+
class StepLoaderError < StandardError
|
16
|
+
attr_reader :step_name, :original_error
|
17
|
+
|
18
|
+
def initialize(message, step_name: nil, original_error: nil)
|
19
|
+
@step_name = step_name
|
20
|
+
@original_error = original_error
|
21
|
+
super(message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class StepNotFoundError < StepLoaderError; end
|
26
|
+
class StepExecutionError < StepLoaderError; end
|
27
|
+
|
28
|
+
attr_reader :context
|
29
|
+
|
30
|
+
delegate :workflow, :config_hash, :context_path, to: :context
|
31
|
+
|
32
|
+
def initialize(workflow, config_hash, context_path)
|
33
|
+
# Support both old and new initialization patterns
|
34
|
+
@context = if workflow.is_a?(WorkflowContext)
|
35
|
+
workflow
|
36
|
+
else
|
37
|
+
WorkflowContext.new(
|
38
|
+
workflow: workflow,
|
39
|
+
config_hash: config_hash,
|
40
|
+
context_path: context_path,
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Finds and loads a step by name
|
46
|
+
#
|
47
|
+
# @param step_name [String, StepName] The name of the step to load
|
48
|
+
# @return [BaseStep] The loaded step instance
|
49
|
+
def load(step_name)
|
50
|
+
name = step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
|
51
|
+
|
52
|
+
# First check for a prompt step (contains spaces)
|
53
|
+
if name.plain_text?
|
54
|
+
step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s, auto_loop: false)
|
55
|
+
configure_step(step, name.to_s)
|
56
|
+
return step
|
57
|
+
end
|
58
|
+
|
59
|
+
# Look for Ruby file in various locations
|
60
|
+
step_file_path = find_step_file(name.to_s)
|
61
|
+
if step_file_path
|
62
|
+
return load_ruby_step(step_file_path, name.to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Look for step directory
|
66
|
+
step_directory = find_step_directory(name.to_s)
|
67
|
+
unless step_directory
|
68
|
+
raise StepNotFoundError.new("Step directory or file not found: #{name}", step_name: name.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
create_step_instance(Roast::Workflow::BaseStep, name.to_s, step_directory)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Find a Ruby step file in various locations
|
77
|
+
def find_step_file(step_name)
|
78
|
+
# Check in context path
|
79
|
+
rb_file_path = File.join(context_path, "#{step_name}.rb")
|
80
|
+
return rb_file_path if File.file?(rb_file_path)
|
81
|
+
|
82
|
+
# Check in shared directory
|
83
|
+
shared_rb_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.rb"))
|
84
|
+
return shared_rb_path if File.file?(shared_rb_path)
|
85
|
+
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find a step directory
|
90
|
+
def find_step_directory(step_name)
|
91
|
+
# Check in context path
|
92
|
+
step_path = File.join(context_path, step_name)
|
93
|
+
return step_path if File.directory?(step_path)
|
94
|
+
|
95
|
+
# Check in shared directory
|
96
|
+
shared_path = File.expand_path(File.join(context_path, "..", "shared", step_name))
|
97
|
+
return shared_path if File.directory?(shared_path)
|
98
|
+
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
# Load a Ruby step from a file
|
103
|
+
def load_ruby_step(file_path, step_name)
|
104
|
+
$stderr.puts "Requiring step file: #{file_path}"
|
105
|
+
|
106
|
+
begin
|
107
|
+
require file_path
|
108
|
+
rescue LoadError => e
|
109
|
+
raise StepNotFoundError.new("Failed to load step file: #{e.message}", step_name: step_name, original_error: e)
|
110
|
+
rescue SyntaxError => e
|
111
|
+
raise StepExecutionError.new("Syntax error in step file: #{e.message}", step_name: step_name, original_error: e)
|
112
|
+
end
|
113
|
+
|
114
|
+
step_class = step_name.classify.constantize
|
115
|
+
context = File.dirname(file_path)
|
116
|
+
create_step_instance(step_class, step_name, context)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Create and configure a step instance
|
120
|
+
def create_step_instance(step_class, step_name, context_path)
|
121
|
+
step = step_class.new(workflow, name: step_name, context_path: context_path)
|
122
|
+
configure_step(step, step_name)
|
123
|
+
step
|
124
|
+
end
|
125
|
+
|
126
|
+
# Configure a step instance with settings from config_hash
|
127
|
+
def configure_step(step, step_name)
|
128
|
+
step_config = config_hash[step_name]
|
129
|
+
|
130
|
+
# Always set the model
|
131
|
+
step.model = determine_model(step_config)
|
132
|
+
|
133
|
+
# Pass resource to step if supported
|
134
|
+
step.resource = workflow.resource if step.respond_to?(:resource=)
|
135
|
+
|
136
|
+
# Apply additional configuration if present
|
137
|
+
apply_step_configuration(step, step_config) if step_config.present?
|
138
|
+
end
|
139
|
+
|
140
|
+
# Determine which model to use for the step
|
141
|
+
def determine_model(step_config)
|
142
|
+
step_config&.dig("model") || config_hash["model"] || DEFAULT_MODEL
|
143
|
+
end
|
144
|
+
|
145
|
+
# Apply configuration settings to a step
|
146
|
+
def apply_step_configuration(step, step_config)
|
147
|
+
step.print_response = step_config["print_response"] if step_config["print_response"].present?
|
148
|
+
step.auto_loop = step_config["loop"] if step_config["loop"].present?
|
149
|
+
step.json = step_config["json"] if step_config["json"].present?
|
150
|
+
step.params = step_config["params"] if step_config["params"].present?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Handles the orchestration of step execution, managing the flow and control
|
6
|
+
# of individual steps without knowing how to execute them
|
7
|
+
#
|
8
|
+
# This class is specifically for executing CUSTOM steps defined in the workflow's
|
9
|
+
# step directory (e.g., steps/*.rb files). It loads and executes Ruby step files
|
10
|
+
# that define a `call` method.
|
11
|
+
#
|
12
|
+
# The primary method execute_step is used by StepExecutorCoordinator for
|
13
|
+
# executing custom Ruby steps.
|
14
|
+
#
|
15
|
+
# TODO: Consider renaming this class to CustomStepOrchestrator to clarify its purpose
|
16
|
+
class StepOrchestrator
|
17
|
+
def initialize(workflow, step_loader, state_manager, error_handler, workflow_executor)
|
18
|
+
@workflow = workflow
|
19
|
+
@step_loader = step_loader
|
20
|
+
@state_manager = state_manager
|
21
|
+
@error_handler = error_handler
|
22
|
+
@workflow_executor = workflow_executor
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_step(name, exit_on_error: true)
|
26
|
+
resource_type = @workflow.respond_to?(:resource) ? @workflow.resource&.type : nil
|
27
|
+
|
28
|
+
@error_handler.with_error_handling(name, resource_type: resource_type) do
|
29
|
+
$stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
|
30
|
+
|
31
|
+
step_object = @step_loader.load(name)
|
32
|
+
step_result = step_object.call
|
33
|
+
|
34
|
+
# Store result in workflow output
|
35
|
+
@workflow.output[name] = step_result
|
36
|
+
|
37
|
+
# Save state after each step
|
38
|
+
@state_manager.save_state(name, step_result)
|
39
|
+
|
40
|
+
step_result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Interface for running workflow steps.
|
6
|
+
# This abstraction breaks the circular dependency between executors and the workflow.
|
7
|
+
class StepRunner
|
8
|
+
def initialize(coordinator)
|
9
|
+
@coordinator = coordinator
|
10
|
+
end
|
11
|
+
|
12
|
+
# Execute a list of steps
|
13
|
+
def execute_steps(steps)
|
14
|
+
@coordinator.execute_steps(steps)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Execute a single step
|
18
|
+
def execute_step(step, options = {})
|
19
|
+
@coordinator.execute_step(step, options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Determines the type of a step and how it should be executed
|
6
|
+
class StepTypeResolver
|
7
|
+
# Step type constants
|
8
|
+
COMMAND_STEP = :command
|
9
|
+
GLOB_STEP = :glob
|
10
|
+
ITERATION_STEP = :iteration
|
11
|
+
CONDITIONAL_STEP = :conditional
|
12
|
+
HASH_STEP = :hash
|
13
|
+
PARALLEL_STEP = :parallel
|
14
|
+
STRING_STEP = :string
|
15
|
+
STANDARD_STEP = :standard
|
16
|
+
|
17
|
+
# Special step names for iterations
|
18
|
+
ITERATION_STEPS = ["repeat", "each"].freeze
|
19
|
+
|
20
|
+
# Special step names for conditionals
|
21
|
+
CONDITIONAL_STEPS = ["if", "unless"].freeze
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Resolve the type of a step
|
25
|
+
# @param step [String, Hash, Array] The step to analyze
|
26
|
+
# @param context [WorkflowContext] The workflow context
|
27
|
+
# @return [Symbol] The step type
|
28
|
+
def resolve(step, context = nil)
|
29
|
+
case step
|
30
|
+
when String
|
31
|
+
resolve_string_step(step, context)
|
32
|
+
when Hash
|
33
|
+
resolve_hash_step(step)
|
34
|
+
when Array
|
35
|
+
PARALLEL_STEP
|
36
|
+
else
|
37
|
+
STANDARD_STEP
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check if a step is a command step
|
42
|
+
# @param step [String] The step to check
|
43
|
+
# @return [Boolean] true if it's a command step
|
44
|
+
def command_step?(step)
|
45
|
+
step.is_a?(String) && step.start_with?("$(")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Check if a step is a glob pattern
|
49
|
+
# @param step [String] The step to check
|
50
|
+
# @param context [WorkflowContext, nil] The workflow context
|
51
|
+
# @return [Boolean] true if it's a glob pattern
|
52
|
+
def glob_step?(step, context = nil)
|
53
|
+
return false unless step.is_a?(String) && step.include?("*")
|
54
|
+
|
55
|
+
# Only treat as glob if we don't have a resource
|
56
|
+
context.nil? || !context.has_resource?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check if a step is an iteration step
|
60
|
+
# @param step [Hash] The step to check
|
61
|
+
# @return [Boolean] true if it's an iteration step
|
62
|
+
def iteration_step?(step)
|
63
|
+
return false unless step.is_a?(Hash)
|
64
|
+
|
65
|
+
step_name = step.keys.first
|
66
|
+
ITERATION_STEPS.include?(step_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if a step is a conditional step
|
70
|
+
# @param step [Hash] The step to check
|
71
|
+
# @return [Boolean] true if it's a conditional step
|
72
|
+
def conditional_step?(step)
|
73
|
+
return false unless step.is_a?(Hash)
|
74
|
+
|
75
|
+
step_name = step.keys.first
|
76
|
+
CONDITIONAL_STEPS.include?(step_name)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Extract the step name from various step formats
|
80
|
+
# @param step [String, Hash, Array] The step
|
81
|
+
# @return [String, nil] The step name or nil
|
82
|
+
def extract_name(step)
|
83
|
+
case step
|
84
|
+
when String
|
85
|
+
step
|
86
|
+
when Hash
|
87
|
+
step.keys.first
|
88
|
+
when Array
|
89
|
+
nil # Parallel steps don't have a single name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def resolve_string_step(step, context)
|
96
|
+
if command_step?(step)
|
97
|
+
COMMAND_STEP
|
98
|
+
elsif glob_step?(step, context)
|
99
|
+
GLOB_STEP
|
100
|
+
else
|
101
|
+
STRING_STEP
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def resolve_hash_step(step)
|
106
|
+
if iteration_step?(step)
|
107
|
+
ITERATION_STEP
|
108
|
+
elsif conditional_step?(step)
|
109
|
+
CONDITIONAL_STEP
|
110
|
+
else
|
111
|
+
HASH_STEP
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Encapsulates common workflow execution context parameters
|
6
|
+
# Reduces data clump anti-pattern by grouping related parameters
|
7
|
+
class WorkflowContext
|
8
|
+
attr_reader :workflow, :config_hash, :context_path
|
9
|
+
|
10
|
+
# Initialize the workflow context
|
11
|
+
# @param workflow [BaseWorkflow] The workflow instance
|
12
|
+
# @param config_hash [Hash] The workflow configuration hash
|
13
|
+
# @param context_path [String] The context directory path
|
14
|
+
def initialize(workflow:, config_hash:, context_path:)
|
15
|
+
@workflow = workflow
|
16
|
+
@config_hash = config_hash
|
17
|
+
@context_path = context_path
|
18
|
+
freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create a new context with updated workflow
|
22
|
+
# @param new_workflow [BaseWorkflow] The new workflow instance
|
23
|
+
# @return [WorkflowContext] A new context with the updated workflow
|
24
|
+
def with_workflow(new_workflow)
|
25
|
+
self.class.new(
|
26
|
+
workflow: new_workflow,
|
27
|
+
config_hash: config_hash,
|
28
|
+
context_path: context_path,
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if the workflow has a resource
|
33
|
+
# @return [Boolean] true if workflow responds to resource and has one
|
34
|
+
def has_resource?
|
35
|
+
workflow.respond_to?(:resource) && workflow.resource
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get the resource type from the workflow
|
39
|
+
# @return [Symbol, nil] The resource type or nil
|
40
|
+
def resource_type
|
41
|
+
has_resource? ? workflow.resource.type : nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get configuration for a specific step
|
45
|
+
# @param step_name [String] The name of the step
|
46
|
+
# @return [Hash] The step configuration or empty hash
|
47
|
+
def step_config(step_name)
|
48
|
+
config_hash[step_name] || {}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check if a step should exit on error
|
52
|
+
# @param step_name [String] The name of the step
|
53
|
+
# @return [Boolean] true if the step should exit on error (default true)
|
54
|
+
def exit_on_error?(step_name)
|
55
|
+
config = step_config(step_name)
|
56
|
+
config.is_a?(Hash) ? config.fetch("exit_on_error", true) : true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|