stroma 0.4.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 +32 -2
- data/lib/stroma/dsl/generator.rb +27 -3
- data/lib/stroma/hooks/applier.rb +53 -8
- data/lib/stroma/hooks/collection.rb +5 -1
- data/lib/stroma/hooks/hook.rb +1 -1
- data/lib/stroma/registry.rb +3 -2
- 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 +3 -16
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.
|
|
@@ -75,8 +83,24 @@ end
|
|
|
75
83
|
|
|
76
84
|
### Usage
|
|
77
85
|
|
|
86
|
+
Create an intermediate class with lifecycle hooks:
|
|
87
|
+
|
|
78
88
|
```ruby
|
|
79
|
-
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
|
+
|
|
80
104
|
input :email, type: String
|
|
81
105
|
|
|
82
106
|
make :create_user
|
|
@@ -89,6 +113,12 @@ class UserService < MyLib::Base
|
|
|
89
113
|
end
|
|
90
114
|
```
|
|
91
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
|
+
|
|
92
122
|
## 🤝 Contributing
|
|
93
123
|
|
|
94
124
|
We welcome contributions! Check out our [Contributing Guide](https://github.com/servactory/stroma/blob/main/CONTRIBUTING.md) to get started.
|
data/lib/stroma/dsl/generator.rb
CHANGED
|
@@ -16,6 +16,19 @@ module Stroma
|
|
|
16
16
|
# - ServiceClass gets @stroma_matrix (same reference)
|
|
17
17
|
# - ServiceClass gets @stroma (unique State per class)
|
|
18
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
|
+
#
|
|
19
32
|
# ## Usage
|
|
20
33
|
#
|
|
21
34
|
# ```ruby
|
|
@@ -59,11 +72,11 @@ module Stroma
|
|
|
59
72
|
# - extensions DSL for registering hooks
|
|
60
73
|
#
|
|
61
74
|
# @return [Module] The generated DSL module
|
|
62
|
-
def generate # rubocop:disable Metrics/MethodLength
|
|
75
|
+
def generate # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
63
76
|
matrix = @matrix
|
|
64
77
|
class_methods = build_class_methods
|
|
65
78
|
|
|
66
|
-
Module.new do
|
|
79
|
+
mod = Module.new do
|
|
67
80
|
@stroma_matrix = matrix
|
|
68
81
|
|
|
69
82
|
class << self
|
|
@@ -75,12 +88,23 @@ module Stroma
|
|
|
75
88
|
base.instance_variable_set(:@stroma_matrix, mtx)
|
|
76
89
|
base.instance_variable_set(:@stroma, State.new)
|
|
77
90
|
|
|
78
|
-
|
|
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) }
|
|
79
98
|
end
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
const_set(:ClassMethods, class_methods)
|
|
83
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
|
|
84
108
|
end
|
|
85
109
|
|
|
86
110
|
private
|
data/lib/stroma/hooks/applier.rb
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
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
|
#
|
|
@@ -50,16 +52,59 @@ module Stroma
|
|
|
50
52
|
|
|
51
53
|
# Applies all registered hooks to the target class.
|
|
52
54
|
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
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
|
|
55
59
|
#
|
|
56
60
|
# @return [void]
|
|
57
61
|
def apply!
|
|
58
62
|
return if @hooks.empty?
|
|
59
63
|
|
|
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
|
|
98
|
+
|
|
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
|
|
60
105
|
@matrix.entries.each do |entry|
|
|
61
|
-
@hooks.before(entry.key).
|
|
62
|
-
@hooks.after(entry.key).
|
|
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) }
|
|
63
108
|
end
|
|
64
109
|
end
|
|
65
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/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
|
data/lib/stroma/registry.rb
CHANGED
|
@@ -70,6 +70,7 @@ module Stroma
|
|
|
70
70
|
return if @finalized
|
|
71
71
|
|
|
72
72
|
@entries.freeze
|
|
73
|
+
@keys = @entries.map(&:key).freeze
|
|
73
74
|
@finalized = true
|
|
74
75
|
end
|
|
75
76
|
|
|
@@ -88,7 +89,7 @@ module Stroma
|
|
|
88
89
|
# @return [Array<Symbol>] The registry keys
|
|
89
90
|
def keys
|
|
90
91
|
ensure_finalized!
|
|
91
|
-
@
|
|
92
|
+
@keys
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
# Checks if a key is registered.
|
|
@@ -98,7 +99,7 @@ module Stroma
|
|
|
98
99
|
# @return [Boolean] true if the key is registered
|
|
99
100
|
def key?(key)
|
|
100
101
|
ensure_finalized!
|
|
101
|
-
@
|
|
102
|
+
@keys.include?(key.to_sym)
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
private
|
|
@@ -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
|
|
@@ -151,6 +137,7 @@ files:
|
|
|
151
137
|
- lib/stroma/settings/registry_settings.rb
|
|
152
138
|
- lib/stroma/settings/setting.rb
|
|
153
139
|
- lib/stroma/state.rb
|
|
140
|
+
- lib/stroma/utils.rb
|
|
154
141
|
- lib/stroma/version.rb
|
|
155
142
|
homepage: https://github.com/servactory/stroma
|
|
156
143
|
licenses:
|
|
@@ -174,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
174
161
|
- !ruby/object:Gem::Version
|
|
175
162
|
version: '0'
|
|
176
163
|
requirements: []
|
|
177
|
-
rubygems_version:
|
|
164
|
+
rubygems_version: 4.0.6
|
|
178
165
|
specification_version: 4
|
|
179
166
|
summary: Foundation for building modular, extensible DSLs
|
|
180
167
|
test_files: []
|