roast-ai 0.4.0 → 0.4.2
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 +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +103 -0
- data/CLAUDE.md +55 -9
- data/Gemfile.lock +19 -10
- data/README.md +69 -3
- data/bin/console +1 -0
- data/docs/AGENT_STEPS.md +33 -9
- data/docs/VALIDATION.md +178 -0
- data/examples/agent_continue/add_documentation/prompt.md +5 -0
- data/examples/agent_continue/add_error_handling/prompt.md +5 -0
- data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
- data/examples/agent_continue/combined_workflow.yml +24 -0
- data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
- data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
- data/examples/agent_continue/document_with_context/prompt.md +5 -0
- data/examples/agent_continue/explore_api/prompt.md +6 -0
- data/examples/agent_continue/implement_client/prompt.md +6 -0
- data/examples/agent_continue/inline_workflow.yml +20 -0
- data/examples/agent_continue/refactor_code/prompt.md +2 -0
- data/examples/agent_continue/verify_changes/prompt.md +6 -0
- data/examples/agent_continue/workflow.yml +27 -0
- data/examples/agent_workflow/workflow.png +0 -0
- data/examples/api_workflow/workflow.png +0 -0
- data/examples/apply_diff_demo/README.md +58 -0
- data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
- data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
- data/examples/apply_diff_demo/workflow.yml +24 -0
- data/examples/available_tools_demo/workflow.png +0 -0
- data/examples/bash_prototyping/api_testing.png +0 -0
- data/examples/bash_prototyping/system_analysis.png +0 -0
- data/examples/case_when/workflow.png +0 -0
- data/examples/cmd/basic_workflow.png +0 -0
- data/examples/cmd/dev_workflow.png +0 -0
- data/examples/cmd/explorer_workflow.png +0 -0
- data/examples/conditional/simple_workflow.png +0 -0
- data/examples/conditional/workflow.png +0 -0
- data/examples/context_management_demo/README.md +43 -0
- data/examples/context_management_demo/workflow.yml +42 -0
- data/examples/direct_coerce_syntax/workflow.png +0 -0
- data/examples/dot_notation/workflow.png +0 -0
- data/examples/exit_on_error/workflow.png +0 -0
- data/examples/grading/rb_test_runner +1 -1
- data/examples/grading/workflow.png +0 -0
- data/examples/interpolation/workflow.png +0 -0
- data/examples/interpolation/workflow.yml +1 -1
- data/examples/iteration/workflow.png +0 -0
- data/examples/json_handling/workflow.png +0 -0
- data/examples/mcp/database_workflow.png +0 -0
- data/examples/mcp/env_demo/workflow.png +0 -0
- data/examples/mcp/filesystem_demo/workflow.png +0 -0
- data/examples/mcp/github_workflow.png +0 -0
- data/examples/mcp/multi_mcp_workflow.png +0 -0
- data/examples/mcp/workflow.png +0 -0
- data/examples/no_model_fallback/README.md +17 -0
- data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
- data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
- data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
- data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
- data/examples/no_model_fallback/sample.rb +42 -0
- data/examples/no_model_fallback/workflow.yml +19 -0
- data/examples/openrouter_example/workflow.png +0 -0
- data/examples/pre_post_processing/workflow.png +0 -0
- data/examples/rspec_to_minitest/workflow.png +0 -0
- data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
- data/examples/shared_config/shared.png +0 -0
- data/examples/single_target_prepost/workflow.png +0 -0
- data/examples/smart_coercion_defaults/workflow.png +0 -0
- data/examples/step_configuration/workflow.png +0 -0
- data/examples/swarm_example.yml +25 -0
- data/examples/tool_config_example/workflow.png +0 -0
- data/examples/user_input/funny_name/workflow.png +0 -0
- data/examples/user_input/simple_input_demo/workflow.png +0 -0
- data/examples/user_input/survey_workflow.png +0 -0
- data/examples/user_input/workflow.png +0 -0
- data/examples/workflow_generator/workflow.png +0 -0
- data/lib/roast/errors.rb +3 -0
- data/lib/roast/helpers/timeout_handler.rb +91 -0
- data/lib/roast/services/context_threshold_checker.rb +42 -0
- data/lib/roast/services/token_counting_service.rb +44 -0
- data/lib/roast/tools/apply_diff.rb +128 -0
- data/lib/roast/tools/bash.rb +15 -9
- data/lib/roast/tools/cmd.rb +32 -12
- data/lib/roast/tools/coding_agent.rb +65 -10
- data/lib/roast/tools/context_summarizer.rb +108 -0
- data/lib/roast/tools/swarm.rb +124 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +9 -2
- data/lib/roast/workflow/base_iteration_step.rb +3 -2
- data/lib/roast/workflow/base_workflow.rb +41 -2
- data/lib/roast/workflow/command_executor.rb +3 -1
- data/lib/roast/workflow/configuration.rb +2 -1
- data/lib/roast/workflow/configuration_loader.rb +63 -1
- data/lib/roast/workflow/configuration_parser.rb +2 -0
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/each_step.rb +1 -1
- data/lib/roast/workflow/input_step.rb +2 -0
- data/lib/roast/workflow/interpolator.rb +23 -1
- data/lib/roast/workflow/output_handler.rb +1 -1
- data/lib/roast/workflow/repeat_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +1 -1
- data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
- data/lib/roast/workflow/state_manager.rb +2 -2
- data/lib/roast/workflow/state_repository_factory.rb +36 -0
- data/lib/roast/workflow/step_completion_reporter.rb +27 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
- data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
- data/lib/roast/workflow/step_loader.rb +1 -1
- data/lib/roast/workflow/step_name_extractor.rb +84 -0
- data/lib/roast/workflow/validation_command.rb +197 -0
- data/lib/roast/workflow/validators/base_validator.rb +44 -0
- data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
- data/lib/roast/workflow/validators/linting_validator.rb +113 -0
- data/lib/roast/workflow/validators/schema_validator.rb +90 -0
- data/lib/roast/workflow/validators/step_collector.rb +57 -0
- data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
- data/lib/roast/workflow/workflow_executor.rb +11 -4
- data/lib/roast/workflow/workflow_initializer.rb +80 -0
- data/lib/roast/workflow/workflow_runner.rb +6 -0
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +158 -0
- data/roast.gemspec +4 -1
- data/schema/workflow.json +77 -1
- metadata +129 -1
@@ -28,11 +28,11 @@ module Roast
|
|
28
28
|
is_last_step = (index == workflow_steps.length - 1)
|
29
29
|
case step
|
30
30
|
when Hash
|
31
|
-
execute(step, is_last_step:
|
31
|
+
execute(step, is_last_step:)
|
32
32
|
when Array
|
33
|
-
execute(step, is_last_step:
|
33
|
+
execute(step, is_last_step:)
|
34
34
|
when String
|
35
|
-
execute(step, is_last_step:
|
35
|
+
execute(step, is_last_step:)
|
36
36
|
# Handle pause after string steps
|
37
37
|
if @context.workflow.pause_step_name == step
|
38
38
|
Kernel.binding.irb # rubocop:disable Lint/Debugger
|
@@ -62,17 +62,17 @@ module Roast
|
|
62
62
|
when StepTypeResolver::AGENT_STEP
|
63
63
|
execute_agent_step(step, options)
|
64
64
|
when StepTypeResolver::GLOB_STEP
|
65
|
-
execute_glob_step(step)
|
65
|
+
execute_glob_step(step, options)
|
66
66
|
when StepTypeResolver::ITERATION_STEP
|
67
|
-
execute_iteration_step(step)
|
67
|
+
execute_iteration_step(step, options)
|
68
68
|
when StepTypeResolver::CONDITIONAL_STEP
|
69
|
-
execute_conditional_step(step)
|
69
|
+
execute_conditional_step(step, options)
|
70
70
|
when StepTypeResolver::CASE_STEP
|
71
|
-
execute_case_step(step)
|
71
|
+
execute_case_step(step, options)
|
72
72
|
when StepTypeResolver::INPUT_STEP
|
73
|
-
execute_input_step(step)
|
73
|
+
execute_input_step(step, options)
|
74
74
|
when StepTypeResolver::HASH_STEP
|
75
|
-
execute_hash_step(step)
|
75
|
+
execute_hash_step(step, options)
|
76
76
|
when StepTypeResolver::PARALLEL_STEP
|
77
77
|
# Use factory for parallel steps
|
78
78
|
executor = StepExecutorFactory.for(step, workflow_executor)
|
@@ -189,11 +189,11 @@ module Roast
|
|
189
189
|
step_orchestrator.execute_step(step_name, exit_on_error:, step_key: options[:step_key], agent_type: :coding_agent)
|
190
190
|
end
|
191
191
|
|
192
|
-
def execute_glob_step(step)
|
192
|
+
def execute_glob_step(step, options = {})
|
193
193
|
Dir.glob(step).join("\n")
|
194
194
|
end
|
195
195
|
|
196
|
-
def execute_iteration_step(step)
|
196
|
+
def execute_iteration_step(step, options = {})
|
197
197
|
name = step.keys.first
|
198
198
|
command = step[name]
|
199
199
|
|
@@ -206,19 +206,19 @@ module Roast
|
|
206
206
|
end
|
207
207
|
end
|
208
208
|
|
209
|
-
def execute_conditional_step(step)
|
209
|
+
def execute_conditional_step(step, options = {})
|
210
210
|
conditional_executor.execute_conditional(step)
|
211
211
|
end
|
212
212
|
|
213
|
-
def execute_case_step(step)
|
213
|
+
def execute_case_step(step, options = {})
|
214
214
|
case_executor.execute_case(step)
|
215
215
|
end
|
216
216
|
|
217
|
-
def execute_input_step(step)
|
217
|
+
def execute_input_step(step, options = {})
|
218
218
|
input_executor.execute_input(step["input"])
|
219
219
|
end
|
220
220
|
|
221
|
-
def execute_hash_step(step)
|
221
|
+
def execute_hash_step(step, options = {})
|
222
222
|
name, command = step.to_a.flatten
|
223
223
|
interpolated_name = interpolator.interpolate(name)
|
224
224
|
|
@@ -230,7 +230,8 @@ module Roast
|
|
230
230
|
|
231
231
|
# Execute the command directly using the appropriate executor
|
232
232
|
# Pass the original key name for configuration lookup
|
233
|
-
|
233
|
+
# Merge options to preserve is_last_step
|
234
|
+
result = execute(interpolated_command, { exit_on_error:, step_key: interpolated_name }.merge(options))
|
234
235
|
context.workflow.output[interpolated_name] = result
|
235
236
|
result
|
236
237
|
end
|
@@ -247,10 +248,10 @@ module Roast
|
|
247
248
|
if StepTypeResolver.command_step?(interpolated_step)
|
248
249
|
# Command step - execute directly, preserving any passed options
|
249
250
|
exit_on_error = options.fetch(:exit_on_error, true)
|
250
|
-
execute_command_step(interpolated_step, { exit_on_error:
|
251
|
+
execute_command_step(interpolated_step, { exit_on_error: })
|
251
252
|
else
|
252
253
|
exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
|
253
|
-
execute_standard_step(interpolated_step, options.merge(exit_on_error:
|
254
|
+
execute_standard_step(interpolated_step, options.merge(exit_on_error:))
|
254
255
|
end
|
255
256
|
end
|
256
257
|
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Decorator that adds token consumption reporting to step execution
|
6
|
+
class StepExecutorWithReporting
|
7
|
+
def initialize(base_executor, context, output: $stderr)
|
8
|
+
@base_executor = base_executor
|
9
|
+
@context = context
|
10
|
+
@reporter = StepCompletionReporter.new(output: output)
|
11
|
+
@name_extractor = StepNameExtractor.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(step, options = {})
|
15
|
+
# Track tokens before execution
|
16
|
+
tokens_before = @context.workflow.context_manager&.total_tokens || 0
|
17
|
+
|
18
|
+
# Execute the step
|
19
|
+
result = @base_executor.execute(step, options)
|
20
|
+
|
21
|
+
# Report token consumption after successful execution
|
22
|
+
tokens_after = @context.workflow.context_manager&.total_tokens || 0
|
23
|
+
tokens_consumed = tokens_after - tokens_before
|
24
|
+
|
25
|
+
step_type = StepTypeResolver.resolve(step, @context)
|
26
|
+
step_name = @name_extractor.extract(step, step_type)
|
27
|
+
@reporter.report(step_name, tokens_consumed, tokens_after)
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
# Override execute_steps to ensure reporting happens for each step
|
33
|
+
def execute_steps(workflow_steps)
|
34
|
+
workflow_steps.each_with_index do |step, index|
|
35
|
+
is_last_step = (index == workflow_steps.length - 1)
|
36
|
+
case step
|
37
|
+
when Hash
|
38
|
+
execute(step, is_last_step:)
|
39
|
+
when Array
|
40
|
+
execute(step, is_last_step:)
|
41
|
+
when String
|
42
|
+
execute(step, is_last_step:)
|
43
|
+
# Handle pause after string steps
|
44
|
+
if @context.workflow.pause_step_name == step
|
45
|
+
Kernel.binding.irb # rubocop:disable Lint/Debugger
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# For other types, delegate to base executor
|
49
|
+
execute(step, is_last_step:)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Delegate all other methods to the base executor
|
55
|
+
def method_missing(method, *args, **kwargs, &block)
|
56
|
+
if @base_executor.respond_to?(method)
|
57
|
+
@base_executor.send(method, *args, **kwargs, &block)
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_to_missing?(method, include_private = false)
|
64
|
+
@base_executor.respond_to?(method, include_private) || super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Extracts human-readable names from various step types
|
6
|
+
class StepNameExtractor
|
7
|
+
def extract(step, step_type)
|
8
|
+
case step_type
|
9
|
+
when StepTypeResolver::COMMAND_STEP
|
10
|
+
extract_command_name(step)
|
11
|
+
when StepTypeResolver::HASH_STEP
|
12
|
+
extract_hash_step_name(step)
|
13
|
+
when StepTypeResolver::ITERATION_STEP
|
14
|
+
extract_iteration_step_name(step)
|
15
|
+
when StepTypeResolver::CONDITIONAL_STEP
|
16
|
+
extract_conditional_step_name(step)
|
17
|
+
when StepTypeResolver::CASE_STEP
|
18
|
+
"case"
|
19
|
+
when StepTypeResolver::INPUT_STEP
|
20
|
+
"input"
|
21
|
+
when StepTypeResolver::AGENT_STEP
|
22
|
+
StepTypeResolver.extract_name(step)
|
23
|
+
when StepTypeResolver::STRING_STEP
|
24
|
+
step.to_s
|
25
|
+
else
|
26
|
+
step.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def extract_command_name(step)
|
33
|
+
cmd = step.to_s.strip
|
34
|
+
cmd.length > 20 ? "#{cmd[0..19]}..." : cmd
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_hash_step_name(step)
|
38
|
+
key, value = step.to_a.first
|
39
|
+
|
40
|
+
# Check if this looks like an inline prompt (key is similar to sanitized value)
|
41
|
+
if value.is_a?(String)
|
42
|
+
# Get first non-empty line
|
43
|
+
first_line = value.lines.map(&:strip).find { |line| !line.empty? } || ""
|
44
|
+
|
45
|
+
# If key looks like it was auto-generated from the content, use truncated content
|
46
|
+
sanitized = first_line.downcase.gsub(/[^a-z0-9_]/, "_").squeeze("_").gsub(/^_|_$/, "")
|
47
|
+
if key.to_s == sanitized || key.to_s.start_with?(sanitized[0..15])
|
48
|
+
# This is likely an inline prompt
|
49
|
+
first_line.length > 20 ? "#{first_line[0..19]}..." : first_line
|
50
|
+
else
|
51
|
+
# This is a labeled step
|
52
|
+
key.to_s
|
53
|
+
end
|
54
|
+
else
|
55
|
+
key.to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_iteration_step_name(step)
|
60
|
+
if step.key?("each")
|
61
|
+
items = step["each"]
|
62
|
+
count = items.respond_to?(:size) ? items.size : "?"
|
63
|
+
"each (#{count} items)"
|
64
|
+
elsif step.key?("repeat")
|
65
|
+
config = step["repeat"]
|
66
|
+
times = config.is_a?(Hash) ? config["times"] || "?" : config
|
67
|
+
"repeat (#{times} times)"
|
68
|
+
else
|
69
|
+
"iteration"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_conditional_step_name(step)
|
74
|
+
if step.key?("if")
|
75
|
+
"if"
|
76
|
+
elsif step.key?("unless")
|
77
|
+
"unless"
|
78
|
+
else
|
79
|
+
"conditional"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Handles the validation command logic for the CLI
|
6
|
+
class ValidationCommand
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute(workflow_path = nil)
|
12
|
+
workflow_files = resolve_workflow_files(workflow_path)
|
13
|
+
validate_workflows(workflow_files)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def resolve_workflow_files(workflow_path)
|
19
|
+
if workflow_path.nil?
|
20
|
+
find_all_workflows
|
21
|
+
else
|
22
|
+
[expand_workflow_path(workflow_path)]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_all_workflows
|
27
|
+
roast_dir = File.join(Dir.pwd, "roast")
|
28
|
+
unless File.directory?(roast_dir)
|
29
|
+
raise Thor::Error, "No roast/ directory found in current path"
|
30
|
+
end
|
31
|
+
|
32
|
+
workflow_files = Dir.glob(File.join(roast_dir, "**/workflow.yml")).sort
|
33
|
+
if workflow_files.empty?
|
34
|
+
raise Thor::Error, "No workflow.yml files found in roast/ directory"
|
35
|
+
end
|
36
|
+
|
37
|
+
workflow_files
|
38
|
+
end
|
39
|
+
|
40
|
+
def expand_workflow_path(workflow_path)
|
41
|
+
expanded_path = if workflow_path.end_with?(".yml", ".yaml") || workflow_path.include?("/")
|
42
|
+
File.expand_path(workflow_path)
|
43
|
+
else
|
44
|
+
File.expand_path("roast/#{workflow_path}/workflow.yml")
|
45
|
+
end
|
46
|
+
|
47
|
+
unless File.exist?(expanded_path)
|
48
|
+
raise Thor::Error, "Workflow file not found: #{expanded_path}"
|
49
|
+
end
|
50
|
+
|
51
|
+
expanded_path
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_workflows(workflow_files)
|
55
|
+
results = ValidationResults.new
|
56
|
+
|
57
|
+
validate_multiple_workflows_display(workflow_files, results)
|
58
|
+
|
59
|
+
display_summary(results)
|
60
|
+
exit_if_needed(results)
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_multiple_workflows_display(workflow_files, results)
|
64
|
+
::CLI::UI::Frame.open("Validating #{workflow_files.size} workflow(s)") do
|
65
|
+
validate_each_workflow(workflow_files, results)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_each_workflow(workflow_files, results)
|
70
|
+
workflow_files.each do |workflow_path|
|
71
|
+
workflow_name = extract_workflow_name(workflow_path)
|
72
|
+
validator = create_validator(workflow_path)
|
73
|
+
# Ensure validation is performed to populate errors/warnings
|
74
|
+
is_valid = validator.valid?
|
75
|
+
results.add_result(workflow_path, validator)
|
76
|
+
|
77
|
+
display_workflow_result(workflow_name, validator, is_valid)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def display_workflow_result(workflow_name, validator, is_valid)
|
82
|
+
if is_valid
|
83
|
+
if validator.warnings.empty?
|
84
|
+
puts ::CLI::UI.fmt("{{green:✓}} {{bold:#{workflow_name}}}")
|
85
|
+
else
|
86
|
+
puts ::CLI::UI.fmt("{{green:✓}} {{bold:#{workflow_name}}} ({{yellow:#{validator.warnings.size} warning(s)}})")
|
87
|
+
end
|
88
|
+
else
|
89
|
+
puts ::CLI::UI.fmt("{{red:✗}} {{bold:#{workflow_name}}} ({{red:#{validator.errors.size} error(s)}})")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_validator(workflow_path)
|
94
|
+
yaml_content = File.read(workflow_path)
|
95
|
+
Validators::ValidationOrchestrator.new(yaml_content, workflow_path)
|
96
|
+
end
|
97
|
+
|
98
|
+
def extract_workflow_name(workflow_path)
|
99
|
+
workflow_path.sub("#{Dir.pwd}/roast/", "").sub("/workflow.yml", "")
|
100
|
+
end
|
101
|
+
|
102
|
+
def display_summary(results)
|
103
|
+
puts
|
104
|
+
|
105
|
+
if results.total_errors == 0 && results.total_warnings == 0
|
106
|
+
puts ::CLI::UI.fmt("{{green:All workflows are valid!}}")
|
107
|
+
elsif results.total_errors == 0
|
108
|
+
puts ::CLI::UI.fmt("{{green:All workflows are valid}} with {{yellow:#{results.total_warnings} total warning(s)}}")
|
109
|
+
display_all_warnings(results)
|
110
|
+
else
|
111
|
+
puts ::CLI::UI.fmt("{{red:Validation failed:}} #{results.total_errors} error(s), #{results.total_warnings} warning(s)")
|
112
|
+
display_all_errors(results)
|
113
|
+
display_all_warnings(results) if results.total_warnings > 0
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def exit_if_needed(results)
|
118
|
+
if results.total_errors > 0
|
119
|
+
exit(1)
|
120
|
+
elsif results.total_warnings > 0 && @options[:strict]
|
121
|
+
exit(1)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def display_errors(errors)
|
126
|
+
::CLI::UI::Frame.open("Errors", color: :red) do
|
127
|
+
errors.each do |error|
|
128
|
+
puts ::CLI::UI.fmt("{{red:• #{error[:message]}}}")
|
129
|
+
puts ::CLI::UI.fmt(" {{gray:→ #{error[:suggestion]}}}") if error[:suggestion]
|
130
|
+
puts
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def display_warnings(warnings)
|
136
|
+
::CLI::UI::Frame.open("Warnings", color: :yellow) do
|
137
|
+
warnings.each do |warning|
|
138
|
+
puts ::CLI::UI.fmt("{{yellow:• #{warning[:message]}}}")
|
139
|
+
puts ::CLI::UI.fmt(" {{gray:→ #{warning[:suggestion]}}}") if warning[:suggestion]
|
140
|
+
puts
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def display_all_errors(results)
|
146
|
+
results.results_with_errors.each do |result|
|
147
|
+
workflow_name = extract_workflow_name(result[:path])
|
148
|
+
::CLI::UI::Frame.open("Errors in #{workflow_name}", color: :red) do
|
149
|
+
result[:validator].errors.each do |error|
|
150
|
+
puts ::CLI::UI.fmt("{{red:• #{error[:message]}}}")
|
151
|
+
puts ::CLI::UI.fmt(" {{gray:→ #{error[:suggestion]}}}") if error[:suggestion]
|
152
|
+
puts
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def display_all_warnings(results)
|
159
|
+
results.results_with_warnings.each do |result|
|
160
|
+
workflow_name = extract_workflow_name(result[:path])
|
161
|
+
::CLI::UI::Frame.open("Warnings in #{workflow_name}", color: :yellow) do
|
162
|
+
result[:validator].warnings.each do |warning|
|
163
|
+
puts ::CLI::UI.fmt("{{yellow:• #{warning[:message]}}}")
|
164
|
+
puts ::CLI::UI.fmt(" {{gray:→ #{warning[:suggestion]}}}") if warning[:suggestion]
|
165
|
+
puts
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Tracks validation results across multiple workflows
|
172
|
+
class ValidationResults
|
173
|
+
attr_reader :total_errors, :total_warnings
|
174
|
+
|
175
|
+
def initialize
|
176
|
+
@total_errors = 0
|
177
|
+
@total_warnings = 0
|
178
|
+
@results = []
|
179
|
+
end
|
180
|
+
|
181
|
+
def add_result(workflow_path, validator)
|
182
|
+
@results << { path: workflow_path, validator: validator }
|
183
|
+
@total_errors += validator.errors.size
|
184
|
+
@total_warnings += validator.warnings.size
|
185
|
+
end
|
186
|
+
|
187
|
+
def results_with_errors
|
188
|
+
@results.select { |result| result[:validator].errors.any? }
|
189
|
+
end
|
190
|
+
|
191
|
+
def results_with_warnings
|
192
|
+
@results.select { |result| result[:validator].warnings.any? }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
module Validators
|
6
|
+
# Base class for all validators
|
7
|
+
class BaseValidator
|
8
|
+
attr_reader :errors, :warnings
|
9
|
+
|
10
|
+
def initialize(parsed_yaml, workflow_path = nil)
|
11
|
+
@parsed_yaml = parsed_yaml
|
12
|
+
@workflow_path = workflow_path
|
13
|
+
@errors = []
|
14
|
+
@warnings = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
raise NotImplementedError, "Subclasses must implement validate"
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid?
|
22
|
+
validate
|
23
|
+
@errors.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def add_error(type:, message:, suggestion: nil, **metadata)
|
29
|
+
error = { type: type, message: message }
|
30
|
+
error[:suggestion] = suggestion if suggestion
|
31
|
+
error.merge!(metadata)
|
32
|
+
@errors << error
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_warning(type:, message:, suggestion: nil, **metadata)
|
36
|
+
warning = { type: type, message: message }
|
37
|
+
warning[:suggestion] = suggestion if suggestion
|
38
|
+
warning.merge!(metadata)
|
39
|
+
@warnings << warning
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|