diff --git a/lib/roast/config_manager.rb b/lib/roast/config_manager.rb index 621f5273..262af1c0 100644 --- a/lib/roast/config_manager.rb +++ b/lib/roast/config_manager.rb @@ -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]] @@ -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) @@ -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) } @@ -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 diff --git a/sorbet/rbi/shims/lib/roast/config_context.rbi b/sorbet/rbi/shims/lib/roast/config_context.rbi index 14d8b1a8..ddbb10c2 100644 --- a/sorbet/rbi/shims/lib/roast/config_context.rbi +++ b/sorbet/rbi/shims/lib/roast/config_context.rbi @@ -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 @@ -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 # ``` # @@ -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 # diff --git a/test/roast/config_manager_test.rb b/test/roast/config_manager_test.rb index 20249516..7d8db4f2 100644 --- a/test/roast/config_manager_test.rb +++ b/test/roast/config_manager_test.rb @@ -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