stroma 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27a735955a54fe9ea248a066afbdf10edaa55bc903b83574e84d405fb01fa020
4
- data.tar.gz: e63528c3ac53e3a32a757493745dac02ee46958a486b184cda3d3d4fc4463c6f
3
+ metadata.gz: 3651f535308d76dc210bfb0d49b87179e20ea806e4bb8032eee5165c7f23d635
4
+ data.tar.gz: 5083f7bc1e8541832be170ee35eb9ddf4852b5fd90e76af8c6d7303066e2a927
5
5
  SHA512:
6
- metadata.gz: 19f5d6847f269dfe65070f267426b68ec7a61b72cdfde478ec3542ed9d75a232465c32412eab4bb8d432e4fb648962b309a5d7dc450ec2b14ce329506a86e55c
7
- data.tar.gz: fc4c1300028fa543f2d097d70dcba472644837579a73d58235e51215ac7792d1ce0a1e423f3daac97dac1bcbcad0ab48642f3b63ae663dade353e505bd59466d
6
+ metadata.gz: 2b56c004b08152484972a38b47919c5b1b299d9316bbbcebc2c533190ea766dee62a9882d81ca97281874c4d47bf8e4447fa31142bd51d9329d2c8a17fe9c0da
7
+ data.tar.gz: cee73e3a9040cf6da488c3326333618d30128dc448179e9d184b991d8602ea7da3ebfcbd50eab9e1d6559b5b14214aaed90f42ad1a6e3757d435933ba3f4f12b
data/README.md CHANGED
@@ -1,4 +1,12 @@
1
- <h1 align="center">Stroma</h1>
1
+ <p align="center">
2
+ <a href="https://servactory.com" target="_blank">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/servactory/stroma/main/.github/logo-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/servactory/stroma/main/.github/logo-light.svg">
6
+ <img alt="Stroma" src="https://raw.githubusercontent.com/servactory/stroma/main/.github/logo-light.svg" width="350" height="70" style="max-width: 100%;">
7
+ </picture>
8
+ </a>
9
+ </p>
2
10
 
3
11
  <p align="center">
4
12
  A foundation for building modular, extensible DSLs in Ruby.
@@ -39,32 +47,27 @@ Building modular DSLs shouldn't require reinventing the wheel. Stroma provides a
39
47
  Stroma is a foundation for library authors building DSL-driven frameworks (service objects, form objects, decorators, etc.).
40
48
 
41
49
  **Core lifecycle:**
42
- 1. **Register** - Define DSL modules at boot time via `Stroma::Registry`
43
- 2. **Compose** - Classes include `Stroma::DSL` to gain all registered modules automatically
44
- 3. **Extend** (optional) - Users can add cross-cutting logic via `before`/`after` hooks
50
+ 1. **Define** - Create a Matrix with DSL modules at boot time
51
+ 2. **Include** - Classes include the matrix's DSL to gain all modules
52
+ 3. **Extend** (optional) - Add cross-cutting logic via `before`/`after` hooks
45
53
 
46
54
  ## 🚀 Quick Start
47
55
 
48
56
  ### Installation
49
57
 
50
58
  ```ruby
51
- spec.add_dependency "stroma", ">= 0.3"
59
+ spec.add_dependency "stroma", ">= 0.4"
52
60
  ```
53
61
 
54
62
  ### Define your library's DSL
55
63
 
56
64
  ```ruby
57
65
  module MyLib
58
- module DSL
59
- # Register DSL modules at load time
60
- Stroma::Registry.register(:inputs, MyLib::Inputs::DSL)
61
- Stroma::Registry.register(:actions, MyLib::Actions::DSL)
62
- Stroma::Registry.finalize!
63
-
64
- def self.included(base)
65
- base.include(Stroma::DSL)
66
- end
66
+ STROMA = Stroma::Matrix.define(:my_lib) do
67
+ register :inputs, MyLib::Inputs::DSL
68
+ register :actions, MyLib::Actions::DSL
67
69
  end
70
+ private_constant :STROMA
68
71
  end
69
72
  ```
70
73
 
@@ -73,15 +76,31 @@ end
73
76
  ```ruby
74
77
  module MyLib
75
78
  class Base
76
- include MyLib::DSL
79
+ include STROMA.dsl
77
80
  end
78
81
  end
79
82
  ```
80
83
 
81
84
  ### Usage
82
85
 
86
+ Create an intermediate class with lifecycle hooks:
87
+
83
88
  ```ruby
84
- class UserService < MyLib::Base
89
+ class ApplicationService < MyLib::Base
90
+ # Add lifecycle hooks (optional)
91
+ extensions do
92
+ before :actions, ApplicationService::Extensions::Rollbackable::DSL
93
+ end
94
+ end
95
+ ```
96
+
97
+ Build services that inherit extension functionality:
98
+
99
+ ```ruby
100
+ class UserService < ApplicationService
101
+ # DSL method from Rollbackable extension
102
+ on_rollback(...)
103
+
85
104
  input :email, type: String
86
105
 
87
106
  make :create_user
@@ -94,6 +113,12 @@ class UserService < MyLib::Base
94
113
  end
95
114
  ```
