roast-ai 0.4.8 → 0.4.9

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -0
  4. data/Gemfile.lock +3 -3
  5. data/README.md +9 -5
  6. data/dsl/less_simple.rb +112 -0
  7. data/dsl/prototype.rb +17 -0
  8. data/dsl/simple.rb +5 -7
  9. data/dsl/step_communication.rb +18 -0
  10. data/examples/grading/README.md +46 -0
  11. data/examples/grading/analyze_coverage/prompt.md +52 -0
  12. data/examples/grading/calculate_final_grade.rb +64 -0
  13. data/examples/grading/format_result.rb +61 -0
  14. data/examples/grading/generate_grades/prompt.md +105 -0
  15. data/examples/grading/generate_recommendations/output.txt +17 -0
  16. data/examples/grading/generate_recommendations/prompt.md +60 -0
  17. data/examples/grading/read_dependencies/prompt.md +15 -0
  18. data/examples/grading/verify_mocks_and_stubs/prompt.md +12 -0
  19. data/examples/grading/verify_test_helpers/prompt.md +53 -0
  20. data/examples/grading/workflow.md +5 -0
  21. data/examples/grading/workflow.yml +28 -0
  22. data/lib/roast/dsl/cog/config.rb +31 -0
  23. data/lib/roast/dsl/cog/stack.rb +21 -0
  24. data/lib/roast/dsl/cog/store.rb +26 -0
  25. data/lib/roast/dsl/cog.rb +70 -0
  26. data/lib/roast/dsl/cog_execution_context.rb +29 -0
  27. data/lib/roast/dsl/cogs/cmd.rb +55 -0
  28. data/lib/roast/dsl/cogs/graph.rb +53 -0
  29. data/lib/roast/dsl/cogs.rb +65 -0
  30. data/lib/roast/dsl/config_context.rb +54 -0
  31. data/lib/roast/dsl/executor.rb +62 -7
  32. data/lib/roast/dsl/workflow_execution_context.rb +47 -0
  33. data/lib/roast/error.rb +7 -0
  34. data/lib/roast/errors.rb +3 -3
  35. data/lib/roast/graph/edge.rb +25 -0
  36. data/lib/roast/graph/node.rb +40 -0
  37. data/lib/roast/graph/quantum_edge.rb +27 -0
  38. data/lib/roast/graph/threaded_exec.rb +93 -0
  39. data/lib/roast/graph.rb +233 -0
  40. data/lib/roast/resources/api_resource.rb +2 -2
  41. data/lib/roast/resources/url_resource.rb +2 -2
  42. data/lib/roast/tools/apply_diff.rb +1 -1
  43. data/lib/roast/tools/ask_user.rb +1 -1
  44. data/lib/roast/tools/bash.rb +1 -1
  45. data/lib/roast/tools/cmd.rb +2 -2
  46. data/lib/roast/tools/coding_agent.rb +2 -2
  47. data/lib/roast/tools/grep.rb +1 -1
  48. data/lib/roast/tools/read_file.rb +1 -1
  49. data/lib/roast/tools/search_file.rb +1 -1
  50. data/lib/roast/tools/swarm.rb +1 -1
  51. data/lib/roast/tools/update_files.rb +2 -2
  52. data/lib/roast/tools/write_file.rb +1 -1
  53. data/lib/roast/tools.rb +1 -1
  54. data/lib/roast/value_objects/api_token.rb +1 -1
  55. data/lib/roast/value_objects/uri_base.rb +1 -1
  56. data/lib/roast/value_objects/workflow_path.rb +1 -1
  57. data/lib/roast/version.rb +1 -1
  58. data/lib/roast/workflow/base_workflow.rb +38 -2
  59. data/lib/roast/workflow/command_executor.rb +1 -1
  60. data/lib/roast/workflow/configuration_loader.rb +1 -1
  61. data/lib/roast/workflow/error_handler.rb +1 -1
  62. data/lib/roast/workflow/step_executor_registry.rb +1 -1
  63. data/lib/roast/workflow/step_loader.rb +1 -1
  64. data/lib/roast/workflow/workflow_executor.rb +1 -1
  65. data/lib/roast.rb +1 -1
  66. data/sorbet/config +2 -0
  67. data/sorbet/rbi/annotations/.gitattributes +1 -0
  68. data/sorbet/rbi/annotations/activesupport.rbi +495 -0
  69. data/sorbet/rbi/annotations/faraday.rbi +17 -0
  70. data/sorbet/rbi/annotations/minitest.rbi +119 -0
  71. data/sorbet/rbi/annotations/mocha.rbi +34 -0
  72. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  73. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  74. data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +2170 -0
  75. data/sorbet/rbi/gems/{rexml@3.4.1.rbi → rexml@3.4.2.rbi} +284 -239
  76. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +11 -0
  77. data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +11 -0
  78. data/sorbet/rbi/todo.rbi +7 -0
  79. metadata +46 -5
  80. data/package-lock.json +0 -6
  81. /data/sorbet/rbi/gems/{rack@2.2.17.rbi → rack@2.2.18.rbi} +0 -0
