Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions lib/roast/config_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def initialize(cog_registry, config_procs, workflow_context)
@workflow_context = workflow_context
@config_context = ConfigContext.new #: ConfigContext
@global_config = Cog::Config.new #: Cog::Config
@global_regexp_configs = {} #: Hash[Regexp, Cog::Config]
@global_name_configs = {} #: Hash[Symbol, Cog::Config]
@general_configs = {} #: Hash[singleton(Cog), Cog::Config]
@regexp_scoped_configs = {} #: Hash[singleton(Cog), Hash[Regexp, Cog::Config]]
@name_scoped_configs = {} #: Hash[singleton(Cog), Hash[Symbol, Cog::Config]]
Expand Down Expand Up @@ -48,11 +50,24 @@ def config_for(cog_class, name = nil)
raise ConfigManagerNotPreparedError unless prepared?

# All cogs will always have a config; empty by default if the cog was never explicitly configured
config = cog_class.config_class.new(@global_config.instance_variable_get(:@values).deep_dup)
# Start with bare global config
config = cog_class.config_class.new(@global_config.values.deep_dup)
# Apply matching global regexp configs (insertion order)
@global_regexp_configs.each do |pattern, cfg|
config = config.merge(cfg) if !name.nil? && pattern.match?(name.to_s)
end
# Apply matching global name config
unless name.nil?
global_name_cfg = @global_name_configs[name]
config = config.merge(global_name_cfg) if global_name_cfg
end
# Apply cog-type general config
config = config.merge(fetch_general_config(cog_class))
# Apply cog-type regexp configs
@regexp_scoped_configs.fetch(cog_class, {}).select do |pattern, _|
pattern.match?(name.to_s) unless name.nil?
end.values.each { |cfg| config = config.merge(cfg) }
# Apply cog-type name config
unless name.nil?
name_scoped_config = fetch_name_scoped_config(cog_class, name)
config = config.merge(name_scoped_config)
Expand Down Expand Up @@ -80,6 +95,16 @@ def fetch_name_scoped_config(cog_class, name)
name_scoped_configs_for_cog[name] ||= cog_class.config_class.new
end

#: (Regexp) -> Cog::Config
def fetch_global_regexp_config(pattern)
@global_regexp_configs[pattern] ||= Cog::Config.new
end

#: (Symbol) -> Cog::Config
def fetch_global_name_config(name)
@global_name_configs[name] ||= Cog::Config.new
end

#: () -> void
def bind_registered_cogs
@cog_registry.cogs.each { |cog_method_name, cog_class| bind_cog(cog_method_name, cog_class) }
Expand Down Expand Up @@ -127,19 +152,30 @@ def on_config(cog_class, cog_name_or_pattern, cog_config_proc)

def bind_global
on_global_method = method(:on_global)
method_to_bind = proc do |&global_proc|
on_global_method.call(global_proc)
method_to_bind = proc do |name_or_pattern = nil, &global_proc|
on_global_method.call(name_or_pattern, global_proc)
end
@config_context.instance_eval do
define_singleton_method(:global, method_to_bind)
end
end

#: (^() -> void ) -> void
def on_global(global_config_proc)
#: ((Symbol | Regexp)?, ^() -> void) -> void
def on_global(name_or_pattern, global_config_proc)
name_or_pattern = name_or_pattern #: untyped
config_object = case name_or_pattern
when NilClass
@global_config
when Regexp
fetch_global_regexp_config(name_or_pattern)
when Symbol
fetch_global_name_config(name_or_pattern)
else
raise ArgumentError, "Invalid type '#{name_or_pattern.class}' for global name_or_pattern"
end
global_config_proc = global_config_proc #: as ^(untyped) -> void
bind_workflow_params(@global_config)
@global_config.instance_exec(&global_config_proc) if global_config_proc
bind_workflow_params(config_object)
config_object.instance_exec(&global_config_proc) if global_config_proc
nil
end

Expand Down
13 changes: 10 additions & 3 deletions sorbet/rbi/shims/lib/roast/config_context.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

