roast-ai 0.1.7 → 0.2.1

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/CHANGELOG.md +49 -1
  4. data/CLAUDE.md +20 -0
  5. data/CLAUDE_NOTES.md +68 -0
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +9 -6
  8. data/README.md +159 -26
  9. data/bin/roast +27 -0
  10. data/docs/ITERATION_SYNTAX.md +147 -0
  11. data/examples/case_when/README.md +58 -0
  12. data/examples/case_when/detect_language/prompt.md +16 -0
  13. data/examples/case_when/workflow.yml +58 -0
  14. data/examples/conditional/README.md +161 -0
  15. data/examples/conditional/check_condition/prompt.md +1 -0
  16. data/examples/conditional/simple_workflow.yml +15 -0
  17. data/examples/conditional/workflow.yml +23 -0
  18. data/examples/direct_coerce_syntax/README.md +32 -0
  19. data/examples/direct_coerce_syntax/workflow.yml +36 -0
  20. data/examples/dot_notation/README.md +37 -0
  21. data/examples/dot_notation/workflow.yml +44 -0
  22. data/examples/exit_on_error/README.md +50 -0
  23. data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
  24. data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
  25. data/examples/exit_on_error/workflow.yml +19 -0
  26. data/examples/grading/workflow.yml +10 -4
  27. data/examples/iteration/IMPLEMENTATION.md +88 -0
  28. data/examples/iteration/README.md +68 -0
  29. data/examples/iteration/analyze_complexity/prompt.md +22 -0
  30. data/examples/iteration/generate_recommendations/prompt.md +21 -0
  31. data/examples/iteration/generate_report/prompt.md +129 -0
  32. data/examples/iteration/implement_fix/prompt.md +25 -0
  33. data/examples/iteration/prioritize_issues/prompt.md +24 -0
  34. data/examples/iteration/prompts/analyze_file.md +28 -0
  35. data/examples/iteration/prompts/generate_summary.md +24 -0
  36. data/examples/iteration/prompts/update_report.md +29 -0
  37. data/examples/iteration/prompts/write_report.md +22 -0
  38. data/examples/iteration/read_file/prompt.md +9 -0
  39. data/examples/iteration/select_next_issue/prompt.md +25 -0
  40. data/examples/iteration/simple_workflow.md +39 -0
  41. data/examples/iteration/simple_workflow.yml +58 -0
  42. data/examples/iteration/update_fix_count/prompt.md +26 -0
  43. data/examples/iteration/verify_fix/prompt.md +29 -0
  44. data/examples/iteration/workflow.yml +42 -0
  45. data/examples/json_handling/README.md +32 -0
  46. data/examples/json_handling/workflow.yml +52 -0
  47. data/examples/openrouter_example/workflow.yml +2 -2
  48. data/examples/smart_coercion_defaults/README.md +65 -0
  49. data/examples/smart_coercion_defaults/workflow.yml +44 -0
  50. data/examples/step_configuration/README.md +87 -0
  51. data/examples/step_configuration/workflow.yml +60 -0
  52. data/examples/workflow_generator/README.md +27 -0
  53. data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
  54. data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
  55. data/examples/workflow_generator/get_user_input/prompt.md +14 -0
  56. data/examples/workflow_generator/info_from_roast.rb +22 -0
  57. data/examples/workflow_generator/workflow.yml +35 -0
  58. data/lib/roast/errors.rb +9 -0
  59. data/lib/roast/factories/api_provider_factory.rb +61 -0
  60. data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
  61. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  62. data/lib/roast/helpers/prompt_loader.rb +50 -1
  63. data/lib/roast/resources/base_resource.rb +7 -0
  64. data/lib/roast/resources.rb +6 -6
  65. data/lib/roast/tools/ask_user.rb +40 -0
  66. data/lib/roast/tools/cmd.rb +1 -1
  67. data/lib/roast/tools/search_file.rb +1 -1
  68. data/lib/roast/tools.rb +11 -1
  69. data/lib/roast/value_objects/api_token.rb +49 -0
  70. data/lib/roast/value_objects/step_name.rb +39 -0
  71. data/lib/roast/value_objects/workflow_path.rb +77 -0
  72. data/lib/roast/value_objects.rb +5 -0
  73. data/lib/roast/version.rb +1 -1
  74. data/lib/roast/workflow/api_configuration.rb +61 -0
  75. data/lib/roast/workflow/base_iteration_step.rb +184 -0
  76. data/lib/roast/workflow/base_step.rb +44 -27
  77. data/lib/roast/workflow/base_workflow.rb +76 -73
  78. data/lib/roast/workflow/case_executor.rb +49 -0
  79. data/lib/roast/workflow/case_step.rb +82 -0
  80. data/lib/roast/workflow/command_executor.rb +88 -0
  81. data/lib/roast/workflow/conditional_executor.rb +50 -0
  82. data/lib/roast/workflow/conditional_step.rb +59 -0
  83. data/lib/roast/workflow/configuration.rb +35 -158
  84. data/lib/roast/workflow/configuration_loader.rb +78 -0
  85. data/lib/roast/workflow/configuration_parser.rb +13 -248
  86. data/lib/roast/workflow/context_path_resolver.rb +43 -0
  87. data/lib/roast/workflow/dot_access_hash.rb +198 -0
  88. data/lib/roast/workflow/each_step.rb +86 -0
  89. data/lib/roast/workflow/error_handler.rb +97 -0
  90. data/lib/roast/workflow/expression_evaluator.rb +78 -0
  91. data/lib/roast/workflow/expression_utils.rb +36 -0
  92. data/lib/roast/workflow/file_state_repository.rb +3 -2
  93. data/lib/roast/workflow/interpolator.rb +34 -0
  94. data/lib/roast/workflow/iteration_executor.rb +103 -0
  95. data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
  96. data/lib/roast/workflow/output_handler.rb +35 -0
  97. data/lib/roast/workflow/output_manager.rb +77 -0
  98. data/lib/roast/workflow/parallel_executor.rb +49 -0
  99. data/lib/roast/workflow/prompt_step.rb +4 -1
  100. data/lib/roast/workflow/repeat_step.rb +75 -0
  101. data/lib/roast/workflow/replay_handler.rb +123 -0
  102. data/lib/roast/workflow/resource_resolver.rb +77 -0
  103. data/lib/roast/workflow/session_manager.rb +6 -2
  104. data/lib/roast/workflow/state_manager.rb +97 -0
  105. data/lib/roast/workflow/step_executor_coordinator.rb +221 -0
  106. data/lib/roast/workflow/step_executor_factory.rb +47 -0
  107. data/lib/roast/workflow/step_executor_registry.rb +79 -0
  108. data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
  109. data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
  110. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
  111. data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
  112. data/lib/roast/workflow/step_finder.rb +97 -0
  113. data/lib/roast/workflow/step_loader.rb +155 -0
  114. data/lib/roast/workflow/step_orchestrator.rb +45 -0
  115. data/lib/roast/workflow/step_runner.rb +23 -0
  116. data/lib/roast/workflow/step_type_resolver.rb +133 -0
  117. data/lib/roast/workflow/workflow_context.rb +60 -0
  118. data/lib/roast/workflow/workflow_executor.rb +90 -209
  119. data/lib/roast/workflow/workflow_initializer.rb +112 -0
  120. data/lib/roast/workflow/workflow_runner.rb +87 -0
  121. data/lib/roast/workflow.rb +3 -0
  122. data/lib/roast.rb +96 -3
  123. data/roast.gemspec +2 -1
  124. data/schema/workflow.json +112 -0
  125. metadata +112 -4
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/step_executors/base_step_executor"
4
+ require "roast/workflow/step_runner"
5
+
6
+ module Roast
7
+ module Workflow
8
+ module StepExecutors
9
+ class HashStepExecutor < BaseStepExecutor
10
+ def execute(step)
11
+ # execute a command and store the output in a variable
12
+ name, command = step.to_a.flatten
13
+
14
+ # Interpolate variable name if it contains {{}}
15
+ interpolated_name = workflow_executor.interpolate(name)
16
+
17
+ if command.is_a?(Hash)
18
+ step_runner.execute_steps([command])
19
+ else
20
+ # Interpolate command value
21
+ interpolated_command = workflow_executor.interpolate(command)
22
+
23
+ # Check if this step has exit_on_error configuration
24
+ step_config = config_hash[interpolated_name]
25
+ exit_on_error = step_config.is_a?(Hash) ? step_config.fetch("exit_on_error", true) : true
26
+
27
+ workflow.output[interpolated_name] = step_runner.execute_step(interpolated_command, exit_on_error: exit_on_error)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def step_runner
34
+ @step_runner ||= StepRunner.new(coordinator)
35
+ end
36
+
37
+ def coordinator
38
+ workflow_executor.step_executor_coordinator
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/step_executors/base_step_executor"
4
+ require "roast/workflow/step_runner"
5
+
6
+ module Roast
7
+ module Workflow
8
+ module StepExecutors
9
+ class ParallelStepExecutor < BaseStepExecutor
10
+ def execute(steps)
11
+ # run steps in parallel, don't proceed until all are done
12
+ threads = steps.map do |sub_step|
13
+ Thread.new do
14
+ # Each thread needs its own isolated execution context
15
+ Thread.current[:step] = sub_step
16
+ Thread.current[:result] = nil
17
+ Thread.current[:error] = nil
18
+
19
+ begin
20
+ # Execute the single step in this thread
21
+ step_runner.execute_steps([sub_step])
22
+ Thread.current[:result] = :success
23
+ rescue => e
24
+ Thread.current[:error] = e
25
+ end
26
+ end
27
+ end
28
+
29
+ # Wait for all threads to complete
30
+ threads.each(&:join)
31
+
32
+ # Check for errors in any thread
33
+ threads.each_with_index do |thread, _index|
34
+ if thread[:error]
35
+ raise thread[:error]
36
+ end
37
+ end
38
+
39
+ :success
40
+ end
41
+
42
+ private
43
+
44
+ def step_runner
45
+ @step_runner ||= StepRunner.new(coordinator)
46
+ end
47
+
48
+ def coordinator
49
+ workflow_executor.step_executor_coordinator
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/step_executors/base_step_executor"
4
+
5
+ module Roast
6
+ module Workflow
7
+ module StepExecutors
8
+ class StringStepExecutor < BaseStepExecutor
9
+ def execute(step)
10
+ # Interpolate any {{}} expressions before executing the step
11
+ interpolated_step = workflow_executor.interpolate(step)
12
+
13
+ # For command steps, check if there's an exit_on_error configuration
14
+ # We need to extract the step name to look up configuration
15
+ if interpolated_step.starts_with?("$(")
16
+ # This is a direct command without a name, so exit_on_error defaults to true
17
+ workflow_executor.execute_step(interpolated_step)
18
+ else
19
+ # Check if this step has exit_on_error configuration
20
+ step_config = config_hash[step]
21
+ exit_on_error = step_config.is_a?(Hash) ? step_config.fetch("exit_on_error", true) : true
22
+
23
+ workflow_executor.execute_step(interpolated_step, exit_on_error: exit_on_error)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -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,155 @@
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: true)
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.key?("print_response")
148
+ step.auto_loop = step_config["loop"] if step_config.key?("loop")
149
+ step.json = step_config["json"] if step_config.key?("json")
150
+ step.params = step_config["params"] if step_config.key?("params")
151
+ step.coerce_to = step_config["coerce_to"].to_sym if step_config.key?("coerce_to")
152
+ end
153
+ end
154
+ end
155
+ 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,133 @@
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
+ CASE_STEP = :case
13
+ HASH_STEP = :hash
14
+ PARALLEL_STEP = :parallel
15
+ STRING_STEP = :string
16
+ STANDARD_STEP = :standard
17
+
18
+ # Special step names for iterations
19
+ ITERATION_STEPS = ["repeat", "each"].freeze
20
+
21
+ # Special step names for conditionals
22
+ CONDITIONAL_STEPS = ["if", "unless"].freeze
23
+
24
+ # Special step name for case statements
25
+ CASE_STEPS = ["case"].freeze
26
+
27
+ class << self
28
+ # Resolve the type of a step
29
+ # @param step [String, Hash, Array] The step to analyze
30
+ # @param context [WorkflowContext] The workflow context
31
+ # @return [Symbol] The step type
32
+ def resolve(step, context = nil)
33
+ case step
34
+ when String
35
+ resolve_string_step(step, context)
36
+ when Hash
37
+ resolve_hash_step(step)
38
+ when Array
39
+ PARALLEL_STEP
40
+ else
41
+ STANDARD_STEP
42
+ end
43
+ end
44
+
45
+ # Check if a step is a command step
46
+ # @param step [String] The step to check
47
+ # @return [Boolean] true if it's a command step
48
+ def command_step?(step)
49
+ step.is_a?(String) && step.start_with?("$(")
50
+ end
51
+
52
+ # Check if a step is a glob pattern
53
+ # @param step [String] The step to check
54
+ # @param context [WorkflowContext, nil] The workflow context
55
+ # @return [Boolean] true if it's a glob pattern
56
+ def glob_step?(step, context = nil)
57
+ return false unless step.is_a?(String) && step.include?("*")
58
+
59
+ # Only treat as glob if we don't have a resource
60
+ context.nil? || !context.has_resource?
61
+ end
62
+
63
+ # Check if a step is an iteration step
64
+ # @param step [Hash] The step to check
65
+ # @return [Boolean] true if it's an iteration step
66
+ def iteration_step?(step)
67
+ return false unless step.is_a?(Hash)
68
+
69
+ step_name = step.keys.first
70
+ ITERATION_STEPS.include?(step_name)
71
+ end
72
+
73
+ # Check if a step is a conditional step
74
+ # @param step [Hash] The step to check
75
+ # @return [Boolean] true if it's a conditional step
76
+ def conditional_step?(step)
77
+ return false unless step.is_a?(Hash)
78
+
79
+ step_name = step.keys.first
80
+ CONDITIONAL_STEPS.include?(step_name)
81
+ end
82
+
83
+ # Check if a step is a case step
84
+ # @param step [Hash] The step to check
85
+ # @return [Boolean] true if it's a case step
86
+ def case_step?(step)
87
+ return false unless step.is_a?(Hash)
88
+
89
+ step_name = step.keys.first
90
+ CASE_STEPS.include?(step_name)
91
+ end
92
+
93
+ # Extract the step name from various step formats
94
+ # @param step [String, Hash, Array] The step
95
+ # @return [String, nil] The step name or nil
96
+ def extract_name(step)
97
+ case step
98
+ when String
99
+ step
100
+ when Hash
101
+ step.keys.first
102
+ when Array
103
+ nil # Parallel steps don't have a single name
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def resolve_string_step(step, context)
110
+ if command_step?(step)
111
+ COMMAND_STEP
112
+ elsif glob_step?(step, context)
113
+ GLOB_STEP
114
+ else
115
+ STRING_STEP
116
+ end
117
+ end
118
+
119
+ def resolve_hash_step(step)
120
+ if iteration_step?(step)
121
+ ITERATION_STEP
122
+ elsif conditional_step?(step)
123
+ CONDITIONAL_STEP
124
+ elsif case_step?(step)
125
+ CASE_STEP
126
+ else
127
+ HASH_STEP
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ 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