Skip to content
Merged
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
21 changes: 8 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.define(:my_lib) do
register :inputs, MyLib::Inputs::DSL
register :actions, MyLib::Actions::DSL
end
private_constant :STROMA
end
```

Expand All @@ -73,7 +68,7 @@ end
```ruby
module MyLib
class Base
include MyLib::DSL
include STROMA.dsl
end
end
```
Expand Down
4 changes: 4 additions & 0 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
124 changes: 0 additions & 124 deletions lib/stroma/dsl.rb

This file was deleted.

116 changes: 116 additions & 0 deletions lib/stroma/dsl/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

module Stroma
module DSL
# Generates a DSL module scoped to a specific Matrix.
#
# ## Purpose
#
# Creates a module that:
# - Stores matrix reference on the module itself
# - Defines ClassMethods for service classes
# - Handles inheritance with state duplication
#
# Memory model:
# - Matrix owns @dsl_module (generated once, cached)
# - ServiceClass gets @stroma_matrix (same reference)
# - ServiceClass gets @stroma (unique State per class)
#
# ## Usage
#
# ```ruby
# # Called internally by Matrix#dsl
# dsl_module = Stroma::DSL::Generator.call(matrix)
#
# # The generated module is included in base classes
# class MyLib::Base
# include dsl_module
# end
# ```
#
# ## Integration
#
# 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 # rubocop:disable Metrics/MethodLength
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

# Builds the ClassMethods module.
#
# @return [Module] The ClassMethods module
def build_class_methods # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
Module.new do
attr_reader :stroma_matrix

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.apply!(child, child.stroma.hooks, stroma_matrix)
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
48 changes: 27 additions & 21 deletions lib/stroma/hooks/applier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,46 @@ module Hooks
#
# ## Purpose
#
# 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.
# 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
# applier = Stroma::Hooks::Applier.new(ChildService, hooks)
# # Called internally during class inheritance
# applier = Stroma::Hooks::Applier.new(ChildService, hooks, matrix)
# 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.
# 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
# @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.
Expand All @@ -39,21 +54,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
Expand Down
Loading
Loading