From 2c45a0a254dd61abca50e87039200cfad28d1e3c Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:08:05 +0700 Subject: [PATCH 01/11] Migrate Stroma to matrix-scoped DSL - Remove global DSL and Registry; replace with matrix-scoped design. - Introduce `Stroma::Matrix` to manage isolated registries per library. - Add `Stroma::DSL::Generator` for creating matrix-scoped DSL modules. - Update `Stroma::Hooks::Applier` and `Stroma::Hooks::Factory` to validate hooks via matrix entries. - Revise specs to test matrix isolation, hook inheritance, and registry behavior. --- README.md | 21 ++- lib/stroma/dsl.rb | 124 ------------------ lib/stroma/dsl/generator.rb | 104 +++++++++++++++ lib/stroma/hooks/applier.rb | 41 ++---- lib/stroma/hooks/factory.rb | 47 ++----- lib/stroma/matrix.rb | 62 +++++++++ lib/stroma/registry.rb | 61 +++------ .../{dsl_spec.rb => dsl/generator_spec.rb} | 99 +++++++------- spec/stroma/hooks/applier_spec.rb | 86 +++++------- spec/stroma/hooks/factory_spec.rb | 17 ++- spec/stroma/matrix_spec.rb | 75 +++++++++++ spec/stroma/registry_spec.rb | 116 +++++++++++----- spec/support/stroma_test_registry.rb | 10 +- 13 files changed, 476 insertions(+), 387 deletions(-) delete mode 100644 lib/stroma/dsl.rb create mode 100644 lib/stroma/dsl/generator.rb create mode 100644 lib/stroma/matrix.rb rename spec/stroma/{dsl_spec.rb => dsl/generator_spec.rb} (61%) create mode 100644 spec/stroma/matrix_spec.rb diff --git a/README.md b/README.md index 6c17651..83d2b4b 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ Building modular DSLs shouldn't require reinventing the wheel. Stroma provides a Stroma is a foundation for library authors building DSL-driven frameworks (service objects, form objects, decorators, etc.). **Core lifecycle:** -1. **Register** - Define DSL modules at boot time via `Stroma::Registry` -2. **Compose** - Classes include `Stroma::DSL` to gain all registered modules automatically -3. **Extend** (optional) - Users can add cross-cutting logic via `before`/`after` hooks +1. **Define** - Create a Matrix with DSL modules at boot time +2. **Include** - Classes include the matrix's DSL to gain all modules +3. **Extend** (optional) - Add cross-cutting logic via `before`/`after` hooks ## 🚀 Quick Start @@ -55,16 +55,11 @@ spec.add_dependency "stroma", ">= 0.3" ```ruby module MyLib - module DSL - # Register DSL modules at load time - Stroma::Registry.register(:inputs, MyLib::Inputs::DSL) - Stroma::Registry.register(:actions, MyLib::Actions::DSL) - Stroma::Registry.finalize! - - def self.included(base) - base.include(Stroma::DSL) - end + STROMA = Stroma::Matrix.new(:my_lib) do + register :inputs, MyLib::Inputs::DSL + register :actions, MyLib::Actions::DSL end + private_constant :STROMA end ``` @@ -73,7 +68,7 @@ end ```ruby module MyLib class Base - include MyLib::DSL + include STROMA.dsl end end ``` diff --git a/lib/stroma/dsl.rb b/lib/stroma/dsl.rb deleted file mode 100644 index b1d4185..0000000 --- a/lib/stroma/dsl.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Stroma - # Main integration point between Stroma and service classes. - # - # ## Purpose - # - # Module that provides the core Stroma functionality to service classes: - # - Includes all registered DSL modules - # - Provides extensions block for hook registration - # - Handles inheritance with proper state copying - # - # ## Usage - # - # Library authors create a DSL module that includes Stroma::DSL: - # - # ```ruby - # module MyLib::DSL - # def self.included(base) - # base.include(Stroma::DSL) - # end - # end - # - # class MyLib::Base - # include MyLib::DSL - # - # extensions do - # before :actions, MyExtension - # end - # end - # ``` - # - # ## Extension Settings Access - # - # Extensions access their settings through the stroma.settings hierarchy: - # - # ```ruby - # # In ClassMethods: - # stroma.settings[:actions][:authorization][:method_name] = :authorize - # - # # In InstanceMethods: - # self.class.stroma.settings[:actions][:authorization][:method_name] - # ``` - # - # ## Integration - # - # Included by service classes that want Stroma hook functionality. - # Provides ClassMethods with: stroma, inherited, extensions. - module DSL - def self.included(base) - base.extend(ClassMethods) - - Registry.entries.each do |entry| - base.include(entry.extension) - end - end - - # Class-level methods for Stroma integration. - # - # ## Purpose - # - # Provides access to Stroma state and hooks DSL at the class level. - # Handles proper duplication during inheritance. - # - # ## Key Methods - # - # - `stroma` - Access the State container - # - `inherited` - Copy state to child classes - # - `extensions` - DSL block for hook registration - module ClassMethods - def self.extended(base) - base.instance_variable_set(:@stroma, State.new) - end - - # Handles inheritance by duplicating Stroma state. - # - # Creates an independent copy of hooks and settings for the child class, - # then applies all registered hooks to the child. - # - # @param child [Class] The child class being created - # @return [void] - def inherited(child) - super - - child.instance_variable_set(:@stroma, stroma.dup) - - Hooks::Applier.new(child, child.stroma.hooks).apply! - end - - # Returns the Stroma state for this service class. - # - # @return [State] The Stroma state container - # - # @example Accessing hooks - # stroma.hooks.before(:actions) - # - # @example Accessing settings - # stroma.settings[:actions][:authorization][:method_name] - def stroma - @stroma ||= State.new - end - - private - - # DSL block for registering hooks. - # - # Evaluates the block in the context of a Hooks::Factory, - # allowing before/after hook registration. - # - # @yield Block with before/after DSL calls - # @return [void] - # - # @example - # extensions do - # before :actions, AuthorizationExtension - # after :outputs, LoggingExtension - # end - def extensions(&block) - @stroma_hooks_factory ||= Hooks::Factory.new(stroma.hooks) - @stroma_hooks_factory.instance_eval(&block) - end - end - end -end diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb new file mode 100644 index 0000000..5693601 --- /dev/null +++ b/lib/stroma/dsl/generator.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Stroma + module DSL + # Generates a DSL module scoped to a specific Matrix. + # + # ## Design + # + # The generated module: + # - Stores matrix reference on the module itself (via @stroma_matrix) + # - Uses ClassMethods module defined via const_set + # - Properly handles inheritance with state duplication + # + # ## Memory Model + # + # ``` + # Matrix (frozen) + # └── @dsl_module (generated Module, cached) + # ├── @stroma_matrix → Matrix + # └── ClassMethods module + # + # ServiceClass (includes dsl) + # ├── @stroma_matrix → Matrix (same reference) + # └── @stroma → State (unique per class) + # ├── hooks → Collection (deep copied on inherit) + # └── settings → Collection (deep copied on inherit) + # ``` + # + # ## Boot vs Runtime + # + # Boot time (one-time): + # - Generator.call creates Module.new + # - ClassMethods module defined inside via const_set + # - First include sets up base class + # + # Runtime (no allocations): + # - stroma_matrix returns cached @stroma_matrix + # - stroma returns cached @stroma + # - extensions block only called at class definition time + class Generator + class << self + def call(matrix) + new(matrix).generate + end + end + + def initialize(matrix) + @matrix = matrix + end + + def generate + matrix = @matrix + class_methods = build_class_methods + + Module.new do + @stroma_matrix = matrix + + class << self + attr_reader :stroma_matrix + + def included(base) + mtx = stroma_matrix + base.extend(self::ClassMethods) + base.instance_variable_set(:@stroma_matrix, mtx) + base.instance_variable_set(:@stroma, State.new) + + mtx.entries.each { |entry| base.include(entry.extension) } + end + end + + const_set(:ClassMethods, class_methods) + end + end + + private + + def build_class_methods + Module.new do + def stroma_matrix + @stroma_matrix + end + + def stroma + @stroma ||= State.new + end + + def inherited(child) + super + child.instance_variable_set(:@stroma_matrix, stroma_matrix) + child.instance_variable_set(:@stroma, stroma.dup) + Hooks::Applier.new(child, child.stroma.hooks, stroma_matrix).apply! + end + + private + + def extensions(&block) + @stroma_hooks_factory ||= Hooks::Factory.new(stroma.hooks, stroma_matrix) + @stroma_hooks_factory.instance_eval(&block) + end + end + end + end + end +end diff --git a/lib/stroma/hooks/applier.rb b/lib/stroma/hooks/applier.rb index d6449a7..c20871f 100644 --- a/lib/stroma/hooks/applier.rb +++ b/lib/stroma/hooks/applier.rb @@ -4,33 +4,21 @@ module Stroma module Hooks # Applies registered hooks to a target class. # - # ## Purpose + # Uses the matrix's registry entries to determine hook order. # - # Iterates through all registered DSL modules and includes corresponding - # before/after hooks in the target class. For each registry entry, - # before hooks are included first, then after hooks. - # - # ## Usage - # - # ```ruby - # applier = Stroma::Hooks::Applier.new(ChildService, hooks) - # applier.apply! - # # ChildService now includes all hook modules - # ``` - # - # ## Integration - # - # Called by Stroma::DSL.inherited after duplicating - # parent's configuration. Uses Registry.entries to determine - # hook application order. + # @example + # applier = Applier.new(ChildService, hooks, matrix) + # applier.apply! class Applier # Creates a new applier for applying hooks to a class. # # @param target_class [Class] The class to apply hooks to # @param hooks [Collection] The hooks collection to apply - def initialize(target_class, hooks) + # @param matrix [Matrix] The matrix providing registry entries + def initialize(target_class, hooks, matrix) @target_class = target_class @hooks = hooks + @matrix = matrix end # Applies all registered hooks to the target class. @@ -39,21 +27,12 @@ def initialize(target_class, hooks) # then after hooks. Does nothing if hooks collection is empty. # # @return [void] - # - # @example - # applier.apply! - # # Target class now includes all extension modules def apply! return if @hooks.empty? - Registry.entries.each do |entry| - @hooks.before(entry.key).each do |hook| - @target_class.include(hook.extension) - end - - @hooks.after(entry.key).each do |hook| - @target_class.include(hook.extension) - end + @matrix.entries.each do |entry| + @hooks.before(entry.key).each { |hook| @target_class.include(hook.extension) } + @hooks.after(entry.key).each { |hook| @target_class.include(hook.extension) } end end end diff --git a/lib/stroma/hooks/factory.rb b/lib/stroma/hooks/factory.rb index 85d8beb..48c4eda 100644 --- a/lib/stroma/hooks/factory.rb +++ b/lib/stroma/hooks/factory.rb @@ -4,39 +4,20 @@ module Stroma module Hooks # DSL interface for registering hooks in extensions block. # - # ## Purpose + # Validates target keys against the matrix's registry. # - # Provides the `before` and `after` methods used within the extensions - # block to register hooks. Validates that target keys exist in Registry - # before adding hooks. - # - # ## Usage - # - # Used within `extensions` block in classes that include Stroma::DSL: - # - # ```ruby - # # Library Base class (includes Stroma::DSL via library's DSL module) - # class MyLib::Base - # include MyLib::DSL # MyLib::DSL includes Stroma::DSL - # - # extensions do - # before :actions, ValidationModule - # after :outputs, LoggingModule - # end - # end - # ``` - # - # ## Integration - # - # Created by DSL.extensions method and receives instance_eval of the block. - # Validates keys against Registry.keys and raises UnknownHookTarget - # for invalid keys. + # @example + # factory = Factory.new(hooks, matrix) + # factory.before(:actions, ValidationModule) + # factory.after(:outputs, LoggingModule) class Factory # Creates a new factory for registering hooks. # # @param hooks [Collection] The hooks collection to add to - def initialize(hooks) + # @param matrix [Matrix] The matrix providing valid keys + def initialize(hooks, matrix) @hooks = hooks + @matrix = matrix end # Registers one or more before hooks for a target key. @@ -49,7 +30,7 @@ def initialize(hooks) # before :actions, ValidationModule, AuthorizationModule def before(key, *extensions) validate_key!(key) - extensions.each { |extension| @hooks.add(:before, key, extension) } + extensions.each { |ext| @hooks.add(:before, key, ext) } end # Registers one or more after hooks for a target key. @@ -62,21 +43,21 @@ def before(key, *extensions) # after :outputs, LoggingModule, AuditModule def after(key, *extensions) validate_key!(key) - extensions.each { |extension| @hooks.add(:after, key, extension) } + extensions.each { |ext| @hooks.add(:after, key, ext) } end private - # Validates that the key exists in the Registry. + # Validates that the key exists in the matrix's registry. # # @param key [Symbol] The key to validate # @raise [Exceptions::UnknownHookTarget] If key is not registered def validate_key!(key) - return if Registry.key?(key) + return if @matrix.key?(key) raise Exceptions::UnknownHookTarget, - "Unknown hook target: #{key.inspect}. " \ - "Valid keys: #{Registry.keys.map(&:inspect).join(', ')}" + "Unknown hook target #{key.inspect} for #{@matrix.name.inspect}. " \ + "Valid: #{@matrix.keys.map(&:inspect).join(', ')}" end end end diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb new file mode 100644 index 0000000..adfa8bc --- /dev/null +++ b/lib/stroma/matrix.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Stroma + # Main entry point for libraries using Stroma. + # + # Creates an isolated registry and generates a scoped DSL module. + # Each matrix has its own registry - no conflicts with other libraries. + # + # ## Lifecycle + # + # 1. Boot time: Matrix.new creates Registry, registers extensions + # 2. Boot time: finalize! freezes registry, freeze freezes Matrix + # 3. Boot time: First include calls dsl, which generates and caches Module + # 4. Runtime: All structures frozen, no allocations + # + # @example + # module MyLib + # STROMA = Stroma::Matrix.new(:my_lib) do + # register :inputs, Inputs::DSL + # register :outputs, Outputs::DSL + # end + # private_constant :STROMA + # end + # + # class MyLib::Base + # include MyLib::STROMA.dsl + # end + class Matrix + attr_reader :name, :registry + + def initialize(name, &block) + @name = name.to_sym + @registry = Registry.new(@name) + @dsl_module = nil + + instance_eval(&block) if block_given? + @registry.finalize! + dsl # Eager generation before freeze + freeze + end + + def register(key, extension) + @registry.register(key, extension) + end + + def dsl + @dsl_module ||= DSL::Generator.call(self) + end + + def entries + registry.entries + end + + def keys + registry.keys + end + + def key?(key) + registry.key?(key) + end + end +end diff --git a/lib/stroma/registry.rb b/lib/stroma/registry.rb index 20d39e8..4e094a4 100644 --- a/lib/stroma/registry.rb +++ b/lib/stroma/registry.rb @@ -1,58 +1,33 @@ # frozen_string_literal: true module Stroma - # Manages global registration of DSL modules for Stroma. + # Manages registration of DSL modules for a specific matrix. # - # ## Purpose + # Each Matrix instance has its own Registry - no global state. + # Implements two-phase lifecycle: registration → finalization. # - # Singleton registry that stores all DSL modules that will be included - # in service classes. Implements two-phase lifecycle: registration - # followed by finalization. - # - # ## Usage - # - # ```ruby - # # During gem initialization: - # Stroma::Registry.register(:inputs, Inputs::DSL) - # Stroma::Registry.register(:outputs, Outputs::DSL) - # Stroma::Registry.finalize! - # - # # After finalization: - # Stroma::Registry.keys # => [:inputs, :outputs] - # Stroma::Registry.key?(:inputs) # => true - # ``` - # - # ## Integration - # - # Used by Stroma::DSL to include all registered modules in service classes. - # Used by Stroma::Hooks::Factory to validate hook target keys. - # - # ## Thread Safety - # - # Registration must occur during single-threaded boot phase. - # After finalization, all read operations are thread-safe. + # @example + # registry = Stroma::Registry.new(:my_lib) + # registry.register(:inputs, Inputs::DSL) + # registry.finalize! + # registry.keys # => [:inputs] class Registry - include Singleton - - class << self - delegate :register, - :finalize!, - :entries, - :keys, - :key?, - to: :instance - end + attr_reader :matrix_name - def initialize + def initialize(matrix_name) + @matrix_name = matrix_name.to_sym @entries = [] @finalized = false end def register(key, extension) - raise Exceptions::RegistryFrozen, "Registry is finalized" if @finalized + raise Exceptions::RegistryFrozen, + "Registry for #{@matrix_name.inspect} is finalized" if @finalized + key = key.to_sym if @entries.any? { |e| e.key == key } - raise Exceptions::KeyAlreadyRegistered, "Key #{key.inspect} already registered" + raise Exceptions::KeyAlreadyRegistered, + "Key #{key.inspect} already registered in #{@matrix_name.inspect}" end @entries << Entry.new(key:, extension:) @@ -77,7 +52,7 @@ def keys def key?(key) ensure_finalized! - @entries.any? { |e| e.key == key } + @entries.any? { |e| e.key == key.to_sym } end private @@ -86,7 +61,7 @@ def ensure_finalized! return if @finalized raise Exceptions::RegistryNotFinalized, - "Registry not finalized. Call Stroma::Registry.finalize! after registration." + "Registry for #{@matrix_name.inspect} not finalized" end end end diff --git a/spec/stroma/dsl_spec.rb b/spec/stroma/dsl/generator_spec.rb similarity index 61% rename from spec/stroma/dsl_spec.rb rename to spec/stroma/dsl/generator_spec.rb index 9c78617..fe9e2c3 100644 --- a/spec/stroma/dsl_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -1,30 +1,54 @@ # frozen_string_literal: true -RSpec.describe Stroma::DSL do - describe ".included" do +RSpec.describe Stroma::DSL::Generator do + let(:inputs_dsl) { Module.new } + let(:outputs_dsl) { Module.new } + + let(:matrix) do + inputs = inputs_dsl + outputs = outputs_dsl + Stroma::Matrix.new(:test) do + register :inputs, inputs + register :outputs, outputs + end + end + + describe ".call" do + let(:dsl_module) { described_class.call(matrix) } + + it "returns a module" do + expect(dsl_module).to be_a(Module) + end + + it "stores matrix reference" do + expect(dsl_module.stroma_matrix).to eq(matrix) + end + end + + describe "generated module" do let(:base_class) do - Class.new do - include Stroma::DSL - end + mtx = matrix + Class.new { include mtx.dsl } end - it "extends the class with ClassMethods", :aggregate_failures do + it "extends class with ClassMethods", :aggregate_failures do expect(base_class).to respond_to(:stroma) + expect(base_class).to respond_to(:stroma_matrix) expect(base_class).to respond_to(:inherited) end it "includes all registered DSL modules" do - Stroma::Registry.entries.each do |entry| - expect(base_class.ancestors).to( - include(entry.extension), - "Expected ancestors to include #{entry.extension} (key: #{entry.key})" - ) - end + expect(base_class.ancestors).to include(inputs_dsl) + expect(base_class.ancestors).to include(outputs_dsl) end - it "creates a stroma state" do + it "creates stroma state" do expect(base_class.stroma).to be_a(Stroma::State) end + + it "stores matrix reference on class" do + expect(base_class.stroma_matrix).to eq(matrix) + end end describe "inheritance" do @@ -37,12 +61,13 @@ def self.included(base) end let(:base_class) do + mtx = matrix ext = extension_module Class.new do - include Stroma::DSL + include mtx.dsl extensions do - before :actions, ext + before :inputs, ext end end end @@ -63,35 +88,8 @@ def self.included(base) expect(child_class.stroma).to be_a(Stroma::State) end - it "hooks are inherited" do - grandchild = Class.new(child_class) - expect(grandchild.ancestors).to include(extension_module) - end - end - - describe "#extensions" do - let(:first_module) { Module.new } - let(:second_module) { Module.new } - - let(:base_class) do - first_ext = first_module - second_ext = second_module - Class.new do - include Stroma::DSL - - extensions do - before :actions, first_ext - after :outputs, second_ext - end - end - end - - it "registers before hooks" do - expect(base_class.stroma.hooks.before(:actions).size).to eq(1) - end - - it "registers after hooks" do - expect(base_class.stroma.hooks.after(:outputs).size).to eq(1) + it "preserves matrix reference" do + expect(child_class.stroma_matrix).to eq(matrix) end end @@ -99,12 +97,13 @@ def self.included(base) let(:extension_module) { Module.new } let(:parent_class) do + mtx = matrix ext = extension_module Class.new do - include Stroma::DSL + include mtx.dsl extensions do - before :actions, ext + before :inputs, ext end end end @@ -125,21 +124,21 @@ def self.included(base) end it "child inherits parent hooks", :aggregate_failures do - expect(child_class.stroma.hooks.before(:actions).size).to eq(1) + expect(child_class.stroma.hooks.before(:inputs).size).to eq(1) expect(child_class.ancestors).to include(extension_module) end it "parent modifications after child creation do not affect child" do - child_before_count = child_class.stroma.hooks.before(:inputs).size + child_before_count = child_class.stroma.hooks.before(:outputs).size new_extension = Module.new parent_class.class_eval do extensions do - before :inputs, new_extension + before :outputs, new_extension end end - expect(child_class.stroma.hooks.before(:inputs).size).to eq(child_before_count) + expect(child_class.stroma.hooks.before(:outputs).size).to eq(child_before_count) end end end diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index f8572b9..d95252c 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -1,83 +1,69 @@ # frozen_string_literal: true RSpec.describe Stroma::Hooks::Applier do + let(:inputs_dsl) { Module.new } + let(:outputs_dsl) { Module.new } + let(:matrix) do + inputs = inputs_dsl + outputs = outputs_dsl + Stroma::Matrix.new(:test) do + register :inputs, inputs + register :outputs, outputs + end + end + let(:hooks) { Stroma::Hooks::Collection.new } let(:target_class) { Class.new } - let(:applier) { described_class.new(target_class, hooks) } + let(:applier) { described_class.new(target_class, hooks, matrix) } describe "#apply!" do - context "when hooks are empty" do - it "does nothing" do - allow(target_class).to receive(:include) - applier.apply! - expect(target_class).not_to have_received(:include) - end + it "does nothing when hooks empty" do + applier.apply! + expect(target_class.ancestors).not_to include(inputs_dsl) end - context "when hooks are present" do - let(:before_module) { Module.new } - let(:after_module) { Module.new } + context "with before hooks" do + let(:before_extension) { Module.new } before do - hooks.add(:before, :actions, before_module) - hooks.add(:after, :outputs, after_module) - end - - it "includes before hooks" do - applier.apply! - expect(target_class.ancestors).to include(before_module) + hooks.add(:before, :inputs, before_extension) end - it "includes after hooks" do + it "includes before hook extension" do applier.apply! - expect(target_class.ancestors).to include(after_module) + expect(target_class.ancestors).to include(before_extension) end end - context "with multiple hooks for same key" do - let(:first_module) { Module.new } - let(:second_module) { Module.new } + context "with after hooks" do + let(:after_extension) { Module.new } before do - hooks.add(:before, :actions, first_module) - hooks.add(:before, :actions, second_module) + hooks.add(:after, :outputs, after_extension) end - it "includes all hooks in order" do + it "includes after hook extension" do applier.apply! - expect(target_class.ancestors).to include(first_module, second_module) + expect(target_class.ancestors).to include(after_extension) end end - context "with before and after hooks for same key" do - let(:inclusion_order) { [] } - - let(:before_module) do - order = inclusion_order - Module.new do - define_singleton_method(:included) do |_base| - order << :before - end - end - end - - let(:after_module) do - order = inclusion_order - Module.new do - define_singleton_method(:included) do |_base| - order << :after - end - end - end + context "with multiple hooks" do + let(:before_inputs) { Module.new } + let(:after_inputs) { Module.new } + let(:before_outputs) { Module.new } before do - hooks.add(:before, :actions, before_module) - hooks.add(:after, :actions, after_module) + hooks.add(:before, :inputs, before_inputs) + hooks.add(:after, :inputs, after_inputs) + hooks.add(:before, :outputs, before_outputs) end - it "includes before hooks before after hooks" do + it "applies all hooks", :aggregate_failures do applier.apply! - expect(inclusion_order).to eq(%i[before after]) + expect(target_class.ancestors).to include(before_inputs) + expect(target_class.ancestors).to include(after_inputs) + expect(target_class.ancestors).to include(before_outputs) end end end diff --git a/spec/stroma/hooks/factory_spec.rb b/spec/stroma/hooks/factory_spec.rb index 786bd5d..5e3789a 100644 --- a/spec/stroma/hooks/factory_spec.rb +++ b/spec/stroma/hooks/factory_spec.rb @@ -2,7 +2,14 @@ RSpec.describe Stroma::Hooks::Factory do let(:hooks) { Stroma::Hooks::Collection.new } - let(:factory) { described_class.new(hooks) } + let(:matrix) do + Stroma::Matrix.new(:test) do + register :inputs, Module.new + register :outputs, Module.new + register :actions, Module.new + end + end + let(:factory) { described_class.new(hooks, matrix) } let(:first_module) { Module.new } let(:second_module) { Module.new } @@ -21,8 +28,7 @@ it "raises UnknownHookTarget for unknown key" do expect { factory.before(:unknown, first_module) }.to raise_error( Stroma::Exceptions::UnknownHookTarget, - "Unknown hook target: :unknown. " \ - "Valid keys: :configuration, :info, :context, :inputs, :internals, :outputs, :actions" + "Unknown hook target :unknown for :test. Valid: :inputs, :outputs, :actions" ) end end @@ -42,15 +48,14 @@ it "raises UnknownHookTarget for unknown key" do expect { factory.after(:unknown, first_module) }.to raise_error( Stroma::Exceptions::UnknownHookTarget, - "Unknown hook target: :unknown. " \ - "Valid keys: :configuration, :info, :context, :inputs, :internals, :outputs, :actions" + "Unknown hook target :unknown for :test. Valid: :inputs, :outputs, :actions" ) end end describe "valid keys" do it "accepts all registered keys" do - %i[configuration info context inputs internals outputs actions].each do |key| + %i[inputs outputs actions].each do |key| expect { factory.before(key, first_module) }.not_to raise_error end end diff --git a/spec/stroma/matrix_spec.rb b/spec/stroma/matrix_spec.rb new file mode 100644 index 0000000..fd48d61 --- /dev/null +++ b/spec/stroma/matrix_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +RSpec.describe Stroma::Matrix do + describe "#initialize" do + it "creates a frozen matrix with name" do + matrix = described_class.new(:test) do + register :inputs, Module.new + end + + expect(matrix.name).to eq(:test) + expect(matrix).to be_frozen + end + + it "finalizes registry automatically" do + matrix = described_class.new(:test) do + register :inputs, Module.new + end + + expect(matrix.registry).to be_a(Stroma::Registry) + expect(matrix.keys).to eq([:inputs]) + end + end + + describe "#register" do + it "delegates to registry" do + matrix = described_class.new(:test) do + register :inputs, Module.new + register :outputs, Module.new + end + + expect(matrix.keys).to eq(%i[inputs outputs]) + end + end + + describe "#dsl" do + let(:matrix) do + described_class.new(:test) do + register :inputs, Module.new + end + end + + it "returns a module" do + expect(matrix.dsl).to be_a(Module) + end + + it "caches the module" do + expect(matrix.dsl).to be(matrix.dsl) + end + end + + describe "isolation" do + let(:matrix_a) do + described_class.new(:lib_a) do + register :inputs, Module.new + register :outputs, Module.new + end + end + + let(:matrix_b) do + described_class.new(:lib_b) do + register :inputs, Module.new + register :events, Module.new + end + end + + it "has independent registries", :aggregate_failures do + expect(matrix_a.keys).to eq(%i[inputs outputs]) + expect(matrix_b.keys).to eq(%i[inputs events]) + end + + it "allows same keys in different matrices" do + expect { matrix_a; matrix_b }.not_to raise_error + end + end +end diff --git a/spec/stroma/registry_spec.rb b/spec/stroma/registry_spec.rb index 5ce9c64..bf25a3d 100644 --- a/spec/stroma/registry_spec.rb +++ b/spec/stroma/registry_spec.rb @@ -1,59 +1,109 @@ # frozen_string_literal: true RSpec.describe Stroma::Registry do - # NOTE: Registry is a Singleton and is already populated by StromaTestRegistry. - # We test using the already-finalized registry. - - describe ".entries" do - it "returns all registered entries", :aggregate_failures do - expect(described_class.entries).to be_an(Array) - expect(described_class.entries).not_to be_empty + describe "#initialize" do + it "creates registry with matrix name" do + registry = described_class.new(:test) + expect(registry.matrix_name).to eq(:test) end + end + + describe "#register" do + let(:registry) { described_class.new(:test) } - it "contains DSL modules in order" do - keys = described_class.entries.map(&:key) - expect(keys).to eq(%i[configuration info context inputs internals outputs actions]) + it "adds entry to registry" do + extension = Module.new + registry.register(:inputs, extension) + registry.finalize! + + expect(registry.keys).to eq([:inputs]) end - end - describe ".keys" do - it "returns all registered keys" do - expect(described_class.keys).to eq(%i[configuration info context inputs internals outputs actions]) + it "raises KeyAlreadyRegistered for duplicate key" do + registry.register(:inputs, Module.new) + + expect { registry.register(:inputs, Module.new) }.to raise_error( + Stroma::Exceptions::KeyAlreadyRegistered, + "Key :inputs already registered in :test" + ) end - end - describe ".register" do - it "raises RegistryFrozen when registry is finalized" do - expect { described_class.register(:test, Module.new) }.to raise_error( + it "raises RegistryFrozen when finalized" do + registry.finalize! + + expect { registry.register(:test, Module.new) }.to raise_error( Stroma::Exceptions::RegistryFrozen, - "Registry is finalized" + "Registry for :test is finalized" ) end end - describe ".key?" do - it "returns true for registered key" do - expect(described_class.key?(:inputs)).to be(true) + describe "#finalize!" do + let(:registry) { described_class.new(:test) } + + it "is idempotent" do + registry.register(:inputs, Module.new) + registry.finalize! + + expect { registry.finalize! }.not_to raise_error end - it "returns true for all registered keys" do - %i[configuration info context inputs internals outputs actions].each do |key| - expect(described_class.key?(key)).to be(true) - end + it "freezes entries" do + registry.register(:inputs, Module.new) + registry.finalize! + + expect(registry.entries).to be_frozen end + end - it "returns false for unregistered key" do - expect(described_class.key?(:unknown)).to be(false) + describe "#entries" do + let(:registry) { described_class.new(:test) } + + it "raises RegistryNotFinalized before finalize" do + registry.register(:inputs, Module.new) + + expect { registry.entries }.to raise_error( + Stroma::Exceptions::RegistryNotFinalized, + "Registry for :test not finalized" + ) + end + + it "returns entries after finalize" do + extension = Module.new + registry.register(:inputs, extension) + registry.finalize! + + expect(registry.entries.first.key).to eq(:inputs) + expect(registry.entries.first.extension).to eq(extension) end + end + + describe "#keys" do + let(:registry) { described_class.new(:test) } + + it "returns all registered keys" do + registry.register(:inputs, Module.new) + registry.register(:outputs, Module.new) + registry.finalize! - it "returns false for nil" do - expect(described_class.key?(nil)).to be(false) + expect(registry.keys).to eq(%i[inputs outputs]) end end - describe ".finalize!" do - it "is idempotent - can be called multiple times" do - expect { described_class.finalize! }.not_to raise_error + describe "#key?" do + let(:registry) { described_class.new(:test) } + + before do + registry.register(:inputs, Module.new) + registry.finalize! + end + + it "returns true for registered key" do + expect(registry.key?(:inputs)).to be(true) + end + + it "returns false for unregistered key" do + expect(registry.key?(:unknown)).to be(false) end end end diff --git a/spec/support/stroma_test_registry.rb b/spec/support/stroma_test_registry.rb index 20513a5..a686a20 100644 --- a/spec/support/stroma_test_registry.rb +++ b/spec/support/stroma_test_registry.rb @@ -12,11 +12,13 @@ module StromaTestRegistry }.freeze def self.setup! - return if Stroma::Registry.instance.instance_variable_get(:@finalized) + # No global setup needed - Matrix is created per-test + end - MOCK_MODULES.each do |key, mod| - Stroma::Registry.register(key, mod) + def self.create_matrix(name = :test, keys: MOCK_MODULES.keys) + modules = MOCK_MODULES + Stroma::Matrix.new(name) do + keys.each { |key| register(key, modules[key]) } end - Stroma::Registry.finalize! end end From 4aefbf7ebf438c297d3a68d3603ce0ca1c6f593b Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:18:15 +0700 Subject: [PATCH 02/11] Enhance documentation for Matrix, Hooks, and Registry - Add detailed code comments and documentation for `Stroma::Matrix`, `Hooks::Applier`, `Hooks::Factory`, `DSL::Generator`, and `Registry`. - Expand examples and usage details in comments. - Clarify lifecycle, purpose, and integration of Matrix-scoped DSL components throughout the library. --- lib/stroma/dsl/generator.rb | 66 ++++++++++++++++++++-------------- lib/stroma/hooks/applier.rb | 21 ++++++++--- lib/stroma/hooks/factory.rb | 26 +++++++++++--- lib/stroma/matrix.rb | 70 +++++++++++++++++++++++++++++-------- lib/stroma/registry.rb | 60 +++++++++++++++++++++++++++---- 5 files changed, 188 insertions(+), 55 deletions(-) diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb index 5693601..11642d7 100644 --- a/lib/stroma/dsl/generator.rb +++ b/lib/stroma/dsl/generator.rb @@ -4,50 +4,61 @@ module Stroma module DSL # Generates a DSL module scoped to a specific Matrix. # - # ## Design + # ## Purpose # - # The generated module: - # - Stores matrix reference on the module itself (via @stroma_matrix) - # - Uses ClassMethods module defined via const_set - # - Properly handles inheritance with state duplication + # Creates a module that: + # - Stores matrix reference on the module itself + # - Defines ClassMethods for service classes + # - Handles inheritance with state duplication # - # ## Memory Model + # Memory model: + # - Matrix owns @dsl_module (generated once, cached) + # - ServiceClass gets @stroma_matrix (same reference) + # - ServiceClass gets @stroma (unique State per class) # - # ``` - # Matrix (frozen) - # └── @dsl_module (generated Module, cached) - # ├── @stroma_matrix → Matrix - # └── ClassMethods module + # ## Usage # - # ServiceClass (includes dsl) - # ├── @stroma_matrix → Matrix (same reference) - # └── @stroma → State (unique per class) - # ├── hooks → Collection (deep copied on inherit) - # └── settings → Collection (deep copied on inherit) - # ``` + # ```ruby + # # Called internally by Matrix#dsl + # dsl_module = Stroma::DSL::Generator.call(matrix) # - # ## Boot vs Runtime + # # The generated module is included in base classes + # class MyLib::Base + # include dsl_module + # end + # ``` # - # Boot time (one-time): - # - Generator.call creates Module.new - # - ClassMethods module defined inside via const_set - # - First include sets up base class + # ## Integration # - # Runtime (no allocations): - # - stroma_matrix returns cached @stroma_matrix - # - stroma returns cached @stroma - # - extensions block only called at class definition time + # Called by Matrix#dsl to generate the DSL module. + # Generated module includes all registered extensions. class Generator class << self + # Generates a DSL module for the given matrix. + # + # @param matrix [Matrix] The matrix to generate DSL for + # @return [Module] The generated DSL module def call(matrix) new(matrix).generate end end + # Creates a new generator for the given matrix. + # + # @param matrix [Matrix] The matrix to generate DSL for def initialize(matrix) @matrix = matrix end + # Generates the DSL module. + # + # Creates a module with ClassMethods that provides: + # - stroma_matrix accessor for matrix reference + # - stroma accessor for per-class state + # - inherited hook for state duplication + # - extensions DSL for registering hooks + # + # @return [Module] The generated DSL module def generate matrix = @matrix class_methods = build_class_methods @@ -74,6 +85,9 @@ def included(base) private + # Builds the ClassMethods module. + # + # @return [Module] The ClassMethods module def build_class_methods Module.new do def stroma_matrix diff --git a/lib/stroma/hooks/applier.rb b/lib/stroma/hooks/applier.rb index c20871f..4925885 100644 --- a/lib/stroma/hooks/applier.rb +++ b/lib/stroma/hooks/applier.rb @@ -4,11 +4,24 @@ module Stroma module Hooks # Applies registered hooks to a target class. # - # Uses the matrix's registry entries to determine hook order. + # ## Purpose # - # @example - # applier = Applier.new(ChildService, hooks, matrix) - # applier.apply! + # Includes hook extension modules into target class. + # Maintains order based on matrix registry entries. + # For each entry: before hooks first, then after hooks. + # + # ## Usage + # + # ```ruby + # # Called internally during class inheritance + # applier = Stroma::Hooks::Applier.new(ChildService, hooks, matrix) + # applier.apply! + # ``` + # + # ## Integration + # + # Called by DSL::Generator's inherited hook. + # Creates a temporary instance that is garbage collected after apply!. class Applier # Creates a new applier for applying hooks to a class. # diff --git a/lib/stroma/hooks/factory.rb b/lib/stroma/hooks/factory.rb index 48c4eda..9cb235c 100644 --- a/lib/stroma/hooks/factory.rb +++ b/lib/stroma/hooks/factory.rb @@ -4,12 +4,27 @@ module Stroma module Hooks # DSL interface for registering hooks in extensions block. # + # ## Purpose + # + # Provides before/after DSL methods for hook registration. # Validates target keys against the matrix's registry. + # Delegates to Hooks::Collection for storage. + # + # ## Usage + # + # ```ruby + # class MyService < MyLib::Base + # extensions do + # before :actions, ValidationModule, AuthModule + # after :outputs, LoggingModule + # end + # end + # ``` + # + # ## Integration # - # @example - # factory = Factory.new(hooks, matrix) - # factory.before(:actions, ValidationModule) - # factory.after(:outputs, LoggingModule) + # Created by DSL::Generator's extensions method. + # Cached as @stroma_hooks_factory on each service class. class Factory # Creates a new factory for registering hooks. # @@ -25,6 +40,7 @@ def initialize(hooks, matrix) # @param key [Symbol] The registry key to hook before # @param extensions [Array] Extension modules to include # @raise [Exceptions::UnknownHookTarget] If key is not registered + # @return [void] # # @example # before :actions, ValidationModule, AuthorizationModule @@ -38,6 +54,7 @@ def before(key, *extensions) # @param key [Symbol] The registry key to hook after # @param extensions [Array] Extension modules to include # @raise [Exceptions::UnknownHookTarget] If key is not registered + # @return [void] # # @example # after :outputs, LoggingModule, AuditModule @@ -52,6 +69,7 @@ def after(key, *extensions) # # @param key [Symbol] The key to validate # @raise [Exceptions::UnknownHookTarget] If key is not registered + # @return [void] def validate_key!(key) return if @matrix.key?(key) diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb index adfa8bc..1aa6dfe 100644 --- a/lib/stroma/matrix.rb +++ b/lib/stroma/matrix.rb @@ -3,31 +3,51 @@ module Stroma # Main entry point for libraries using Stroma. # + # ## Purpose + # # Creates an isolated registry and generates a scoped DSL module. # Each matrix has its own registry - no conflicts with other libraries. # - # ## Lifecycle + # Lifecycle: + # - Boot time: Matrix.new creates Registry, registers extensions + # - Boot time: finalize! freezes registry, dsl generates Module + # - Boot time: freeze makes Matrix immutable + # - Runtime: All structures frozen, no allocations # - # 1. Boot time: Matrix.new creates Registry, registers extensions - # 2. Boot time: finalize! freezes registry, freeze freezes Matrix - # 3. Boot time: First include calls dsl, which generates and caches Module - # 4. Runtime: All structures frozen, no allocations + # ## Usage # - # @example - # module MyLib - # STROMA = Stroma::Matrix.new(:my_lib) do - # register :inputs, Inputs::DSL - # register :outputs, Outputs::DSL - # end - # private_constant :STROMA + # ```ruby + # module MyLib + # STROMA = Stroma::Matrix.new(:my_lib) do + # register :inputs, Inputs::DSL + # register :outputs, Outputs::DSL # end + # private_constant :STROMA + # end # - # class MyLib::Base - # include MyLib::STROMA.dsl - # end + # class MyLib::Base + # include MyLib::STROMA.dsl + # end + # ``` + # + # ## Integration + # + # Stored as a constant in the library's namespace. + # Owns the Registry and generates DSL module via DSL::Generator. class Matrix + # @!attribute [r] name + # @return [Symbol] The matrix identifier + # @!attribute [r] registry + # @return [Registry] The registry of DSL modules attr_reader :name, :registry + # Creates a new Matrix with given name. + # + # Evaluates the block to register DSL modules, then finalizes + # the registry and freezes the matrix. + # + # @param name [Symbol, String] The matrix identifier + # @yield Block for registering DSL modules def initialize(name, &block) @name = name.to_sym @registry = Registry.new(@name) @@ -39,22 +59,42 @@ def initialize(name, &block) freeze end + # Registers a DSL module with the given key. + # + # @param key [Symbol] The registry key + # @param extension [Module] The DSL module to register + # @return [void] def register(key, extension) @registry.register(key, extension) end + # Returns the generated DSL module. + # + # Creates and caches the module on first call. + # + # @return [Module] The DSL module to include in base classes def dsl @dsl_module ||= DSL::Generator.call(self) end + # Returns all registered entries. + # + # @return [Array] The registry entries def entries registry.entries end + # Returns all registered keys. + # + # @return [Array] The registry keys def keys registry.keys end + # Checks if a key is registered. + # + # @param key [Symbol] The key to check + # @return [Boolean] true if the key is registered def key?(key) registry.key?(key) end diff --git a/lib/stroma/registry.rb b/lib/stroma/registry.rb index 4e094a4..f7b0c1e 100644 --- a/lib/stroma/registry.rb +++ b/lib/stroma/registry.rb @@ -3,23 +3,49 @@ module Stroma # Manages registration of DSL modules for a specific matrix. # - # Each Matrix instance has its own Registry - no global state. + # ## Purpose + # + # Stores DSL module entries with their keys. # Implements two-phase lifecycle: registration → finalization. + # Each Matrix has its own Registry - no global state. + # + # ## Usage + # + # ```ruby + # registry = Stroma::Registry.new(:my_lib) + # registry.register(:inputs, Inputs::DSL) + # registry.register(:outputs, Outputs::DSL) + # registry.finalize! + # + # registry.keys # => [:inputs, :outputs] + # registry.key?(:inputs) # => true + # ``` + # + # ## Integration # - # @example - # registry = Stroma::Registry.new(:my_lib) - # registry.register(:inputs, Inputs::DSL) - # registry.finalize! - # registry.keys # => [:inputs] + # Created and owned by Matrix. + # Entries are accessed via Matrix#entries and Matrix#keys. class Registry + # @!attribute [r] matrix_name + # @return [Symbol] The name of the owning matrix attr_reader :matrix_name + # Creates a new registry for the given matrix. + # + # @param matrix_name [Symbol, String] The matrix identifier def initialize(matrix_name) @matrix_name = matrix_name.to_sym @entries = [] @finalized = false end + # Registers a DSL module with the given key. + # + # @param key [Symbol, String] The registry key + # @param extension [Module] The DSL module to register + # @raise [Exceptions::RegistryFrozen] If registry is finalized + # @raise [Exceptions::KeyAlreadyRegistered] If key already exists + # @return [void] def register(key, extension) raise Exceptions::RegistryFrozen, "Registry for #{@matrix_name.inspect} is finalized" if @finalized @@ -33,6 +59,11 @@ def register(key, extension) @entries << Entry.new(key:, extension:) end + # Finalizes the registry, preventing further registrations. + # + # Idempotent - can be called multiple times safely. + # + # @return [void] def finalize! return if @finalized @@ -40,16 +71,29 @@ def finalize! @finalized = true end + # Returns all registered entries. + # + # @raise [Exceptions::RegistryNotFinalized] If not finalized + # @return [Array] The registry entries def entries ensure_finalized! @entries end + # Returns all registered keys. + # + # @raise [Exceptions::RegistryNotFinalized] If not finalized + # @return [Array] The registry keys def keys ensure_finalized! @entries.map(&:key) end + # Checks if a key is registered. + # + # @param key [Symbol, String] The key to check + # @raise [Exceptions::RegistryNotFinalized] If not finalized + # @return [Boolean] true if the key is registered def key?(key) ensure_finalized! @entries.any? { |e| e.key == key.to_sym } @@ -57,6 +101,10 @@ def key?(key) private + # Ensures the registry is finalized. + # + # @raise [Exceptions::RegistryNotFinalized] If not finalized + # @return [void] def ensure_finalized! return if @finalized From 80f62d58a5650c7f20e75e558084dc583d345f25 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:22:33 +0700 Subject: [PATCH 03/11] Refactor Stroma DSL with matrix-scoped design - Replace `Stroma::DSL` with `Stroma::DSL::Generator` for module generation. - Introduce `Stroma::Matrix` to manage isolated DSL registries. - Update `Stroma::Hooks::Applier` and `Factory` to support Matrix integration. - Remove obsolete global DSL and Registry patterns. - Adjust `Steepfile` to account for dynamic module generation. - Minor stylistic adjustments in `Hooks::Factory` methods. --- Steepfile | 4 ++++ lib/stroma/hooks/factory.rb | 4 ++-- sig/lib/stroma/dsl.rbs | 22 ---------------------- sig/lib/stroma/dsl/generator.rbs | 17 +++++++++++++++++ sig/lib/stroma/hooks/applier.rbs | 3 ++- sig/lib/stroma/hooks/factory.rbs | 3 ++- sig/lib/stroma/matrix.rbs | 22 ++++++++++++++++++++++ sig/lib/stroma/registry.rbs | 15 +++++---------- 8 files changed, 54 insertions(+), 36 deletions(-) delete mode 100644 sig/lib/stroma/dsl.rbs create mode 100644 sig/lib/stroma/dsl/generator.rbs create mode 100644 sig/lib/stroma/matrix.rbs diff --git a/Steepfile b/Steepfile index 6c92589..61c1f49 100644 --- a/Steepfile +++ b/Steepfile @@ -26,4 +26,8 @@ target :lib do # Complex splat delegation (*args) in fetch method causes type checking issues ignore "lib/stroma/settings/setting.rb" + + # Dynamic module generation via Module.new causes type checking issues + # Steep can't analyze methods inside Module.new blocks + ignore "lib/stroma/dsl/generator.rb" end diff --git a/lib/stroma/hooks/factory.rb b/lib/stroma/hooks/factory.rb index 9cb235c..8656109 100644 --- a/lib/stroma/hooks/factory.rb +++ b/lib/stroma/hooks/factory.rb @@ -46,7 +46,7 @@ def initialize(hooks, matrix) # before :actions, ValidationModule, AuthorizationModule def before(key, *extensions) validate_key!(key) - extensions.each { |ext| @hooks.add(:before, key, ext) } + extensions.each { |extension| @hooks.add(:before, key, extension) } end # Registers one or more after hooks for a target key. @@ -60,7 +60,7 @@ def before(key, *extensions) # after :outputs, LoggingModule, AuditModule def after(key, *extensions) validate_key!(key) - extensions.each { |ext| @hooks.add(:after, key, ext) } + extensions.each { |extension| @hooks.add(:after, key, extension) } end private diff --git a/sig/lib/stroma/dsl.rbs b/sig/lib/stroma/dsl.rbs deleted file mode 100644 index a982024..0000000 --- a/sig/lib/stroma/dsl.rbs +++ /dev/null @@ -1,22 +0,0 @@ -module Stroma - module DSL - def self.included: (Class base) -> void - - module ClassMethods - @stroma: State - @stroma_hooks_factory: Hooks::Factory - - def self.extended: (Class base) -> void - - # Note: child receives ClassMethods via inheritance, so it has stroma method - # Using untyped to allow stroma method call on child - def inherited: (untyped child) -> void - - def stroma: () -> State - - private - - def extensions: () { [self: Hooks::Factory] -> void } -> void - end - end -end diff --git a/sig/lib/stroma/dsl/generator.rbs b/sig/lib/stroma/dsl/generator.rbs new file mode 100644 index 0000000..8953dbe --- /dev/null +++ b/sig/lib/stroma/dsl/generator.rbs @@ -0,0 +1,17 @@ +module Stroma + module DSL + class Generator + @matrix: Matrix + + def self.call: (Matrix matrix) -> Module + + def initialize: (Matrix matrix) -> void + + def generate: () -> Module + + private + + def build_class_methods: () -> Module + end + end +end diff --git a/sig/lib/stroma/hooks/applier.rbs b/sig/lib/stroma/hooks/applier.rbs index ec5dae7..a6aafd2 100644 --- a/sig/lib/stroma/hooks/applier.rbs +++ b/sig/lib/stroma/hooks/applier.rbs @@ -3,8 +3,9 @@ module Stroma class Applier @target_class: Class @hooks: Collection + @matrix: Matrix - def initialize: (Class target_class, Collection hooks) -> void + def initialize: (Class target_class, Collection hooks, Matrix matrix) -> void def apply!: () -> void end diff --git a/sig/lib/stroma/hooks/factory.rbs b/sig/lib/stroma/hooks/factory.rbs index a39babd..4afec3a 100644 --- a/sig/lib/stroma/hooks/factory.rbs +++ b/sig/lib/stroma/hooks/factory.rbs @@ -2,8 +2,9 @@ module Stroma module Hooks class Factory @hooks: Collection + @matrix: Matrix - def initialize: (Collection hooks) -> void + def initialize: (Collection hooks, Matrix matrix) -> void def before: (Symbol key, *Module extensions) -> void diff --git a/sig/lib/stroma/matrix.rbs b/sig/lib/stroma/matrix.rbs new file mode 100644 index 0000000..e0be150 --- /dev/null +++ b/sig/lib/stroma/matrix.rbs @@ -0,0 +1,22 @@ +module Stroma + class Matrix + @name: Symbol + @registry: Registry + @dsl_module: Module? + + attr_reader name: Symbol + attr_reader registry: Registry + + def initialize: (Symbol | String name) ?{ () -> void } -> void + + def register: (Symbol key, Module extension) -> void + + def dsl: () -> Module + + def entries: () -> Array[Entry] + + def keys: () -> Array[Symbol] + + def key?: (Symbol key) -> bool + end +end diff --git a/sig/lib/stroma/registry.rbs b/sig/lib/stroma/registry.rbs index 2d6993f..a631fa8 100644 --- a/sig/lib/stroma/registry.rbs +++ b/sig/lib/stroma/registry.rbs @@ -1,19 +1,14 @@ module Stroma class Registry - include Singleton - + @matrix_name: Symbol @entries: Array[Entry] @finalized: bool - def self.register: (Symbol key, Module extension) -> void - def self.finalize!: () -> void - def self.entries: () -> Array[Entry] - def self.keys: () -> Array[Symbol] - def self.key?: (Symbol key) -> bool + attr_reader matrix_name: Symbol - def initialize: () -> void + def initialize: (Symbol | String matrix_name) -> void - def register: (Symbol key, Module extension) -> void + def register: (Symbol | String key, Module extension) -> void def finalize!: () -> void @@ -21,7 +16,7 @@ module Stroma def keys: () -> Array[Symbol] - def key?: (Symbol key) -> bool + def key?: (Symbol | String key) -> bool private From 6a8d1c248a2e91a0fe046e6afc8d41e9bb39a1e9 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:27:34 +0700 Subject: [PATCH 04/11] Mark specs and adjust methods for clearer structure - Add `:aggregate_failures` for better failure aggregation in various specs. - Refactor assignments and align `build_class_methods` with RuboCop standards. - Use `attr_reader` for `stroma_matrix` and simplify DSL method memoization. - Format exception handling for improved readability in `Registry#register`. - Suppress RuboCop rule in specs where memoized helpers exceed counts. --- lib/stroma/dsl/generator.rb | 8 +++----- lib/stroma/matrix.rb | 2 +- lib/stroma/registry.rb | 6 ++++-- spec/stroma/dsl/generator_spec.rb | 2 +- spec/stroma/hooks/applier_spec.rb | 2 +- spec/stroma/matrix_spec.rb | 9 ++++++--- spec/stroma/registry_spec.rb | 2 +- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb index 11642d7..8a8f210 100644 --- a/lib/stroma/dsl/generator.rb +++ b/lib/stroma/dsl/generator.rb @@ -59,7 +59,7 @@ def initialize(matrix) # - extensions DSL for registering hooks # # @return [Module] The generated DSL module - def generate + def generate # rubocop:disable Metrics/MethodLength matrix = @matrix class_methods = build_class_methods @@ -88,11 +88,9 @@ def included(base) # Builds the ClassMethods module. # # @return [Module] The ClassMethods module - def build_class_methods + def build_class_methods # rubocop:disable Metrics/MethodLength, Metrics/AbcSize Module.new do - def stroma_matrix - @stroma_matrix - end + attr_reader :stroma_matrix def stroma @stroma ||= State.new diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb index 1aa6dfe..3ee775c 100644 --- a/lib/stroma/matrix.rb +++ b/lib/stroma/matrix.rb @@ -74,7 +74,7 @@ def register(key, extension) # # @return [Module] The DSL module to include in base classes def dsl - @dsl_module ||= DSL::Generator.call(self) + @dsl ||= DSL::Generator.call(self) end # Returns all registered entries. diff --git a/lib/stroma/registry.rb b/lib/stroma/registry.rb index f7b0c1e..6258882 100644 --- a/lib/stroma/registry.rb +++ b/lib/stroma/registry.rb @@ -47,8 +47,10 @@ def initialize(matrix_name) # @raise [Exceptions::KeyAlreadyRegistered] If key already exists # @return [void] def register(key, extension) - raise Exceptions::RegistryFrozen, - "Registry for #{@matrix_name.inspect} is finalized" if @finalized + if @finalized + raise Exceptions::RegistryFrozen, + "Registry for #{@matrix_name.inspect} is finalized" + end key = key.to_sym if @entries.any? { |e| e.key == key } diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index fe9e2c3..980755b 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -37,7 +37,7 @@ expect(base_class).to respond_to(:inherited) end - it "includes all registered DSL modules" do + it "includes all registered DSL modules", :aggregate_failures do expect(base_class.ancestors).to include(inputs_dsl) expect(base_class.ancestors).to include(outputs_dsl) end diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index d95252c..6462c5b 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -48,7 +48,7 @@ end end - context "with multiple hooks" do + context "with multiple hooks" do # rubocop:disable RSpec/MultipleMemoizedHelpers let(:before_inputs) { Module.new } let(:after_inputs) { Module.new } let(:before_outputs) { Module.new } diff --git a/spec/stroma/matrix_spec.rb b/spec/stroma/matrix_spec.rb index fd48d61..e38ef02 100644 --- a/spec/stroma/matrix_spec.rb +++ b/spec/stroma/matrix_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Stroma::Matrix do describe "#initialize" do - it "creates a frozen matrix with name" do + it "creates a frozen matrix with name", :aggregate_failures do matrix = described_class.new(:test) do register :inputs, Module.new end @@ -11,7 +11,7 @@ expect(matrix).to be_frozen end - it "finalizes registry automatically" do + it "finalizes registry automatically", :aggregate_failures do matrix = described_class.new(:test) do register :inputs, Module.new end @@ -69,7 +69,10 @@ end it "allows same keys in different matrices" do - expect { matrix_a; matrix_b }.not_to raise_error + expect do + matrix_a + matrix_b + end.not_to raise_error end end end diff --git a/spec/stroma/registry_spec.rb b/spec/stroma/registry_spec.rb index bf25a3d..269982a 100644 --- a/spec/stroma/registry_spec.rb +++ b/spec/stroma/registry_spec.rb @@ -68,7 +68,7 @@ ) end - it "returns entries after finalize" do + it "returns entries after finalize", :aggregate_failures do extension = Module.new registry.register(:inputs, extension) registry.finalize! From 0be5004041378924ba2360a56545daa8f44ead14 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:40:21 +0700 Subject: [PATCH 05/11] Simplify DSL initialization and improve hook application - Remove redundant eager DSL module generation during `Matrix` initialization. - Refactor `Hooks::Applier` to use a new class-level `apply!` method for convenience. - Update `DSL::Generator` to leverage the new `apply!` method for cleaner hook application. - Add thread-safety documentation to `Hooks::Collection` and `Hooks::Factory`. --- lib/stroma/dsl/generator.rb | 2 +- lib/stroma/hooks/applier.rb | 14 ++++++++++++++ lib/stroma/hooks/collection.rb | 4 ++++ lib/stroma/hooks/factory.rb | 4 ++++ lib/stroma/matrix.rb | 7 ++----- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb index 8a8f210..f1308ac 100644 --- a/lib/stroma/dsl/generator.rb +++ b/lib/stroma/dsl/generator.rb @@ -100,7 +100,7 @@ def inherited(child) super child.instance_variable_set(:@stroma_matrix, stroma_matrix) child.instance_variable_set(:@stroma, stroma.dup) - Hooks::Applier.new(child, child.stroma.hooks, stroma_matrix).apply! + Hooks::Applier.apply!(child, child.stroma.hooks, stroma_matrix) end private diff --git a/lib/stroma/hooks/applier.rb b/lib/stroma/hooks/applier.rb index 4925885..33e3418 100644 --- a/lib/stroma/hooks/applier.rb +++ b/lib/stroma/hooks/applier.rb @@ -23,6 +23,20 @@ module Hooks # Called by DSL::Generator's inherited hook. # Creates a temporary instance that is garbage collected after apply!. class Applier + class << self + # Applies all registered hooks to the target class. + # + # Convenience class method that creates an applier and applies hooks. + # + # @param target_class [Class] The class to apply hooks to + # @param hooks [Collection] The hooks collection to apply + # @param matrix [Matrix] The matrix providing registry entries + # @return [void] + def apply!(target_class, hooks, matrix) + new(target_class, hooks, matrix).apply! + end + end + # Creates a new applier for applying hooks to a class. # # @param target_class [Class] The class to apply hooks to diff --git a/lib/stroma/hooks/collection.rb b/lib/stroma/hooks/collection.rb index 297df1e..a6f4b36 100644 --- a/lib/stroma/hooks/collection.rb +++ b/lib/stroma/hooks/collection.rb @@ -28,6 +28,10 @@ module Hooks # Stored in Stroma::State and used by # Stroma::Hooks::Applier to apply hooks to classes. # Properly duplicated during class inheritance via initialize_dup. + # + # @note Thread Safety: Collection is mutable and NOT thread-safe. + # Hook registration via `add` should only occur during class definition + # (load time), which is typically single-threaded in Ruby applications. class Collection extend Forwardable diff --git a/lib/stroma/hooks/factory.rb b/lib/stroma/hooks/factory.rb index 8656109..fb1f633 100644 --- a/lib/stroma/hooks/factory.rb +++ b/lib/stroma/hooks/factory.rb @@ -25,6 +25,10 @@ module Hooks # # Created by DSL::Generator's extensions method. # Cached as @stroma_hooks_factory on each service class. + # + # @note Thread Safety: Factory uses memoization and is NOT thread-safe. + # Hook registration via extensions block should only occur during class + # definition (load time), which is typically single-threaded in Ruby. class Factory # Creates a new factory for registering hooks. # diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb index 3ee775c..cc18c0f 100644 --- a/lib/stroma/matrix.rb +++ b/lib/stroma/matrix.rb @@ -51,11 +51,10 @@ class Matrix def initialize(name, &block) @name = name.to_sym @registry = Registry.new(@name) - @dsl_module = nil instance_eval(&block) if block_given? @registry.finalize! - dsl # Eager generation before freeze + @dsl = DSL::Generator.call(self) freeze end @@ -70,11 +69,9 @@ def register(key, extension) # Returns the generated DSL module. # - # Creates and caches the module on first call. - # # @return [Module] The DSL module to include in base classes def dsl - @dsl ||= DSL::Generator.call(self) + @dsl end # Returns all registered entries. From a148b6b2a272ee6de73044a3f3b9ee30c24c44a2 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:41:29 +0700 Subject: [PATCH 06/11] Add `dsl` as `attr_reader` and remove thread-safety notes - Replace `dsl` method with an `attr_reader` in `Matrix` for simplicity. - Remove redundant thread-safety notes from `Hooks::Collection` and `Hooks::Factory` as they described expected behavior during class definition. - Streamline code by reducing unnecessary comments and methods. --- lib/stroma/hooks/collection.rb | 4 ---- lib/stroma/hooks/factory.rb | 4 ---- lib/stroma/matrix.rb | 11 +++-------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/lib/stroma/hooks/collection.rb b/lib/stroma/hooks/collection.rb index a6f4b36..297df1e 100644 --- a/lib/stroma/hooks/collection.rb +++ b/lib/stroma/hooks/collection.rb @@ -28,10 +28,6 @@ module Hooks # Stored in Stroma::State and used by # Stroma::Hooks::Applier to apply hooks to classes. # Properly duplicated during class inheritance via initialize_dup. - # - # @note Thread Safety: Collection is mutable and NOT thread-safe. - # Hook registration via `add` should only occur during class definition - # (load time), which is typically single-threaded in Ruby applications. class Collection extend Forwardable diff --git a/lib/stroma/hooks/factory.rb b/lib/stroma/hooks/factory.rb index fb1f633..8656109 100644 --- a/lib/stroma/hooks/factory.rb +++ b/lib/stroma/hooks/factory.rb @@ -25,10 +25,6 @@ module Hooks # # Created by DSL::Generator's extensions method. # Cached as @stroma_hooks_factory on each service class. - # - # @note Thread Safety: Factory uses memoization and is NOT thread-safe. - # Hook registration via extensions block should only occur during class - # definition (load time), which is typically single-threaded in Ruby. class Factory # Creates a new factory for registering hooks. # diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb index cc18c0f..030e619 100644 --- a/lib/stroma/matrix.rb +++ b/lib/stroma/matrix.rb @@ -39,7 +39,9 @@ class Matrix # @return [Symbol] The matrix identifier # @!attribute [r] registry # @return [Registry] The registry of DSL modules - attr_reader :name, :registry + # @!attribute [r] dsl + # @return [Module] The DSL module to include in base classes + attr_reader :name, :registry, :dsl # Creates a new Matrix with given name. # @@ -67,13 +69,6 @@ def register(key, extension) @registry.register(key, extension) end - # Returns the generated DSL module. - # - # @return [Module] The DSL module to include in base classes - def dsl - @dsl - end - # Returns all registered entries. # # @return [Array] The registry entries From 8dfde9a10a8d3c0d8602cbc700387b84f8d6c241 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 21:42:58 +0700 Subject: [PATCH 07/11] Refactor Matrix and add class-level hook application - Replace `dsl` method with `attr_reader` in `Matrix` for consistency and simplicity. - Add `apply!` class method to `Hooks::Applier` for streamlined hook application. - Remove redundant `dsl` initialization for cleaner code. --- sig/lib/stroma/hooks/applier.rbs | 2 ++ sig/lib/stroma/matrix.rbs | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sig/lib/stroma/hooks/applier.rbs b/sig/lib/stroma/hooks/applier.rbs index a6aafd2..628e090 100644 --- a/sig/lib/stroma/hooks/applier.rbs +++ b/sig/lib/stroma/hooks/applier.rbs @@ -5,6 +5,8 @@ module Stroma @hooks: Collection @matrix: Matrix + def self.apply!: (Class target_class, Collection hooks, Matrix matrix) -> void + def initialize: (Class target_class, Collection hooks, Matrix matrix) -> void def apply!: () -> void diff --git a/sig/lib/stroma/matrix.rbs b/sig/lib/stroma/matrix.rbs index e0be150..6e7d331 100644 --- a/sig/lib/stroma/matrix.rbs +++ b/sig/lib/stroma/matrix.rbs @@ -2,17 +2,16 @@ module Stroma class Matrix @name: Symbol @registry: Registry - @dsl_module: Module? + @dsl: Module attr_reader name: Symbol attr_reader registry: Registry + attr_reader dsl: Module def initialize: (Symbol | String name) ?{ () -> void } -> void def register: (Symbol key, Module extension) -> void - def dsl: () -> Module - def entries: () -> Array[Entry] def keys: () -> Array[Symbol] From 766c80302151645f66e2beba8ee550e59b86473a Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 22:07:21 +0700 Subject: [PATCH 08/11] Remove StromaTestRegistry and refactor hook application tests - Delete `StromaTestRegistry` module and its references for code cleanup. - Remove redundant setup in `spec_helper` related to the deleted module. - Add `apply!` class method specs to `Hooks::Applier` for improved test coverage. --- spec/spec_helper.rb | 3 --- spec/stroma/hooks/applier_spec.rb | 13 +++++++++++++ spec/support/stroma_test_registry.rb | 24 ------------------------ 3 files changed, 13 insertions(+), 27 deletions(-) delete mode 100644 spec/support/stroma_test_registry.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 44bf7d6..985c6f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,7 +28,4 @@ c.max_formatted_output_length = nil end - config.before(:suite) do - StromaTestRegistry.setup! - end end diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 6462c5b..62f455d 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -16,6 +16,19 @@ let(:target_class) { Class.new } let(:applier) { described_class.new(target_class, hooks, matrix) } + describe ".apply!" do + let(:before_extension) { Module.new } + + before do + hooks.add(:before, :inputs, before_extension) + end + + it "applies hooks via class method" do + described_class.apply!(target_class, hooks, matrix) + expect(target_class.ancestors).to include(before_extension) + end + end + describe "#apply!" do it "does nothing when hooks empty" do applier.apply! diff --git a/spec/support/stroma_test_registry.rb b/spec/support/stroma_test_registry.rb deleted file mode 100644 index a686a20..0000000 --- a/spec/support/stroma_test_registry.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module StromaTestRegistry - MOCK_MODULES = { - configuration: Module.new, - info: Module.new, - context: Module.new, - inputs: Module.new, - internals: Module.new, - outputs: Module.new, - actions: Module.new - }.freeze - - def self.setup! - # No global setup needed - Matrix is created per-test - end - - def self.create_matrix(name = :test, keys: MOCK_MODULES.keys) - modules = MOCK_MODULES - Stroma::Matrix.new(name) do - keys.each { |key| register(key, modules[key]) } - end - end -end From 3b6ca234cc61cedffacd297549285325cc99bdd5 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 22:16:57 +0700 Subject: [PATCH 09/11] Add `Matrix.define` method for simplified instance creation - Introduce `Matrix.define` as a class method to streamline matrix creation. - Update related examples in documentation and tests to prefer `.define` over `.new`. - Ensure `Matrix.define` semantically indicates the creation of an immutable DSL scope. - Add unit tests to validate equivalency between `.define` and `.new`. - Adjust type signatures and method comments to reflect the new API. --- README.md | 2 +- lib/stroma/matrix.rb | 22 +++++++++++++++++++++- sig/lib/stroma/matrix.rbs | 2 ++ spec/stroma/matrix_spec.rb | 26 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83d2b4b..d5dc09f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ spec.add_dependency "stroma", ">= 0.3" ```ruby module MyLib - STROMA = Stroma::Matrix.new(:my_lib) do + STROMA = Stroma::Matrix.define(:my_lib) do register :inputs, MyLib::Inputs::DSL register :actions, MyLib::Actions::DSL end diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb index 030e619..c72a69d 100644 --- a/lib/stroma/matrix.rb +++ b/lib/stroma/matrix.rb @@ -18,7 +18,7 @@ module Stroma # # ```ruby # module MyLib - # STROMA = Stroma::Matrix.new(:my_lib) do + # STROMA = Stroma::Matrix.define(:my_lib) do # register :inputs, Inputs::DSL # register :outputs, Outputs::DSL # end @@ -35,6 +35,26 @@ module Stroma # Stored as a constant in the library's namespace. # Owns the Registry and generates DSL module via DSL::Generator. class Matrix + class << self + # Defines a new Matrix with given name. + # + # Preferred way to create a Matrix. Semantically indicates + # that we are defining an immutable DSL scope. + # + # @param name [Symbol, String] The matrix identifier + # @yield Block for registering DSL modules + # @return [Matrix] The frozen matrix instance + # + # @example + # STROMA = Stroma::Matrix.define(:my_lib) do + # register :inputs, Inputs::DSL + # register :outputs, Outputs::DSL + # end + def define(name, &block) + new(name, &block) + end + end + # @!attribute [r] name # @return [Symbol] The matrix identifier # @!attribute [r] registry diff --git a/sig/lib/stroma/matrix.rbs b/sig/lib/stroma/matrix.rbs index 6e7d331..e80c883 100644 --- a/sig/lib/stroma/matrix.rbs +++ b/sig/lib/stroma/matrix.rbs @@ -8,6 +8,8 @@ module Stroma attr_reader registry: Registry attr_reader dsl: Module + def self.define: (Symbol | String name) ?{ () -> void } -> Matrix + def initialize: (Symbol | String name) ?{ () -> void } -> void def register: (Symbol key, Module extension) -> void diff --git a/spec/stroma/matrix_spec.rb b/spec/stroma/matrix_spec.rb index e38ef02..e2c31e8 100644 --- a/spec/stroma/matrix_spec.rb +++ b/spec/stroma/matrix_spec.rb @@ -1,6 +1,32 @@ # frozen_string_literal: true RSpec.describe Stroma::Matrix do + describe ".define" do + it "creates a frozen matrix with name", :aggregate_failures do + matrix = described_class.define(:test) do + register :inputs, Module.new + end + + expect(matrix.name).to eq(:test) + expect(matrix).to be_frozen + end + + it "is equivalent to .new" do + inputs_mod = Module.new + + matrix_via_define = described_class.define(:test) do + register :inputs, inputs_mod + end + + matrix_via_new = described_class.new(:test) do + register :inputs, inputs_mod + end + + expect(matrix_via_define.name).to eq(matrix_via_new.name) + expect(matrix_via_define.keys).to eq(matrix_via_new.keys) + end + end + describe "#initialize" do it "creates a frozen matrix with name", :aggregate_failures do matrix = described_class.new(:test) do From 5c0219b357defd55f56e5d93e30d08bca821a818 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 22:18:10 +0700 Subject: [PATCH 10/11] Update specs to use `Matrix.define` instead of `Matrix.new` - Replace `Matrix.new` with `Matrix.define` in various test files for consistency with the updated API. - Adjust lifecycle documentation in `Matrix` to reflect the transition to `.define`. - Ensure all examples align with the simplified matrix creation approach introduced in recent changes. --- lib/stroma/matrix.rb | 2 +- spec/stroma/dsl/generator_spec.rb | 2 +- spec/stroma/hooks/applier_spec.rb | 2 +- spec/stroma/hooks/factory_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/stroma/matrix.rb b/lib/stroma/matrix.rb index c72a69d..fc2423b 100644 --- a/lib/stroma/matrix.rb +++ b/lib/stroma/matrix.rb @@ -9,7 +9,7 @@ module Stroma # Each matrix has its own registry - no conflicts with other libraries. # # Lifecycle: - # - Boot time: Matrix.new creates Registry, registers extensions + # - Boot time: Matrix.define creates Registry, registers extensions # - Boot time: finalize! freezes registry, dsl generates Module # - Boot time: freeze makes Matrix immutable # - Runtime: All structures frozen, no allocations diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index 980755b..ec8aa8f 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -7,7 +7,7 @@ let(:matrix) do inputs = inputs_dsl outputs = outputs_dsl - Stroma::Matrix.new(:test) do + Stroma::Matrix.define(:test) do register :inputs, inputs register :outputs, outputs end diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 62f455d..93b98d6 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -6,7 +6,7 @@ let(:matrix) do inputs = inputs_dsl outputs = outputs_dsl - Stroma::Matrix.new(:test) do + Stroma::Matrix.define(:test) do register :inputs, inputs register :outputs, outputs end diff --git a/spec/stroma/hooks/factory_spec.rb b/spec/stroma/hooks/factory_spec.rb index 5e3789a..ed59922 100644 --- a/spec/stroma/hooks/factory_spec.rb +++ b/spec/stroma/hooks/factory_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Stroma::Hooks::Factory do let(:hooks) { Stroma::Hooks::Collection.new } let(:matrix) do - Stroma::Matrix.new(:test) do + Stroma::Matrix.define(:test) do register :inputs, Module.new register :outputs, Module.new register :actions, Module.new From 27814be09144c5970c94fee956e6d1c153077ae0 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 13 Jan 2026 22:25:04 +0700 Subject: [PATCH 11/11] Mark specs with `:aggregate_failures` and clean up unused tests - Add `:aggregate_failures` metadata to enhance failure aggregation in relevant tests. - Remove redundant test for `dsl` module caching to simplify the test suite. - Minor adjustments to align with current standards for streamlined specs. - Clean up unused lines in `spec_helper` for better readability. --- spec/spec_helper.rb | 1 - spec/stroma/matrix_spec.rb | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 985c6f6..716e4fe 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,5 +27,4 @@ # doing truncation. c.max_formatted_output_length = nil end - end diff --git a/spec/stroma/matrix_spec.rb b/spec/stroma/matrix_spec.rb index e2c31e8..7c7293e 100644 --- a/spec/stroma/matrix_spec.rb +++ b/spec/stroma/matrix_spec.rb @@ -11,7 +11,7 @@ expect(matrix).to be_frozen end - it "is equivalent to .new" do + it "is equivalent to .new", :aggregate_failures do inputs_mod = Module.new matrix_via_define = described_class.define(:test) do @@ -68,10 +68,6 @@ it "returns a module" do expect(matrix.dsl).to be_a(Module) end - - it "caches the module" do - expect(matrix.dsl).to be(matrix.dsl) - end end describe "isolation" do