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 +4 -4
- data/README.md +41 -16
- data/lib/stroma/dsl/generator.rb +140 -0
- data/lib/stroma/hooks/applier.rb +74 -23
- data/lib/stroma/hooks/collection.rb +5 -1
- data/lib/stroma/hooks/factory.rb +17 -18
- data/lib/stroma/hooks/hook.rb +1 -1
- data/lib/stroma/matrix.rb +114 -0
- data/lib/stroma/registry.rb +60 -34
- data/lib/stroma/settings/collection.rb +5 -1
- data/lib/stroma/settings/registry_settings.rb +5 -1
- data/lib/stroma/settings/setting.rb +5 -1
- data/lib/stroma/state.rb +2 -2
- data/lib/stroma/utils.rb +34 -0
- data/lib/stroma/version.rb +1 -1
- data/lib/stroma.rb +0 -2
- metadata +5 -17
- data/lib/stroma/dsl.rb +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3651f535308d76dc210bfb0d49b87179e20ea806e4bb8032eee5165c7f23d635
|
|
4
|
+
data.tar.gz: 5083f7bc1e8541832be170ee35eb9ddf4852b5fd90e76af8c6d7303066e2a927
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b56c004b08152484972a38b47919c5b1b299d9316bbbcebc2c533190ea766dee62a9882d81ca97281874c4d47bf8e4447fa31142bd51d9329d2c8a17fe9c0da
|
|
7
|
+
data.tar.gz: cee73e3a9040cf6da488c3326333618d30128dc448179e9d184b991d8602ea7da3ebfcbd50eab9e1d6559b5b14214aaed90f42ad1a6e3757d435933ba3f4f12b
|
data/README.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
<
|
|
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. **
|
|
43
|
-
2. **
|
|
44
|
-
3. **Extend** (optional) -
|
|
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.
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/stroma/hooks/applier.rb
CHANGED
|
@@ -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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
#
|
|
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
|
|
24
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
39
|
-
#
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
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
|
data/lib/stroma/hooks/factory.rb
CHANGED
|
@@ -6,21 +6,16 @@ module Stroma
|
|
|
6
6
|
#
|
|
7
7
|
# ## Purpose
|
|
8
8
|
#
|
|
9
|
-
# Provides
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
#
|
|
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
|
|
32
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
74
|
+
return if @matrix.key?(key)
|
|
76
75
|
|
|
77
76
|
raise Exceptions::UnknownHookTarget,
|
|
78
|
-
"Unknown hook target
|
|
79
|
-
"Valid
|
|
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
|
data/lib/stroma/hooks/hook.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/stroma/registry.rb
CHANGED
|
@@ -1,92 +1,118 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Stroma
|
|
4
|
-
# Manages
|
|
4
|
+
# Manages registration of DSL modules for a specific matrix.
|
|
5
5
|
#
|
|
6
6
|
# ## Purpose
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
-
# #
|
|
21
|
-
#
|
|
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
|
-
#
|
|
28
|
-
#
|
|
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
|
-
|
|
29
|
+
# @!attribute [r] matrix_name
|
|
30
|
+
# @return [Symbol] The name of the owning matrix
|
|
31
|
+
attr_reader :matrix_name
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
-
@settings = original.
|
|
63
|
+
@hooks = original.hooks.dup
|
|
64
|
+
@settings = original.settings.dup
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
end
|
data/lib/stroma/utils.rb
ADDED
|
@@ -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
|
data/lib/stroma/version.rb
CHANGED
data/lib/stroma.rb
CHANGED
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.
|
|
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:
|
|
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
|