@@ -0,0 +1,15 @@
1
+ Use the provided functions to find and read important dependencies of the provided test file named <%= workflow.file %>.
2
+
3
+ The first dependency you should always look for is the source file for the prime subject of the test (whatever class this test file is claiming to test). Use `read_file` to read the subject's source code into your conversation transcript, but only if it's not already there from a previous chat.
4
+
5
+ If you can identify other important application-level dependencies then read them too.
6
+ How many extra dependencies to research is left to your discretion, but ALWAYS make sure you have the subject under test (SUT) in your context before responding.
7
+
8
+ Once you are finished using tool functions, respond with the relative path to the source file of the SUT inside <sut> tags. IMPORTANT: Include the full relative path from the project root, including any directory prefixes like lib/, app/, etc.
9
+
10
+ Example:
11
+
12
+ If you are told to find the dependencies of `test/services/country_db_interface_test.rb`,
13
+ then you would use the functions as explained above and ultimately respond with `<sut>app/services/country_db_interface.rb</sut>`
14
+
15
+ If the file is found at `lib/roast/workflow/workflow_initializer.rb`, respond with `<sut>lib/roast/workflow/workflow_initializer.rb</sut>` (include the lib/ prefix)
@@ -0,0 +1,12 @@
1
+ Find places in the provided test code where stubbing and mocking are used. Search for the corresponding implementation source code of those dependencies elsewhere in the codebase to validate that the stub or mock matches the implementation that it is doubling. Use the tool functions provided to find and read the dependencies.
2
+
3
+ Once you've found the dependencies, verify that any mocks and stubs accurately reflect the real implementation. If there are discrepancies, list them out alphabetically with:
4
+
5
+ 1. The name of the mocked/stubbed method
6
+ 2. What the mock/stub expects in terms of arguments and/or return values
7
+ 3. What the actual implementation actually takes as arguments and returns
8
+ 4. Suggestions for fixing the discrepancy
9
+
10
+ Note: If there are no discrepancies, do not summarize those that accurately reflect their real implementations in the codebase, just respond "All mocks and stubs verified."
11
+
12
+ IMPORTANT: There's absolutely no need for you to waste time grepping for methods/functions that you know belong to testing libraries such as Mocha's `expects` and `stubs`. Only search for the implementation of things that are stubbed and/or mocked in the test to verify whether the test code matches the implementation code.
@@ -0,0 +1,53 @@
1
+ Now identify custom test helpers used in this test for the following purpose:
2
+
3
+ 1. Analyzing if they are used correctly
4
+ 2. Understanding test code that has had significant chunks of implementation abstracted away into helpers
5
+ 3. Fully understanding custom assertions that are not included by default in Ruby on Rails or part of your base knowledge
6
+
7
+ Your grep tool function is vital for this work. It provides 4 lines of context before and after the matching line.
8
+
9
+ For example, if you call `grep(string: "def assert_sql")`, the output will include:
10
+
11
+ ```
12
+ .test/support/helpers/sql_assertions.rb-101- end
13
+ .test/support/helpers/sql_assertions.rb-102- result
14
+ .test/support/helpers/sql_assertions.rb-103- end
15
+ .test/support/helpers/sql_assertions.rb-104-
16
+ .test/support/helpers/sql_assertions.rb:105: def assert_sql(*patterns_to_match, **kwargs, &block)
17
+ .test/support/helpers/sql_assertions.rb-106- mysql_only_test!
18
+ .test/support/helpers/sql_assertions.rb-107-
19
+ .test/support/helpers/sql_assertions.rb-108- result = T.let(nil, T.nilable(T::Boolean))
20
+ .test/support/helpers/sql_assertions.rb-109- counter = ActiveRecord::SQLCounter.new(**kwargs)
21
+ ```
22
+
23
+ Unfortunately, many test helper methods are undocumented. In those cases (like the example above) the pre-context will be junk. However, there are a number of helper methods that do have very specific and narrow use cases, and those do tend to be well-documented. In those cases, you should use `read_file` to be able to read the full documentation.
24
+
25
+ For example, here is the result of calling `grep(string: "def assert_sql_events")`
26
+
27
+ ```
28
+ .test/support/helpers/externals_helper.rb-93- # @example Logs events in the list that did not occur
29
+ .test/support/helpers/externals_helper.rb-94- # expected_queries = { "Shop Load" => 1, "User Load" => 1 }
30
+ .test/support/helpers/externals_helper.rb-95- # # Fails and reports that User Load occured 0 times instead of expected 1
31
+ .test/support/helpers/externals_helper.rb-96- # assert_sql_events(expected_queries) { Shop.current_or_find(shop.id) }
32
+ .test/support/helpers/externals_helper.rb:97: def assert_sql_events(expected_events, &block)
33
+ .test/support/helpers/externals_helper.rb-98- mysql_only_test!
34
+ .test/support/helpers/externals_helper.rb-99-
35
+ .test/support/helpers/externals_helper.rb-100- mysql_events = ExternalsCollector.new(&block).events
36
+ .test/support/helpers/externals_helper.rb-101- .select { |e| e.first == :mysql }
37
+ ```
38
+
39
+ Notice that the documentation for the `assert_sql_events` method is cutoff. Use your `read_file` tool function to get the whole test helper source code and gain better understanding of how it is intended to be used, with the side benefit of also being able to see how it is implemented.
40
+
41
+ Note: You will undoubtedly already be familiar with some of Minitest and RSpec's built-in helpers. There is no need to search for those, since they are packaged as gems you won't find them anyway.
42
+
43
+ DO NOT FORGET TO PREPEND `def` TO YOUR QUERY TO FIND A METHOD DEFINITION INSTEAD OF USAGES, otherwise you may bring back a very large and useless result set!!!
44
+
45
+ Once you are done understanding the custom test helpers used in the test file, analyze and report on whether it seems like any of the helpers are:
46
+
47
+ 1. Used incorrectly
48
+ 2. Used unnecessarily
49
+ 3. Any other problem related to the use of helper methods
50
+
51
+ Where possible, use your best judgment to make recommendations for how to fix problems that you find, but ONLY related to test helpers.
52
+
53
+ Note: You are only being used to help find problems so it is not necessary to report on correct usage of helpers or to make positive comments.
@@ -0,0 +1,5 @@
1
+ As a senior software engineer and testing expert, evaluate the quality of this test file based on guidelines that will be subsequently provided.
2
+
3
+ Next I will now provide the source code of the test that we will be analyzing, and then step you through a series of analysis activities, before finally asking you to provided a final report.
4
+
5
+ # <%= file %> <%= File.read(file) %>
@@ -0,0 +1,28 @@
1
+ name: Test Grading
2
+
3
+ tools:
4
+ - Roast::Tools::Grep
5
+ - Roast::Tools::ReadFile
6
+ - Roast::Tools::SearchFile
7
+
8
+ steps:
9
+ - read_dependencies
10
+ -
11
+ - verify_test_helpers
12
+ - verify_mocks_and_stubs
13
+ - generate_grades
14
+ - calculate_final_grade
15
+ - format_result
16
+ - generate_recommendations
17
+
18
+ analyze_coverage:
19
+ json: true
20
+
21
+ generate_grades:
22
+ model: o3
23
+ json: true
24
+
25
+ generate_recommendations:
26
+ model: o3
27
+ json: true
28
+
@@ -0,0 +1,31 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class Cog
7
+ class Config
8
+ attr_reader :values
9
+
10
+ def initialize(initial = {})
11
+ @values = initial
12
+ end
13
+
14
+ def merge(config_object)
15
+ self.class.new(values.merge(config_object.values))
16
+ end
17
+
18
+ # It is recommended to implement a custom config object for a nicer interface,
19
+ # but for simple cases where it would just be a key value store we provide one by default.
20
+
21
+ def []=(key, value)
22
+ @values[key] = value
23
+ end
24
+
25
+ def [](key)
26
+ @values[key]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class Cog
7
+ class Stack
8
+ delegate :map, :push, :size, :empty?, to: :@queue
9
+
10
+ def initialize
11
+ @queue = []
12
+ end
13
+
14
+ #: () -> Roast::DSL::Cog?
15
+ def pop
16
+ @queue.shift
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class Cog
7
+ class Store
8
+ class CogAlreadyDefinedError < Roast::Error; end
9
+
10
+ delegate :[], to: :store
11
+
12
+ #: (Symbol, Roast::DSL::Cog) -> Roast::DSL::Cog
13
+ def insert(id, inst)
14
+ raise CogAlreadyDefinedError if store.key?(id)
15
+
16
+ store[id] = inst
17
+ end
18
+
19
+ #: () -> Hash[Symbol, Roast::DSL::Cog]
20
+ def store
21
+ @store ||= {}
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class Cog
7
+ class CogAlreadyRanError < StandardError; end
8
+
9
+ class << self
10
+ def on_create
11
+ eigen = self
12
+ proc do |instance_name = Random.uuid, &action|
13
+ #: self as Roast::DSL::WorkflowExecutionContext
14
+ add_cog_instance(instance_name, eigen.new(instance_name, action))
15
+ end
16
+ end
17
+
18
+ def on_config
19
+ eigen = self
20
+ proc do |cog_name = nil, &configuration|
21
+ #: self as Roast::DSL::ConfigContext
22
+ config_object = if cog_name.nil?
23
+ fetch_execution_scope(eigen)
24
+ else
25
+ fetch_or_create_cog_config(eigen, cog_name)
26
+ end
27
+
28
+ config_object.instance_exec(&configuration) if configuration
29
+ end
30
+ end
31
+
32
+ def config_class
33
+ @config_class ||= find_child_config_or_default
34
+ end
35
+
36
+ private
37
+
38
+ def find_child_config_or_default
39
+ config_constant = "#{name}::Config"
40
+ const_defined?(config_constant) ? const_get(config_constant) : Cog::Config # rubocop:disable Sorbet/ConstantsFromStrings
41
+ end
42
+ end
43
+
44
+ attr_reader :name, :output
45
+
46
+ def initialize(name, cog_input_proc)
47
+ @name = name
48
+ @cog_input_proc = cog_input_proc
49
+ @finished = false
50
+ end
51
+
52
+ def run!(config, cog_execution_context)
53
+ raise CogAlreadyRanError if ran?
54
+
55
+ @config = config
56
+ @output = execute(cog_execution_context.instance_exec(&@cog_input_proc))
57
+ @finished = true
58
+ end
59
+
60
+ def ran?
61
+ @finished
62
+ end
63
+
64
+ # Inheriting cog must implement this
65
+ def execute(input)
66
+ raise NotImplementedError
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ # Contains the cogs already executed in this run.
7
+ class CogExecutionContext
8
+ # Raises if you access a cog in an execution block that hasn't already been run.
9
+ class IncompleteCogExecutionAccessError < StandardError; end
10
+
11
+ def initialize(cogs, bound_names)
12
+ @cogs = cogs
13
+ bind_cog_methods(bound_names)
14
+ end
15
+
16
+ private
17
+
18
+ def bind_cog_methods(bound_names)
19
+ bound_names.map do |name|
20
+ define_singleton_method(name.to_sym, ->(name) do
21
+ @cogs[name].tap do |cog|
22
+ raise IncompleteCogExecutionAccessError unless cog.ran?
23
+ end.output
24
+ end)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Cmd < Cog
8
+ class Output
9
+ #: String?
10
+ attr_reader :command_output
11
+
12
+ #: String?
13
+ attr_reader :err
14
+
15
+ #: Process::Status?
16
+ attr_reader :status
17
+
18
+ #: (
19
+ #| String? output,
20
+ #| String? error,
21
+ #| Process::Status? status
22
+ #| ) -> void
23
+ def initialize(output, error, status)
24
+ @command_output = output
25
+ @err = error
26
+ @status = status
27
+ end
28
+ end
29
+
30
+ class Config < Cog::Config
31
+ #: () -> void
32
+ def print_all!
33
+ @values[:print_all] = true
34
+ end
35
+
36
+ #: () -> bool
37
+ def print_all?
38
+ !!@values[:print_all]
39
+ end
40
+
41
+ def display!
42
+ print_all!
43
+ end
44
+ end
45
+
46
+ #: (String) -> Output
47
+ def execute(input)
48
+ result = Output.new(*Roast::Helpers::CmdRunner.capture3(input))
49
+ puts result.command_output if @config.print_all?
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Graph < Roast::DSL::Cog
8
+ #: Proc
9
+ attr_reader :block
10
+
11
+ #: (Symbol) { (Roast::DSL::Cogs::Graph) -> void } -> void
12
+ def initialize(name, &block)
13
+ @name = name
14
+ @block = block
15
+ @graph = Roast::Graph.new
16
+ super(name, proc {})
17
+ end
18
+
19
+ #: () -> void
20
+ def on_invoke
21
+ populate!(@graph)
22
+ end
23
+
24
+ #: () -> Symbol
25
+ def store_id
26
+ @name.to_sym
27
+ end
28
+
29
+ #: (Roast::DSL::Cog) -> void
30
+ def update(other)
31
+ return unless other.is_a?(Roast::DSL::Cogs::Graph)
32
+
33
+ return if other.block.nil?
34
+
35
+ other.populate!(@graph)
36
+ end
37
+
38
+ # Populates the provided graph in-place, with the definition of how to populate in the block
39
+ #: (Roast::DSL::Cogs::Graph) -> void
40
+ def populate!(graph)
41
+ return if @block.nil?
42
+
43
+ @block.call(graph)
44
+ end
45
+
46
+ #: () -> void
47
+ def execute
48
+ @graph.execute
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class << self
8
+ #: (String) -> void
9
+ def load_all_for(rube_rb_fpath)
10
+ rube_rb_fpath = File.expand_path(rube_rb_fpath)
11
+ all_cog_files(rube_rb_fpath).each do |cog_fpath|
12
+ require cog_fpath
13
+ end
14
+ bind_all_cog_invokers
15
+ end
16
+
17
+ #: () -> Array[Class]
18
+ def all_cog_classes
19
+ # rubocop:disable Sorbet/ConstantsFromStrings
20
+ Roast::DSL::Cogs.constants.map do |cog_class_name|
21
+ Roast::DSL::Cogs.const_get(cog_class_name)
22
+ end
23
+ # rubocop:enable Sorbet/ConstantsFromStrings
24
+ end
25
+
26
+ private
27
+
28
+ #: () -> void
29
+ def bind_all_cog_invokers
30
+ all_cog_classes.each do |cog_class|
31
+ # At some point we may want to tuck this all into a Roast::DSL::Binding/Scope/Context to avoid polluting toplevel.
32
+ TOPLEVEL_BINDING.eval(binding_string_for(cog_class))
33
+ end
34
+ end
35
+
36
+ #: (Class) -> String
37
+ def binding_string_for(cog_class)
38
+ <<~RUBY
39
+ def #{cog_class.method_name}(*args, **kwargs, &block)
40
+ #{cog_class.name}.invoke(*args, **kwargs, &block)
41
+ end
42
+ RUBY
43
+ end
44
+
45
+ #: (String) -> Array[String]
46
+ def all_cog_files(rube_rb_fpath)
47
+ dirs = [project_cogs_dir(rube_rb_fpath), internal_cogs_dir]
48
+ dirs.map do |dir|
49
+ Dir.glob(File.join(dir, "*.rb")) # Just toplevel .rb files
50
+ end.flatten
51
+ end
52
+
53
+ #: (String) -> String
54
+ def project_cogs_dir(rube_rb_fpath)
55
+ File.join(File.dirname(rube_rb_fpath), "cogs")
56
+ end
57
+
58
+ #: () -> String
59
+ def internal_cogs_dir
60
+ File.join(File.dirname(__FILE__), "cogs")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class ConfigContext
7
+ def initialize(cogs, config_proc)
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
53
+ end
54
+ end
@@ -4,23 +4,78 @@
4
4
  module Roast