module Roast
class ConfigContext

# Configure all cogs globally with shared settings
#
# Apply configuration that affects all cog instances in the workflow. Configuration
Expand All @@ -16,6 +15,14 @@ module Roast
# global do
# # Configuration here applies to all cogs
# end
#
# global(:step_name) do
# # Configuration here applies to all cogs named :step_name
# end
#
# global(/pattern/) do
# # Configuration here applies to all cogs whose name matches /pattern/
# end
# end
# ```
#
Expand All @@ -35,8 +42,8 @@ module Roast
# - `working_directory(path)` - Set the working directory for external commands invoked by the cog
# - `use_current_working_directory!` - Use the current working directory
#
#: () {() [self: Roast::Cog::Config] -> void} -> void
def global(&block); end
#: (?(Symbol | Regexp)?) {() [self: Roast::Cog::Config] -> void} -> void
def global(name_or_pattern = nil, &block); end

# Configure the `call` cog
#
Expand Down
106 changes: 106 additions & 0 deletions test/roast/config_manager_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,112 @@ def build_manager(config_procs = [], params: WorkflowParams.new([], [], {}))
assert manager.config_for(TestCog).abort_on_failure?
end

test "global with name specifier applies only to matching cog name" do
config_proc = proc do
global(:my_step) { self[:marker] = "named_global" }
end
manager = build_manager([config_proc])
manager.prepare!

matching_config = manager.config_for(TestCog, :my_step)
non_matching_config = manager.config_for(TestCog, :other_step)

assert_equal "named_global", matching_config.values[:marker]
assert_nil non_matching_config.values[:marker]
end

test "global with regexp specifier applies to matching cog names" do
config_proc = proc do
global(/^api_/) { async! }
end
manager = build_manager([config_proc])
manager.prepare!

matching_config = manager.config_for(TestCog, :api_call)
non_matching_config = manager.config_for(TestCog, :db_query)

assert matching_config.async?
refute non_matching_config.async?
end

test "global with name specifier does not apply when name is nil" do
config_proc = proc do
global(:my_step) { async! }
end
manager = build_manager([config_proc])
manager.prepare!

config = manager.config_for(TestCog)

refute config.async?
end

test "global regexp does not apply when cog name is nil" do
config_proc = proc do
global(/.*/) { async! }
end
manager = build_manager([config_proc])
manager.prepare!

config = manager.config_for(TestCog)

refute config.async?
end

test "global cascade order: bare < regexp < name" do
config_proc = proc do
global { self[:priority] = "bare" }
global(/my/) { self[:priority] = "regexp" }
global(:my_step) { self[:priority] = "name" }
end
manager = build_manager([config_proc])
manager.prepare!

# Named global should win over regexp and bare
config = manager.config_for(TestCog, :my_step)
assert_equal "name", config.values[:priority]
end

test "multiple global regexps apply in insertion order" do
config_proc = proc do
global(/^a/) { self[:priority] = "first" }
global(/api/) { self[:priority] = "second" }
end
manager = build_manager([config_proc])
manager.prepare!

# Both match :api_call β€” second one wins (applied last)
config = manager.config_for(TestCog, :api_call)
assert_equal "second", config.values[:priority]
end

test "global name config is overridden by cog-specific name config" do
config_proc = proc do
global(:my_step) { self[:marker] = "global" }
test_cog(:my_step) { timeout 90 }
end
manager = build_manager([config_proc])
manager.prepare!

config = manager.config_for(TestCog, :my_step)

# Cog-specific name config takes precedence for timeout
assert_equal 90, config.timeout
# Global name config's marker is also present (merged)
assert_equal "global", config.values[:marker]
end

test "workflow params are accessible inside a global name config block" do
captured = nil
config_proc = proc do
global(:my_step) { captured = target! }
end
manager = build_manager([config_proc], params: WorkflowParams.new(["Gemfile"], [], {}))
manager.prepare!

assert_equal "Gemfile", captured
end

test "workflow params are not accessible in the top-level config block body" do
config_proc = proc do
args
Expand Down
Loading