96
115
 
116
+ Extensions allow you to add cross-cutting concerns like transactions, authorization, and rollback support. See [extension examples](https://github.com/servactory/servactory/tree/main/examples/application_service/extensions) for implementation details.
117
+
118
+ ## 💎 Projects Using Stroma
119
+
120
+ - [Servactory](https://github.com/servactory/servactory) — Service objects framework for Ruby applications
121
+
97
122
  ## 🤝 Contributing
98
123
 
99
124
  We welcome contributions! Check out our [Contributing Guide](https://github.com/servactory/stroma/blob/main/CONTRIBUTING.md) to get started.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module DSL
5
+ # Generates a DSL module scoped to a specific Matrix.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Creates a module that:
10
+ # - Stores matrix reference on the module itself
11
+ # - Defines ClassMethods for service classes
12
+ # - Handles inheritance with state duplication
13
+ #
14
+ # Memory model:
15
+ # - Matrix owns @dsl_module (generated once, cached)
16
+ # - ServiceClass gets @stroma_matrix (same reference)
17
+ # - ServiceClass gets @stroma (unique State per class)
18
+ #
19
+ # ## Deferred Entry Inclusion
20
+ #
21
+ # Entry extensions are NOT included via `Module#include` at the base class level.
22
+ # Instead, only the `self.included` callback is fired (via `send(:included, base)`)
23
+ # to set up ClassMethods, constants, etc. The actual module insertion into the
24
+ # ancestor chain (`append_features`) is deferred until {Hooks::Applier} interleaves
25
+ # entries with hooks in child classes.
26
+ #
27
+ # **Contract:** Entry extensions MUST implement `self.included` as idempotent.
28
+ # The callback fires twice per entry per class hierarchy:
29
+ # 1. At base class creation (deferred, via `send(:included, base)`)
30
+ # 2. At child class creation (real, via `include` in {Hooks::Applier})
31
+ #
32
+ # ## Usage
33
+ #
34
+ # ```ruby
35
+ # # Called internally by Matrix#dsl
36
+ # dsl_module = Stroma::DSL::Generator.call(matrix)
37
+ #
38
+ # # The generated module is included in base classes
39
+ # class MyLib::Base
40
+ # include dsl_module
41
+ # end
42
+ # ```
43
+ #
44
+ # ## Integration
45
+ #
46
+ # Called by Matrix#dsl to generate the DSL module.
47
+ # Generated module includes all registered extensions.
48
+ class Generator
49
+ class << self
50
+ # Generates a DSL module for the given matrix.
51
+ #
52
+ # @param matrix [Matrix] The matrix to generate DSL for
53
+ # @return [Module] The generated DSL module
54
+ def call(matrix)
55
+ new(matrix).generate
56
+ end
57
+ end
58
+
59
+ # Creates a new generator for the given matrix.
60
+ #
61
+ # @param matrix [Matrix] The matrix to generate DSL for
62
+ def initialize(matrix)
63
+ @matrix = matrix
64
+ end
65
+
66
+ # Generates the DSL module.
67
+ #
68
+ # Creates a module with ClassMethods that provides:
69
+ # - stroma_matrix accessor for matrix reference
70
+ # - stroma accessor for per-class state
71
+ # - inherited hook for state duplication
72
+ # - extensions DSL for registering hooks
73
+ #
74
+ # @return [Module] The generated DSL module
75
+ def generate # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
76
+ matrix = @matrix
77
+ class_methods = build_class_methods
78
+
79
+ mod = Module.new do
80
+ @stroma_matrix = matrix
81
+
82
+ class << self
83
+ attr_reader :stroma_matrix
84
+
85
+ def included(base)
86
+ mtx = stroma_matrix
87
+ base.extend(self::ClassMethods)
88
+ base.instance_variable_set(:@stroma_matrix, mtx)
89
+ base.instance_variable_set(:@stroma, State.new)
90
+
91
+ # Deferred inclusion: triggers `included` callback without `append_features`.
92
+ # The callback runs ClassMethods/Workspace setup on base.
93
+ # `append_features` (actual module insertion into ancestors) is deferred
94
+ # until Applier interleaves entries with hooks in child classes.
95
+ # NOTE: `included` will fire again when Applier calls `include` on child,
96
+ # so entry extensions must design `self.included` as idempotent.
97
+ mtx.entries.each { |entry| entry.extension.send(:included, base) }
98
+ end
99
+ end
100
+
101
+ const_set(:ClassMethods, class_methods)
102
+ end
103
+
104
+ Utils.name_module(mod, "Stroma::DSL(#{matrix.name})")
105
+ Utils.name_module(class_methods, "Stroma::DSL(#{matrix.name})::ClassMethods")
106
+
107
+ mod
108
+ end
109
+
110
+ private
111
+
112
+ # Builds the ClassMethods module.
113
+ #
114
+ # @return [Module] The ClassMethods module
115
+ def build_class_methods # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
116
+ Module.new do
117
+ attr_reader :stroma_matrix
118
+
119
+ def stroma
120
+ @stroma ||= State.new
121
+ end
122
+
123
+ def inherited(child)
124
+ super
125
+ child.instance_variable_set(:@stroma_matrix, stroma_matrix)
126
+ child.instance_variable_set(:@stroma, stroma.dup)
127
+ Hooks::Applier.apply!(child, child.stroma.hooks, stroma_matrix)
128
+ end
129
+
130
+ private
131
+
132
+ def extensions(&block)
133
+ @stroma_hooks_factory ||= Hooks::Factory.new(stroma.hooks, stroma_matrix)
134
+ @stroma_hooks_factory.instance_eval(&block)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -2,58 +2,109 @@
2
2
 
3
3
  module Stroma
4
4
  module Hooks
5
- # Applies registered hooks to a target class.
5
+ # Applies registered hooks to a target class with deferred entry inclusion.
6
6
  #
7
7
  # ## Purpose
8
8
  #
9
- # Iterates through all registered DSL modules and includes corresponding
10
- # before/after hooks in the target class. For each registry entry,
11
- # before hooks are included first, then after hooks.
9
+ # Manages hook and entry module inclusion into the target class.
10
+ # Operates in three modes depending on current state:
11
+ # - No hooks: returns immediately (entries stay deferred)
12
+ # - Entries already in ancestors: includes only new hooks
13
+ # - Entries not in ancestors: interleaves entries with hooks for correct MRO
12
14
  #
13
15
  # ## Usage
14
16
  #
15
17
  # ```ruby
16
- # applier = Stroma::Hooks::Applier.new(ChildService, hooks)
18
+ # # Called internally during class inheritance
19
+ # applier = Stroma::Hooks::Applier.new(ChildService, hooks, matrix)
17
20
  # applier.apply!
18
- # # ChildService now includes all hook modules
19
21
  # ```
20
22
  #
21
23
  # ## Integration
22
24
  #
23
- # Called by Stroma::DSL.inherited after duplicating
24
- # parent's configuration. Uses Registry.entries to determine
25
- # hook application order.
25
+ # Called by DSL::Generator's inherited hook.
26
+ # Creates a temporary instance that is garbage collected after apply!.
26
27
  class Applier
28
+ class << self
29
+ # Applies all registered hooks to the target class.
30
+ #
31
+ # Convenience class method that creates an applier and applies hooks.
32
+ #
33
+ # @param target_class [Class] The class to apply hooks to
34
+ # @param hooks [Collection] The hooks collection to apply
35
+ # @param matrix [Matrix] The matrix providing registry entries
36
+ # @return [void]
37
+ def apply!(target_class, hooks, matrix)
38
+ new(target_class, hooks, matrix).apply!
39
+ end
40
+ end
41
+
27
42
  # Creates a new applier for applying hooks to a class.
28
43
  #
29
44
  # @param target_class [Class] The class to apply hooks to
30
45
  # @param hooks [Collection] The hooks collection to apply
31
- def initialize(target_class, hooks)
46
+ # @param matrix [Matrix] The matrix providing registry entries
47
+ def initialize(target_class, hooks, matrix)
32
48
  @target_class = target_class
33
49
  @hooks = hooks
50
+ @matrix = matrix
34
51
  end
35
52
 
36
53
  # Applies all registered hooks to the target class.
37
54
  #
38
- # For each registry entry, includes before hooks first,
39
- # then after hooks. Does nothing if hooks collection is empty.
55
+ # Three modes based on current state:
56
+ # - No hooks: return immediately (defer entry inclusion)
57
+ # - Entries already in ancestors: include only hooks
58
+ # - Entries not in ancestors: interleave entries with hooks
40
59
  #
41
60
  # @return [void]
42
- #
43
- # @example
44
- # applier.apply!
45
- # # Target class now includes all extension modules
46
61
  def apply!
47
62
  return if @hooks.empty?
48
63
 
49
- Registry.entries.each do |entry|
50
- @hooks.before(entry.key).each do |hook|
51
- @target_class.include(hook.extension)
52
- end
64
+ if entries_in_ancestors?
65
+ include_hooks_only
66
+ else
67
+ include_entries_with_hooks
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Checks whether all entry extensions are already in the target class ancestors.
74
+ #
75
+ # Uses all? so that partial inclusion (some entries present, some not)
76
+ # falls through to include_entries_with_hooks where Ruby skips
77
+ # already-included modules (idempotent) and interleaves the rest.
78
+ #
79
+ # @return [Boolean]
80
+ def entries_in_ancestors?
81
+ ancestors = @target_class.ancestors
82
+ @matrix.entries.all? { |e| ancestors.include?(e.extension) }
83
+ end
84
+
85
+ # Includes entries interleaved with their hooks.
86
+ #
87
+ # For each entry: after hooks first (reversed), then entry, then before hooks (reversed).
88
+ # reverse_each ensures first registered = outermost in MRO.
89
+ #
90
+ # @return [void]
91
+ def include_entries_with_hooks
92
+ @matrix.entries.each do |entry|
93
+ @hooks.after(entry.key).reverse_each { |hook| @target_class.include(hook.extension) }
94
+ @target_class.include(entry.extension)
95
+ @hooks.before(entry.key).reverse_each { |hook| @target_class.include(hook.extension) }
96
+ end
97
+ end
53
98
 
54
- @hooks.after(entry.key).each do |hook|
55
- @target_class.include(hook.extension)
56
- end
99
+ # Includes only hook extensions without entries.
100
+ #
101
+ # Used when entries are already in ancestors (multi-level inheritance).
102
+ #
103
+ # @return [void]
104
+ def include_hooks_only
105
+ @matrix.entries.each do |entry|
106
+ @hooks.before(entry.key).reverse_each { |hook| @target_class.include(hook.extension) }
107
+ @hooks.after(entry.key).reverse_each { |hook| @target_class.include(hook.extension) }
57
108
  end
58
109
  end
59
110
  end
@@ -59,7 +59,7 @@ module Stroma
59
59
  # @return [void]
60
60
  def initialize_dup(original)
61
61
  super
62
- @collection = original.instance_variable_get(:@collection).dup
62
+ @collection = original.collection.dup
63
63
  end
64
64
 
65
65
  # Adds a new hook to the collection.
@@ -96,6 +96,10 @@ module Stroma
96
96
  def after(key)
97
97
  @collection.select { |hook| hook.after? && hook.target_key == key }
98
98
  end
99
+
100
+ protected
101
+
102
+ attr_reader :collection
99
103
  end
100
104
  end
101
105
  end
@@ -6,21 +6,16 @@ module Stroma
6
6
  #
7
7
  # ## Purpose
8
8
  #
9
- # Provides the `before` and `after` methods used within the extensions
10
- # block to register hooks. Validates that target keys exist in Registry
11
- # before adding hooks.
9
+ # Provides before/after DSL methods for hook registration.
10
+ # Validates target keys against the matrix's registry.
11
+ # Delegates to Hooks::Collection for storage.
12
12
  #
13
13
  # ## Usage
14
14
  #
15
- # Used within `extensions` block in classes that include Stroma::DSL:
16
- #
17
15
  # ```ruby
18
- # # Library Base class (includes Stroma::DSL via library's DSL module)
19
- # class MyLib::Base
20
- # include MyLib::DSL # MyLib::DSL includes Stroma::DSL
21
- #
16
+ # class MyService < MyLib::Base
22
17
  # extensions do
23
- # before :actions, ValidationModule
18
+ # before :actions, ValidationModule, AuthModule
24
19
  # after :outputs, LoggingModule
25
20
  # end
26
21
  # end
@@ -28,15 +23,16 @@ module Stroma
28
23
  #
29
24
  # ## Integration
30
25
  #
31
- # Created by DSL.extensions method and receives instance_eval of the block.
32
- # Validates keys against Registry.keys and raises UnknownHookTarget
33
- # for invalid keys.
26
+ # Created by DSL::Generator's extensions method.
27
+ # Cached as @stroma_hooks_factory on each service class.
34
28
  class Factory
35
29
  # Creates a new factory for registering hooks.
36
30
  #
37
31
  # @param hooks [Collection] The hooks collection to add to
38
- def initialize(hooks)
32
+ # @param matrix [Matrix] The matrix providing valid keys
33
+ def initialize(hooks, matrix)
39
34
  @hooks = hooks
35
+ @matrix = matrix
40
36
  end
41
37
 
42
38
  # Registers one or more before hooks for a target key.
@@ -44,6 +40,7 @@ module Stroma
44
40
  # @param key [Symbol] The registry key to hook before
45
41
  # @param extensions [Array<Module>] Extension modules to include
46
42
  # @raise [Exceptions::UnknownHookTarget] If key is not registered
43
+ # @return [void]
47
44
  #
48
45
  # @example
49
46
  # before :actions, ValidationModule, AuthorizationModule
@@ -57,6 +54,7 @@ module Stroma
57
54
  # @param key [Symbol] The registry key to hook after
58
55
  # @param extensions [Array<Module>] Extension modules to include
59
56
  # @raise [Exceptions::UnknownHookTarget] If key is not registered
57
+ # @return [void]
60
58
  #
61
59
  # @example
62
60
  # after :outputs, LoggingModule, AuditModule
@@ -67,16 +65,17 @@ module Stroma
67
65
 
68
66
  private
69
67
 
70
- # Validates that the key exists in the Registry.
68
+ # Validates that the key exists in the matrix's registry.
71
69
  #
72
70
  # @param key [Symbol] The key to validate
73
71
  # @raise [Exceptions::UnknownHookTarget] If key is not registered
72
+ # @return [void]
74
73
  def validate_key!(key)
75
- return if Registry.key?(key)
74
+ return if @matrix.key?(key)
76
75
 
77
76
  raise Exceptions::UnknownHookTarget,
78
- "Unknown hook target: #{key.inspect}. " \
79
- "Valid keys: #{Registry.keys.map(&:inspect).join(', ')}"
77
+ "Unknown hook target #{key.inspect} for #{@matrix.name.inspect}. " \
78
+ "Valid: #{@matrix.keys.map(&:inspect).join(', ')}"
80
79
  end
81
80
  end
82
81
  end
@@ -46,7 +46,7 @@ module Stroma
46
46
  # @param extension [Module] Extension module to include
47
47
  # @raise [Exceptions::InvalidHookType] If type is invalid
48
48
  def initialize(type:, target_key:, extension:)
49
- if VALID_HOOK_TYPES.exclude?(type)
49
+ unless VALID_HOOK_TYPES.include?(type)
50
50
  raise Exceptions::InvalidHookType,
51
51
  "Invalid hook type: #{type.inspect}. Valid types: #{VALID_HOOK_TYPES.map(&:inspect).join(', ')}"
52
52
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ # Main entry point for libraries using Stroma.
5
+ #
6
+ # ## Purpose
7
+ #
8
+ # Creates an isolated registry and generates a scoped DSL module.
9
+ # Each matrix has its own registry - no conflicts with other libraries.
10
+ #
11
+ # Lifecycle:
12
+ # - Boot time: Matrix.define creates Registry, registers extensions
13
+ # - Boot time: finalize! freezes registry, dsl generates Module
14
+ # - Boot time: freeze makes Matrix immutable
15
+ # - Runtime: All structures frozen, no allocations
16
+ #
17
+ # ## Usage
18
+ #
19
+ # ```ruby
20
+ # module MyLib
21
+ # STROMA = Stroma::Matrix.define(:my_lib) do
22
+ # register :inputs, Inputs::DSL
23
+ # register :outputs, Outputs::DSL
24
+ # end
25
+ # private_constant :STROMA
26
+ # end
27
+ #
28
+ # class MyLib::Base
29
+ # include MyLib::STROMA.dsl
30
+ # end
31
+ # ```
32
+ #
33
+ # ## Integration
34
+ #
35
+ # Stored as a constant in the library's namespace.
36
+ # Owns the Registry and generates DSL module via DSL::Generator.
37
+ class Matrix
38
+ class << self
39
+ # Defines a new Matrix with given name.
40
+ #
41
+ # Preferred way to create a Matrix. Semantically indicates
42
+ # that we are defining an immutable DSL scope.
43
+ #
44
+ # @param name [Symbol, String] The matrix identifier
45
+ # @yield Block for registering DSL modules
46
+ # @return [Matrix] The frozen matrix instance
47
+ #
48
+ # @example
49
+ # STROMA = Stroma::Matrix.define(:my_lib) do
50
+ # register :inputs, Inputs::DSL
51
+ # register :outputs, Outputs::DSL
52
+ # end
53
+ def define(name, &block)
54
+ new(name, &block)
55
+ end
56
+ end
57
+
58
+ # @!attribute [r] name
59
+ # @return [Symbol] The matrix identifier
60
+ # @!attribute [r] registry
61
+ # @return [Registry] The registry of DSL modules
62
+ # @!attribute [r] dsl
63
+ # @return [Module] The DSL module to include in base classes
64
+ attr_reader :name, :registry, :dsl
65
+
66
+ # Creates a new Matrix with given name.
67
+ #
68
+ # Evaluates the block to register DSL modules, then finalizes
69
+ # the registry and freezes the matrix.
70
+ #
71
+ # @param name [Symbol, String] The matrix identifier
72
+ # @yield Block for registering DSL modules
73
+ def initialize(name, &block)
74
+ @name = name.to_sym
75
+ @registry = Registry.new(@name)
76
+
77
+ instance_eval(&block) if block_given?
78
+ @registry.finalize!
79
+ @dsl = DSL::Generator.call(self)
80
+ freeze
81
+ end
82
+
83
+ # Registers a DSL module with the given key.
84
+ #
85
+ # @param key [Symbol] The registry key
86
+ # @param extension [Module] The DSL module to register
87
+ # @return [void]
88
+ def register(key, extension)
89
+ @registry.register(key, extension)
90
+ end
91
+
92
+ # Returns all registered entries.
93
+ #
94
+ # @return [Array<Entry>] The registry entries
95
+ def entries
96
+ registry.entries
97
+ end
98
+
99
+ # Returns all registered keys.
100
+ #
101
+ # @return [Array<Symbol>] The registry keys
102
+ def keys
103
+ registry.keys
104
+ end
105
+
106
+ # Checks if a key is registered.
107
+ #
108
+ # @param key [Symbol] The key to check
109
+ # @return [Boolean] true if the key is registered
110
+ def key?(key)
111
+ registry.key?(key)
112
+ end
113
+ end
114
+ end
@@ -1,92 +1,118 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stroma
4
- # Manages global registration of DSL modules for Stroma.
4
+ # Manages registration of DSL modules for a specific matrix.
5
5
  #
6
6
  # ## Purpose
7
7
  #
8
- # Singleton registry that stores all DSL modules that will be included
9
- # in service classes. Implements two-phase lifecycle: registration
10
- # followed by finalization.
8
+ # Stores DSL module entries with their keys.
9
+ # Implements two-phase lifecycle: registration → finalization.
10
+ # Each Matrix has its own Registry - no global state.
11
11
  #
12
12
  # ## Usage
13
13
  #
14
14
  # ```ruby
15
- # # During gem initialization:
16
- # Stroma::Registry.register(:inputs, Inputs::DSL)
17
- # Stroma::Registry.register(:outputs, Outputs::DSL)
18
- # Stroma::Registry.finalize!
15
+ # registry = Stroma::Registry.new(:my_lib)
16
+ # registry.register(:inputs, Inputs::DSL)
17
+ # registry.register(:outputs, Outputs::DSL)
18
+ # registry.finalize!
19
19
  #
20
- # # After finalization:
21
- # Stroma::Registry.keys # => [:inputs, :outputs]
22
- # Stroma::Registry.key?(:inputs) # => true
20
+ # registry.keys # => [:inputs, :outputs]
21
+ # registry.key?(:inputs) # => true
23
22
  # ```
24
23
  #
25
24
  # ## Integration
26
25
  #
27
- # Used by Stroma::DSL to include all registered modules in service classes.
28
- # Used by Stroma::Hooks::Factory to validate hook target keys.
29
- #
30
- # ## Thread Safety
31
- #
32
- # Registration must occur during single-threaded boot phase.
33
- # After finalization, all read operations are thread-safe.
26
+ # Created and owned by Matrix.
27
+ # Entries are accessed via Matrix#entries and Matrix#keys.
34
28
  class Registry
35
- include Singleton
29
+ # @!attribute [r] matrix_name
30
+ # @return [Symbol] The name of the owning matrix
31
+ attr_reader :matrix_name
36
32
 
37
- class << self
38
- delegate :register,
39
- :finalize!,
40
- :entries,
41
- :keys,
42
- :key?,
43
- to: :instance
44
- end
45
-
46
- def initialize
33
+ # Creates a new registry for the given matrix.
34
+ #
35
+ # @param matrix_name [Symbol, String] The matrix identifier
36
+ def initialize(matrix_name)
37
+ @matrix_name = matrix_name.to_sym
47
38
  @entries = []
48
39
  @finalized = false
49
40
  end
50
41
 
42
+ # Registers a DSL module with the given key.
43
+ #
44
+ # @param key [Symbol, String] The registry key
45
+ # @param extension [Module] The DSL module to register
46
+ # @raise [Exceptions::RegistryFrozen] If registry is finalized
47
+ # @raise [Exceptions::KeyAlreadyRegistered] If key already exists
48
+ # @return [void]
51
49
  def register(key, extension)
52
- raise Exceptions::RegistryFrozen, "Registry is finalized" if @finalized
50
+ if @finalized
51
+ raise Exceptions::RegistryFrozen,
52
+ "Registry for #{@matrix_name.inspect} is finalized"
53
+ end
53
54
 
55
+ key = key.to_sym
54
56
  if @entries.any? { |e| e.key == key }
55
- raise Exceptions::KeyAlreadyRegistered, "Key #{key.inspect} already registered"
57
+ raise Exceptions::KeyAlreadyRegistered,
58
+ "Key #{key.inspect} already registered in #{@matrix_name.inspect}"
56
59
  end
57
60
 
58
61
  @entries << Entry.new(key:, extension:)
59
62
  end
60
63
 
64
+ # Finalizes the registry, preventing further registrations.
65
+ #
66
+ # Idempotent - can be called multiple times safely.
67
+ #
68
+ # @return [void]
61
69
  def finalize!
62
70
  return if @finalized
63
71
 
64
72
  @entries.freeze
73
+ @keys = @entries.map(&:key).freeze
65
74
  @finalized = true
66
75
  end
67
76
 
77
+ # Returns all registered entries.
78
+ #
79
+ # @raise [Exceptions::RegistryNotFinalized] If not finalized
80
+ # @return [Array<Entry>] The registry entries
68
81
  def entries
69
82
  ensure_finalized!
70
83
  @entries
71
84
  end
72
85
 
86
+ # Returns all registered keys.
87
+ #
88
+ # @raise [Exceptions::RegistryNotFinalized] If not finalized
89
+ # @return [Array<Symbol>] The registry keys
73
90
  def keys
74
91
  ensure_finalized!
75
- @entries.map(&:key)
92
+ @keys
76
93
  end
77
94
 
95
+ # Checks if a key is registered.
96
+ #
97
+ # @param key [Symbol, String] The key to check
98
+ # @raise [Exceptions::RegistryNotFinalized] If not finalized
99
+ # @return [Boolean] true if the key is registered
78
100
  def key?(key)
79
101
  ensure_finalized!
80
- @entries.any? { |e| e.key == key }
102
+ @keys.include?(key.to_sym)
81
103
  end
82
104
 
83
105
  private
84
106
 
107
+ # Ensures the registry is finalized.
108
+ #
109
+ # @raise [Exceptions::RegistryNotFinalized] If not finalized
110
+ # @return [void]
85
111
  def ensure_finalized!
86
112
  return if @finalized
87
113
 
88
114
  raise Exceptions::RegistryNotFinalized,
89
- "Registry not finalized. Call Stroma::Registry.finalize! after registration."
115
+ "Registry for #{@matrix_name.inspect} not finalized"
90
116
  end
91
117
  end
92
118
  end
@@ -61,7 +61,7 @@ module Stroma
61
61
  # @return [void]
62
62
  def initialize_dup(original)
63
63
  super
64
- @storage = original.instance_variable_get(:@storage).transform_values(&:dup)
64
+ @storage = original.storage.transform_values(&:dup)
65
65
  end
66
66
 
67
67
  # Accesses or creates RegistrySettings for a registry key.
@@ -83,6 +83,10 @@ module Stroma
83
83
  def to_h
84
84
  @storage.transform_values(&:to_h)
85
85
  end
86
+
87
+ protected
88
+
89
+ attr_reader :storage
86
90
  end
87
91
  end
88
92
  end
@@ -59,7 +59,7 @@ module Stroma
59
59
  # @return [void]
60
60
  def initialize_dup(original)
61
61
  super
62
- @storage = original.instance_variable_get(:@storage).transform_values(&:dup)
62
+ @storage = original.storage.transform_values(&:dup)
63
63
  end
64
64
 
65
65
  # Accesses or creates a Setting for an extension.
@@ -81,6 +81,10 @@ module Stroma
81
81
  def to_h
82
82
  @storage.transform_values(&:to_h)
83
83
  end
84
+
85
+ protected
86
+
87
+ attr_reader :storage
84
88
  end
85
89
  end
86
90
  end
@@ -69,7 +69,7 @@ module Stroma
69
69
  # @return [void]
70
70
  def initialize_dup(original)
71
71
  super
72
- @data = deep_dup(original.instance_variable_get(:@data))
72
+ @data = deep_dup(original.data)
73
73
  end
74
74
 
75
75
  # Converts to a plain Hash.
@@ -93,6 +93,10 @@ module Stroma
93
93
  @data.fetch(key.to_sym, *args, &block)
94
94
  end
95
95
 
96
+ protected
97
+
98
+ attr_reader :data
99
+
96
100
  private
97
101
 
98
102
  # Recursively duplicates nested Hash and Array structures.
data/lib/stroma/state.rb CHANGED
@@ -60,8 +60,8 @@ module Stroma
60
60
  # @return [void]
61
61
  def initialize_dup(original)
62
62
  super
63
- @hooks = original.instance_variable_get(:@hooks).dup
64
- @settings = original.instance_variable_get(:@settings).dup
63
+ @hooks = original.hooks.dup
64
+ @settings = original.settings.dup
65
65
  end
66
66
  end
67
67
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ # Shared utility methods for the Stroma framework.
5
+ #
6
+ # ## Purpose
7
+ #
8
+ # Provides common helper methods used across multiple Stroma components.
9
+ # All methods are module functions - callable as both module methods
10
+ # and instance methods when included.
11
+ module Utils
12
+ module_function
13
+
14
+ # Assigns a temporary name to an anonymous module for debugging clarity.
15
+ # Uses set_temporary_name (Ruby 3.3+) when available.
16
+ #
17
+ # TODO: Remove the else branch when Ruby 3.2 support is dropped.
18
+ # The define_singleton_method fallback is a temporary workaround
19
+ # that only affects #inspect and #to_s. Unlike set_temporary_name,
20
+ # it does not set #name, so the module remains technically anonymous.
21
+ #
22
+ # @param mod [Module] The module to name
23
+ # @param name [String] The temporary name
24
+ # @return [void]
25
+ def name_module(mod, name)
26
+ if mod.respond_to?(:set_temporary_name)
27
+ mod.set_temporary_name(name)
28
+ else
29
+ mod.define_singleton_method(:inspect) { name }
30
+ mod.define_singleton_method(:to_s) { name }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,7 +3,7 @@
3
3
  module Stroma
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 3
6
+ MINOR = 5
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
data/lib/stroma.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  require "zeitwerk"
4
4
 
5
- require "active_support/all"
6
-
7
5
  loader = Zeitwerk::Loader.for_gem
8
6
  loader.ignore("#{__dir__}/stroma/test_kit/rspec")
9
7
  loader.inflector.inflect(
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stroma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: activesupport
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '5.1'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '5.1'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: zeitwerk
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -132,7 +118,7 @@ files:
132
118
  - README.md
133
119
  - Rakefile
134
120
  - lib/stroma.rb
135
- - lib/stroma/dsl.rb
121
+ - lib/stroma/dsl/generator.rb
136
122
  - lib/stroma/engine.rb
137
123
  - lib/stroma/entry.rb
138
124
  - lib/stroma/exceptions/base.rb
@@ -145,11 +131,13 @@ files:
145
131
  - lib/stroma/hooks/collection.rb
146
132
  - lib/stroma/hooks/factory.rb
147
133
  - lib/stroma/hooks/hook.rb
134
+ - lib/stroma/matrix.rb
148
135
  - lib/stroma/registry.rb
149
136
  - lib/stroma/settings/collection.rb
150
137
  - lib/stroma/settings/registry_settings.rb
151
138
  - lib/stroma/settings/setting.rb
152
139
  - lib/stroma/state.rb
140
+ - lib/stroma/utils.rb
153
141
  - lib/stroma/version.rb
154
142
  homepage: https://github.com/servactory/stroma
155
143
  licenses:
@@ -173,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
161
  - !ruby/object:Gem::Version
174
162
  version: '0'
175
163
  requirements: []
176
- rubygems_version: 3.7.2
164
+ rubygems_version: 4.0.6
177
165
  specification_version: 4
178
166
  summary: Foundation for building modular, extensible DSLs
179
167
  test_files: []
data/lib/stroma/dsl.rb DELETED
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Stroma
4
- # Main integration point between Stroma and service classes.
5
- #
6
- # ## Purpose
7
- #
8
- # Module that provides the core Stroma functionality to service classes:
9
- # - Includes all registered DSL modules
10
- # - Provides extensions block for hook registration
11
- # - Handles inheritance with proper state copying
12
- #
13
- # ## Usage
14
- #
15
- # Library authors create a DSL module that includes Stroma::DSL:
16
- #
17
- # ```ruby
18
- # module MyLib::DSL
19
- # def self.included(base)
20
- # base.include(Stroma::DSL)
21
- # end
22
- # end
23
- #
24
- # class MyLib::Base
25
- # include MyLib::DSL
26
- #
27
- # extensions do
28
- # before :actions, MyExtension
29
- # end
30
- # end
31
- # ```
32
- #
33
- # ## Extension Settings Access
34
- #
35
- # Extensions access their settings through the stroma.settings hierarchy:
36
- #
37
- # ```ruby
38
- # # In ClassMethods:
39
- # stroma.settings[:actions][:authorization][:method_name] = :authorize
40
- #
41
- # # In InstanceMethods:
42
- # self.class.stroma.settings[:actions][:authorization][:method_name]
43
- # ```
44
- #
45
- # ## Integration
46
- #
47
- # Included by service classes that want Stroma hook functionality.
48
- # Provides ClassMethods with: stroma, inherited, extensions.
49
- module DSL
50
- def self.included(base)
51
- base.extend(ClassMethods)
52
-
53
- Registry.entries.each do |entry|
54
- base.include(entry.extension)
55
- end
56
- end
57
-
58
- # Class-level methods for Stroma integration.
59
- #
60
- # ## Purpose
61
- #
62
- # Provides access to Stroma state and hooks DSL at the class level.
63
- # Handles proper duplication during inheritance.
64
- #
65
- # ## Key Methods
66
- #
67
- # - `stroma` - Access the State container
68
- # - `inherited` - Copy state to child classes
69
- # - `extensions` - DSL block for hook registration
70
- module ClassMethods
71
- def self.extended(base)
72
- base.instance_variable_set(:@stroma, State.new)
73
- end
74
-
75
- # Handles inheritance by duplicating Stroma state.
76
- #
77
- # Creates an independent copy of hooks and settings for the child class,
78
- # then applies all registered hooks to the child.
79
- #
80
- # @param child [Class] The child class being created
81
- # @return [void]
82
- def inherited(child)
83
- super
84
-
85
- child.instance_variable_set(:@stroma, stroma.dup)
86
-
87
- Hooks::Applier.new(child, child.stroma.hooks).apply!
88
- end
89
-
90
- # Returns the Stroma state for this service class.
91
- #
92
- # @return [State] The Stroma state container
93
- #
94
- # @example Accessing hooks
95
- # stroma.hooks.before(:actions)
96
- #
97
- # @example Accessing settings
98
- # stroma.settings[:actions][:authorization][:method_name]
99
- def stroma
100
- @stroma ||= State.new
101
- end
102
-
103
- private
104
-
105
- # DSL block for registering hooks.
106
- #
107
- # Evaluates the block in the context of a Hooks::Factory,
108
- # allowing before/after hook registration.
109
- #
110
- # @yield Block with before/after DSL calls
111
- # @return [void]
112
- #
113
- # @example
114
- # extensions do
115
- # before :actions, AuthorizationExtension
116
- # after :outputs, LoggingExtension
117
- # end
118
- def extensions(&block)
119
- @stroma_hooks_factory ||= Hooks::Factory.new(stroma.hooks)
120
- @stroma_hooks_factory.instance_eval(&block)
121
- end
122
- end
123
- end
124
- end