5
5
  module DSL
6
6
  class Executor
7
+ class ExecutorError < Roast::Error; end
8
+ class ExecutorAlreadyPreparedError < ExecutorError; end
9
+ class ExecutorAlreadyCompletedError < ExecutorError; end
10
+
7
11
  class << self
8
12
  def from_file(workflow_path)
9
- execute(File.read(workflow_path))
13
+ run!(File.read(workflow_path))
10
14
  end
11
15
 
12
16
  private
13
17
 
14
- def execute(input)
15
- new.instance_eval(input)
18
+ def run!(workflow_definition)
19
+ executor = new
20
+ executor.prepare!(workflow_definition)
21
+ executor.start!
22
+ end
23
+ end
24
+
25
+ def prepare!(input)
26
+ # You can only initialize an executor once.
27
+ raise ExecutorAlreadyPreparedError if @prepared
28
+
29
+ extract_dsl_procs(input)
30
+ @cogs = Cog::Store.new
31
+ @cog_stack = Cog::Stack.new
32
+
33
+ @config_context = ConfigContext.new(@cogs, @config_proc)
34
+ @config_context.prepare!
35
+ @execution_context = WorkflowExecutionContext.new(@cogs, @cog_stack, @execution_proc)
36
+ @execution_context.prepare!
37
+
38
+ @prepared = true
39
+ end
40
+
41
+ def start!
42
+ # Now we run the cogs!
43
+ # You can only do this once, executors are not reusable to avoid state pollution
44
+ raise ExecutorAlreadyCompletedError if @completed
45
+
46
+ @cog_stack.map do |name, cog|
47
+ cog.run!(
48
+ @config_context.fetch_merged_config(cog.class, name.to_sym),
49
+ @execution_context.cog_execution_context,
50
+ )
16
51
  end
52
+
53
+ @completed = true
17
54
  end
18
55
 
19
- # Define methods to be used in workflows below.
56
+ def prepared?
57
+ @prepared ||= false
58
+ end
59
+
60
+ def completed?
61
+ @completed ||= false
62
+ end
63
+
64
+ #: { () [self: ConfigContext] -> void} -> void
65
+ def config(&block)
66
+ @config_proc = block
67
+ end
68
+
69
+ #: { () [self: WorkflowExecutionContext] -> void} -> void
70
+ def execute(&block)
71
+ @execution_proc = block
72
+ end
20
73
 
21
- def shell(command_string)
22
- output, _status = Roast::Helpers::CmdRunner.capture2e(command_string)
23
- puts output
74
+ # Separating the instance evals ensures that we can reuse the same cog method
75
+ # names between config and execute, while have the backing objects be completely
76
+ # different. This means we have an enforced separation between configuring and running.
77
+ def extract_dsl_procs(input)
78
+ instance_eval(input)
24
79
  end
25
80
  end
26
81
  end
@@ -0,0 +1,47 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class WorkflowExecutionContext
7
+ def initialize(cogs, cog_stack, execution_proc)
8
+ @cogs = cogs
9
+ @cog_stack = cog_stack
10
+ @execution_proc = execution_proc
11
+ @bound_names = []
12
+ end
13
+
14
+ def prepare!
15
+ bind_default_cogs
16
+ instance_eval(&@execution_proc)
17
+ end
18
+
19
+ def cog_execution_context
20
+ @cog_execution_context ||= CogExecutionContext.new(@cogs, @bound_names)
21
+ end
22
+
23
+ private
24
+
25
+ def add_cog_instance(name, cog)
26
+ @cogs.insert(name, cog)
27
+ @cog_stack.push([name, cog])
28
+ end
29
+
30
+ def output(name)
31
+ @cogs[name].output
32
+ end
33
+
34
+ #: () -> void
35
+ def bind_default_cogs
36
+ bind_cog(Cogs::Cmd, :cmd)
37
+ end
38
+
39
+ def bind_cog(cog_class, name)
40
+ @bound_names << name
41
+ instance_eval do
42
+ define_singleton_method(name, &cog_class.on_create)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,7 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ # Base class for all internal Roast errors.
6
+ class Error < StandardError; end
7
+ end