stroma 0.1.0 → 0.2.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: 68cbb11810b858b8c7ae037857a017f5a9c912b60016ecaf87d3e7d7c7f31ac3
4
- data.tar.gz: 02ef57657805b08bfed9b3c050621aa9f0b1f5afd8fc1461f4cf70c724ebfef4
3
+ metadata.gz: d3d83cec4af7649ab328cdae2c74b06a2f9d094cb755d469d594b0327fa1a279
4
+ data.tar.gz: 68191a403cccd3a6be19db18034df1b662d84dda0f587061c071b8e8bb34b787
5
5
  SHA512:
6
- metadata.gz: b0f29a4456d77075e4a13933ae67e2e28ef03de467b8dc67dffe3a967d35b8af46c9f9211a610421f93262848771a9df034d3e548dd6e31e856d7f09ac70e788
7
- data.tar.gz: 6659c659788fa76f2a730230fa38a0e92d009fcbe6ac1df9c7a5ec78ddd892dd3a97a6f0e96052f12d49cb144b74280dd89511efa0eeb31527ae9b98733d2726
6
+ metadata.gz: 29a687436d804c7677f2efcb4d51c26afd53511320a920da45e8d65b31b54d3c490cf936e11968d9a0e3cc5a90fdf1b2e68e9e0e0083cd6714d6e1241961c09e
7
+ data.tar.gz: eaba7ef983b606516eddd337205f6d7dd76845aff25bf3708fe6d1719629af013dff1a6e56ce3d038515e99911b6e952ee93ebdc3abc07bed6fc81fb97be71a1
data/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Stroma
1
+ <h1 align="center">Stroma</h1>
2
2
 
3
- Soon
3
+ <p align="center">
4
+ A foundation for building modular, extensible DSLs in Ruby.
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://rubygems.org/gems/stroma"><img src="https://img.shields.io/gem/v/stroma?logo=rubygems&logoColor=fff" alt="Gem version"></a>
9
+ <a href="https://github.com/servactory/stroma/releases"><img src="https://img.shields.io/github/release-date/servactory/stroma" alt="Release Date"></a>
10
+ <a href="https://rubygems.org/gems/stroma"><img src="https://img.shields.io/gem/dt/stroma" alt="Downloads"></a>
11
+ <a href="https://www.ruby-lang.org"><img src="https://img.shields.io/badge/Ruby-3.2+-red" alt="Ruby version"></a>
12
+ </p>
13
+
14
+ <!--
15
+ ## 📚 Documentation
16
+
17
+ See [stroma.servactory.com](https://stroma.servactory.com) for documentation, including:
18
+
19
+ - Architecture overview
20
+ - Registry and DSL modules
21
+ - Hooks and extensions
22
+ - Settings hierarchy
23
+ - API reference
24
+ -->
25
+
26
+ ## 💡 Why Stroma?
27
+
28
+ Building modular DSLs shouldn't require reinventing the wheel. Stroma provides a structured approach for library authors to compose DSL modules with:
29
+
30
+ - 🔌 **Module Registration** - Register DSL modules at boot time, compose them into a unified interface
31
+ - 🧱 **Structured Composition** - Include all registered modules automatically via single DSL entry point
32
+ - 🏛️ **Inheritance Safe** - Per-class state isolation with automatic deep copying
33
+ - 🪝 **Extension Hooks** - Optional before/after hooks for user customization
34
+ - ⚙️ **Extension Settings** - Three-level hierarchical storage for extension configuration
35
+ - 🔒 **Thread Safe** - Immutable registry after finalization, safe concurrent reads
36
+
37
+ ## 🧬 Concept
38
+
39
+ Stroma is a foundation for library authors building DSL-driven frameworks (service objects, form objects, decorators, etc.).
40
+
41
+ **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
45
+
46
+ ## 🚀 Quick Start
47
+
48
+ ### Installation
49
+
50
+ ```ruby
51
+ gem "stroma"
52
+ ```
53
+
54
+ ### Define your library's DSL
55
+
56
+ ```ruby
57
+ 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
67
+ end
68
+ end
69
+ ```
70
+
71
+ ### Create base class
72
+
73
+ ```ruby
74
+ module MyLib
75
+ class Base
76
+ include MyLib::DSL
77
+ end
78
+ end
79
+ ```
80
+
81
+ ### Usage
82
+
83
+ ```ruby
84
+ class UserService < MyLib::Base
85
+ input :email, type: String
86
+
87
+ make :create_user
88
+
89
+ private
90
+
91
+ def create_user
92
+ # implementation
93
+ end
94
+ end
95
+ ```
96
+
97
+ ## 🤝 Contributing
98
+
99
+ We welcome contributions! Check out our [Contributing Guide](https://github.com/servactory/stroma/blob/main/CONTRIBUTING.md) to get started.
100
+
101
+ **Ways to contribute:**
102
+ - 🐛 Report bugs and issues
103
+ - 💡 Suggest new features
104
+ - 📝 Improve documentation
105
+ - 🧪 Add test cases
106
+ - 🔧 Submit pull requests
107
+
108
+ ## 🙏 Acknowledgments
109
+
110
+ Special thanks to all our [contributors](https://github.com/servactory/stroma/graphs/contributors)!
111
+
112
+ ## 📄 License
113
+
114
+ Stroma is available as open source under the terms of the [MIT License](./LICENSE).
data/lib/stroma/dsl.rb ADDED
@@ -0,0 +1,124 @@
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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ # Represents a registered DSL entry in the Stroma registry.
5
+ #
6
+ # ## Purpose
7
+ #
8
+ # Immutable value object that holds information about a DSL module
9
+ # registered in the Stroma system. Each entry has a unique key
10
+ # and references a Module that will be included in service classes.
11
+ #
12
+ # ## Attributes
13
+ #
14
+ # - `key` (Symbol): Unique identifier for the DSL module (:inputs, :outputs, :actions)
15
+ # - `extension` (Module): The actual DSL module to be included
16
+ #
17
+ # ## Usage
18
+ #
19
+ # Entries are created internally by Registry.register:
20
+ #
21
+ # ```ruby
22
+ # Stroma::Registry.register(:inputs, MyInputsDSL)
23
+ # # Creates: Entry.new(key: :inputs, extension: MyInputsDSL)
24
+ # ```
25
+ #
26
+ # ## Immutability
27
+ #
28
+ # Entry is immutable (Data object) - once created, it cannot be modified.
29
+ Entry = Data.define(:key, :extension)
30
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Exceptions
5
+ # Base exception class for all Stroma-specific exceptions
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Serves as the parent class for all custom exceptions in the Stroma subsystem.
10
+ # Allows catching all Stroma-related exceptions with a single rescue clause.
11
+ #
12
+ # ## Usage
13
+ #
14
+ # All Stroma exceptions inherit from this base class:
15
+ #
16
+ # ```ruby
17
+ # begin
18
+ # Stroma::Registry.register(:custom, CustomModule)
19
+ # rescue Stroma::Exceptions::Base => e
20
+ # # Catches any Stroma-specific exception
21
+ # handle_stroma_error(e)
22
+ # end
23
+ # ```
24
+ #
25
+ # ## Integration
26
+ #
27
+ # Can be used in application error handlers for centralized error handling:
28
+ #
29
+ # ```ruby
30
+ # rescue_from Stroma::Exceptions::Base, with: :handle_stroma_error
31
+ # ```
32
+ #
33
+ # ## Subclasses
34
+ #
35
+ # - RegistryFrozen - Raised when modifying a finalized registry
36
+ # - RegistryNotFinalized - Raised when accessing registry before finalization
37
+ # - KeyAlreadyRegistered - Raised when registering a duplicate key
38
+ # - UnknownHookTarget - Raised when using an invalid hook target key
39
+ # - InvalidHookType - Raised when using an invalid hook type (:before/:after)
40
+ class Base < StandardError
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Exceptions
5
+ # Raised when an invalid hook type is provided.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Ensures that only valid hook types (:before, :after) are used
10
+ # when creating Stroma::Hooks::Hook objects. Provides fail-fast
11
+ # behavior during class definition rather than silent failures at runtime.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # # This will raise InvalidHookType:
17
+ # Stroma::Hooks::Hook.new(
18
+ # type: :invalid,
19
+ # target_key: :actions,
20
+ # extension: MyModule
21
+ # )
22
+ # # => Stroma::Exceptions::InvalidHookType:
23
+ # # Invalid hook type: :invalid. Valid types: :before, :after
24
+ # ```
25
+ class InvalidHookType < Base; end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Exceptions
5
+ # Raised when registering a duplicate key in the registry
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Indicates that a DSL module key has already been registered.
10
+ # Each DSL module must have a unique key in the registry.
11
+ #
12
+ # ## Usage
13
+ #
14
+ # Raised when attempting to register a duplicate key:
15
+ #
16
+ # ```ruby
17
+ # Stroma::Registry.register(:inputs, Inputs::DSL)
18
+ # Stroma::Registry.register(:inputs, AnotherModule)
19
+ # # Raises: Stroma::Exceptions::KeyAlreadyRegistered
20
+ # ```
21
+ #
22
+ # ## Integration
23
+ #
24
+ # This exception typically indicates a configuration error - each
25
+ # DSL module should only be registered once. Check for duplicate
26
+ # registrations in your initialization code.
27
+ class KeyAlreadyRegistered < Base
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Exceptions
5
+ # Raised when attempting to modify a finalized registry
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Indicates that the Stroma::Registry has been finalized and cannot accept
10
+ # new module registrations. The registry is finalized once during gem
11
+ # initialization and remains immutable thereafter.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # Raised when attempting to register modules after finalize!:
16
+ #
17
+ # ```ruby
18
+ # Stroma::Registry.finalize!
19
+ # Stroma::Registry.register(:custom, CustomModule)
20
+ # # Raises: Stroma::Exceptions::RegistryFrozen
21
+ # ```
22
+ #
23
+ # ## Integration
24
+ #
25
+ # This exception typically indicates a programming error - module
26
+ # registration should only occur during application boot, before
27
+ # any service classes are defined.
28
+ class RegistryFrozen < Base
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Exceptions
5
+ # Raised when accessing registry before finalization
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Indicates that the Stroma::Registry was accessed before finalize! was called.
10
+ # The registry must be finalized before it can be used to ensure all DSL modules
11
+ # are registered in the correct order.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # Raised when accessing registry methods before finalization:
16
+ #
17
+ # ```ruby
18
+ # # Before finalize! is called
19
+ # Stroma::Registry.entries
20
+ # # Raises: Stroma::Exceptions::RegistryNotFinalized
21
+ # ```
22
+ #
23
+ # ## Integration
24
+ #
25
+ # This exception typically indicates that Stroma::DSL module was not
26
+ # properly loaded. Ensure stroma gem is properly required before
27
+ # defining service classes.
28
+ class RegistryNotFinalized < Base
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Exceptions
5
+ # Raised when using an invalid hook target key
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Indicates that an unknown key was used as a hook target in the
10
+ # extensions block. Only registered DSL module keys can be used
11
+ # as hook targets.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # Raised when using an invalid key in extensions block:
16
+ #
17
+ # ```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
+ #
22
+ # extensions do
23
+ # before :unknown_key, SomeModule
24
+ # # Raises: Stroma::Exceptions::UnknownHookTarget
25
+ # end
26
+ # end
27
+ # ```
28
+ #
29
+ # ## Integration
30
+ #
31
+ # Valid hook target keys are determined by registered DSL modules.
32
+ # Check Stroma::Registry.keys for the list of valid targets.
33
+ class UnknownHookTarget < Base
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Hooks
5
+ # Applies registered hooks to a target class.
6
+ #
7
+ # ## Purpose
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.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # applier = Stroma::Hooks::Applier.new(ChildService, hooks)
17
+ # applier.apply!
18
+ # # ChildService now includes all hook modules
19
+ # ```
20
+ #
21
+ # ## Integration
22
+ #
23
+ # Called by Stroma::DSL.inherited after duplicating
24
+ # parent's configuration. Uses Registry.entries to determine
25
+ # hook application order.
26
+ class Applier
27
+ # Creates a new applier for applying hooks to a class.
28
+ #
29
+ # @param target_class [Class] The class to apply hooks to
30
+ # @param hooks [Collection] The hooks collection to apply
31
+ def initialize(target_class, hooks)
32
+ @target_class = target_class
33
+ @hooks = hooks
34
+ end
35
+
36
+ # Applies all registered hooks to the target class.
37
+ #
38
+ # For each registry entry, includes before hooks first,
39
+ # then after hooks. Does nothing if hooks collection is empty.
40
+ #
41
+ # @return [void]
42
+ #
43
+ # @example
44
+ # applier.apply!
45
+ # # Target class now includes all extension modules
46
+ def apply!
47
+ return if @hooks.empty?
48
+
49
+ Registry.entries.each do |entry|
50
+ @hooks.before(entry.key).each do |hook|
51
+ @target_class.include(hook.extension)
52
+ end
53
+
54
+ @hooks.after(entry.key).each do |hook|
55
+ @target_class.include(hook.extension)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Hooks
5
+ # Mutable collection manager for Hook objects.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Stores Hook objects and provides query methods to retrieve hooks
10
+ # by type and target key. Supports proper duplication during class
11
+ # inheritance to ensure configuration isolation.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # hooks = Stroma::Hooks::Collection.new
17
+ # hooks.add(:before, :actions, MyModule)
18
+ # hooks.add(:after, :actions, AnotherModule)
19
+ #
20
+ # hooks.before(:actions) # => [Hook(...)]
21
+ # hooks.after(:actions) # => [Hook(...)]
22
+ # hooks.empty? # => false
23
+ # hooks.size # => 2
24
+ # ```
25
+ #
26
+ # ## Integration
27
+ #
28
+ # Stored in Stroma::State and used by
29
+ # Stroma::Hooks::Applier to apply hooks to classes.
30
+ # Properly duplicated during class inheritance via initialize_dup.
31
+ class Collection
32
+ extend Forwardable
33
+
34
+ # @!method each
35
+ # Iterates over all hooks in the collection.
36
+ # @yield [Hook] Each hook in the collection
37
+ # @!method map
38
+ # Maps over all hooks in the collection.
39
+ # @yield [Hook] Each hook in the collection
40
+ # @return [Array] Mapped results
41
+ # @!method size
42
+ # Returns the number of hooks in the collection.
43
+ # @return [Integer] Number of hooks
44
+ # @!method empty?
45
+ # Checks if the collection is empty.
46
+ # @return [Boolean] true if no hooks registered
47
+ def_delegators :@collection, :each, :map, :size, :empty?
48
+
49
+ # Creates a new hooks collection.
50
+ #
51
+ # @param collection [Set] Initial collection of hooks (default: empty Set)
52
+ def initialize(collection = Set.new)
53
+ @collection = collection
54
+ end
55
+
56
+ # Creates a deep copy during inheritance.
57
+ #
58
+ # @param original [Collection] The original collection being duplicated
59
+ # @return [void]
60
+ def initialize_dup(original)
61
+ super
62
+ @collection = original.instance_variable_get(:@collection).dup
63
+ end
64
+
65
+ # Adds a new hook to the collection.
66
+ #
67
+ # @param type [Symbol] Hook type (:before or :after)
68
+ # @param target_key [Symbol] Registry key to hook into
69
+ # @param extension [Module] Extension module to include
70
+ # @return [Set] The updated collection
71
+ #
72
+ # @example
73
+ # hooks.add(:before, :actions, ValidationModule)
74
+ def add(type, target_key, extension)
75
+ @collection << Hook.new(type:, target_key:, extension:)
76
+ end
77
+
78
+ # Returns all before hooks for a given key.
79
+ #
80
+ # @param key [Symbol] The target key to filter by
81
+ # @return [Array<Hook>] Hooks that run before the target
82
+ #
83
+ # @example
84
+ # hooks.before(:actions) # => [Hook(type: :before, ...)]
85
+ def before(key)
86
+ @collection.select { |hook| hook.before? && hook.target_key == key }
87
+ end
88
+
89
+ # Returns all after hooks for a given key.
90
+ #
91
+ # @param key [Symbol] The target key to filter by
92
+ # @return [Array<Hook>] Hooks that run after the target
93
+ #
94
+ # @example
95
+ # hooks.after(:actions) # => [Hook(type: :after, ...)]
96
+ def after(key)
97
+ @collection.select { |hook| hook.after? && hook.target_key == key }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Hooks
5
+ # DSL interface for registering hooks in extensions block.
6
+ #
7
+ # ## Purpose
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.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # Used within `extensions` block in classes that include Stroma::DSL:
16
+ #
17
+ # ```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
+ #
22
+ # extensions do
23
+ # before :actions, ValidationModule
24
+ # after :outputs, LoggingModule
25
+ # end
26
+ # end
27
+ # ```
28
+ #
29
+ # ## Integration
30
+ #
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.
34
+ class Factory
35
+ # Creates a new factory for registering hooks.
36
+ #
37
+ # @param hooks [Collection] The hooks collection to add to
38
+ def initialize(hooks)
39
+ @hooks = hooks
40
+ end
41
+
42
+ # Registers one or more before hooks for a target key.
43
+ #
44
+ # @param key [Symbol] The registry key to hook before
45
+ # @param extensions [Array<Module>] Extension modules to include
46
+ # @raise [Exceptions::UnknownHookTarget] If key is not registered
47
+ #
48
+ # @example
49
+ # before :actions, ValidationModule, AuthorizationModule
50
+ def before(key, *extensions)
51
+ validate_key!(key)
52
+ extensions.each { |extension| @hooks.add(:before, key, extension) }
53
+ end
54
+
55
+ # Registers one or more after hooks for a target key.
56
+ #
57
+ # @param key [Symbol] The registry key to hook after
58
+ # @param extensions [Array<Module>] Extension modules to include
59
+ # @raise [Exceptions::UnknownHookTarget] If key is not registered
60
+ #
61
+ # @example
62
+ # after :outputs, LoggingModule, AuditModule
63
+ def after(key, *extensions)
64
+ validate_key!(key)
65
+ extensions.each { |extension| @hooks.add(:after, key, extension) }
66
+ end
67
+
68
+ private
69
+
70
+ # Validates that the key exists in the Registry.
71
+ #
72
+ # @param key [Symbol] The key to validate
73
+ # @raise [Exceptions::UnknownHookTarget] If key is not registered
74
+ def validate_key!(key)
75
+ return if Registry.key?(key)
76
+
77
+ raise Exceptions::UnknownHookTarget,
78
+ "Unknown hook target: #{key.inspect}. " \
79
+ "Valid keys: #{Registry.keys.map(&:inspect).join(', ')}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Hooks
5
+ # Valid hook types for Hook validation.
6
+ VALID_HOOK_TYPES = %i[before after].freeze
7
+ private_constant :VALID_HOOK_TYPES
8
+
9
+ # Immutable value object representing a hook configuration.
10
+ #
11
+ # ## Purpose
12
+ #
13
+ # Defines when and where an extension module should be included
14
+ # relative to a registered DSL module. Hook is immutable - once
15
+ # created, it cannot be modified.
16
+ #
17
+ # ## Attributes
18
+ #
19
+ # @!attribute [r] type
20
+ # @return [Symbol] Either :before or :after
21
+ # @!attribute [r] target_key
22
+ # @return [Symbol] Key of the DSL module to hook into (:inputs, :actions, etc.)
23
+ # @!attribute [r] extension
24
+ # @return [Module] The module to include at the hook point
25
+ #
26
+ # ## Usage
27
+ #
28
+ # ```ruby
29
+ # hook = Stroma::Hooks::Hook.new(
30
+ # type: :before,
31
+ # target_key: :actions,
32
+ # extension: MyExtension
33
+ # )
34
+ # hook.before? # => true
35
+ # hook.after? # => false
36
+ # ```
37
+ #
38
+ # ## Immutability
39
+ #
40
+ # Hook is a Data object - frozen and immutable after creation.
41
+ Hook = Data.define(:type, :target_key, :extension) do
42
+ # Initializes a new Hook with validation.
43
+ #
44
+ # @param type [Symbol] Hook type (:before or :after)
45
+ # @param target_key [Symbol] Registry key to hook into
46
+ # @param extension [Module] Extension module to include
47
+ # @raise [Exceptions::InvalidHookType] If type is invalid
48
+ def initialize(type:, target_key:, extension:)
49
+ if VALID_HOOK_TYPES.exclude?(type)
50
+ raise Exceptions::InvalidHookType,
51
+ "Invalid hook type: #{type.inspect}. Valid types: #{VALID_HOOK_TYPES.map(&:inspect).join(', ')}"
52
+ end
53
+
54
+ super
55
+ end
56
+
57
+ # Checks if this is a before hook.
58
+ #
59
+ # @return [Boolean] true if type is :before
60
+ def before?
61
+ type == :before
62
+ end
63
+
64
+ # Checks if this is an after hook.
65
+ #
66
+ # @return [Boolean] true if type is :after
67
+ def after?
68
+ type == :after
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ # Manages global registration of DSL modules for Stroma.
5
+ #
6
+ # ## Purpose
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.
11
+ #
12
+ # ## Usage
13
+ #
14
+ # ```ruby
15
+ # # During gem initialization:
16
+ # Stroma::Registry.register(:inputs, Inputs::DSL)
17
+ # Stroma::Registry.register(:outputs, Outputs::DSL)
18
+ # Stroma::Registry.finalize!
19
+ #
20
+ # # After finalization:
21
+ # Stroma::Registry.keys # => [:inputs, :outputs]
22
+ # Stroma::Registry.key?(:inputs) # => true
23
+ # ```
24
+ #
25
+ # ## Integration
26
+ #
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.
34
+ class Registry
35
+ include Singleton
36
+
37
+ class << self
38
+ delegate :register,
39
+ :finalize!,
40
+ :entries,
41
+ :keys,
42
+ :key?,
43
+ to: :instance
44
+ end
45
+
46
+ def initialize
47
+ @entries = []
48
+ @finalized = false
49
+ end
50
+
51
+ def register(key, extension)
52
+ raise Exceptions::RegistryFrozen, "Registry is finalized" if @finalized
53
+
54
+ if @entries.any? { |e| e.key == key }
55
+ raise Exceptions::KeyAlreadyRegistered, "Key #{key.inspect} already registered"
56
+ end
57
+
58
+ @entries << Entry.new(key:, extension:)
59
+ end
60
+
61
+ def finalize!
62
+ return if @finalized
63
+
64
+ @entries.freeze
65
+ @finalized = true
66
+ end
67
+
68
+ def entries
69
+ ensure_finalized!
70
+ @entries
71
+ end
72
+
73
+ def keys
74
+ ensure_finalized!
75
+ @entries.map(&:key)
76
+ end
77
+
78
+ def key?(key)
79
+ ensure_finalized!
80
+ @entries.any? { |e| e.key == key }
81
+ end
82
+
83
+ private
84
+
85
+ def ensure_finalized!
86
+ return if @finalized
87
+
88
+ raise Exceptions::RegistryNotFinalized,
89
+ "Registry not finalized. Call Stroma::Registry.finalize! after registration."
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Settings
5
+ # Top-level hierarchical container for extension settings.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Provides two-level hierarchical access to extension settings:
10
+ # registry_key -> extension_name -> setting values.
11
+ # Auto-vivifies RegistrySettings on first access.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # settings = Stroma::Settings::Collection.new
17
+ # settings[:actions][:authorization][:method_name] = :authorize
18
+ # settings[:actions][:transactional][:enabled] = true
19
+ #
20
+ # settings.keys # => [:actions]
21
+ # settings.empty? # => false
22
+ # settings.to_h # => { actions: { authorization: { method_name: :authorize }, ... } }
23
+ # ```
24
+ #
25
+ # ## Integration
26
+ #
27
+ # Stored in Stroma::State alongside Hooks::Collection.
28
+ # Accessed via `stroma.settings` in service classes.
29
+ # Properly duplicated during class inheritance via initialize_dup.
30
+ class Collection
31
+ extend Forwardable
32
+
33
+ # @!method each
34
+ # Iterates over all registry key settings.
35
+ # @yield [key, settings] Each registry key and its RegistrySettings
36
+ # @!method keys
37
+ # Returns all registry keys with settings.
38
+ # @return [Array<Symbol>] List of registry keys
39
+ # @!method size
40
+ # Returns the number of registry keys configured.
41
+ # @return [Integer] Number of registry keys
42
+ # @!method empty?
43
+ # Checks if no settings are configured.
44
+ # @return [Boolean] true if empty
45
+ # @!method map
46
+ # Maps over all registry key settings.
47
+ # @yield [key, settings] Each registry key and its RegistrySettings
48
+ # @return [Array] Mapped results
49
+ def_delegators :@storage, :each, :keys, :size, :empty?, :map
50
+
51
+ # Creates a new settings collection.
52
+ #
53
+ # @param storage [Hash] Initial storage (default: empty Hash)
54
+ def initialize(storage = {})
55
+ @storage = storage
56
+ end
57
+
58
+ # Creates a deep copy during inheritance.
59
+ #
60
+ # @param original [Collection] The original collection being duplicated
61
+ # @return [void]
62
+ def initialize_dup(original)
63
+ super
64
+ @storage = original.instance_variable_get(:@storage).transform_values(&:dup)
65
+ end
66
+
67
+ # Accesses or creates RegistrySettings for a registry key.
68
+ #
69
+ # Auto-vivifies a new RegistrySettings on first access.
70
+ #
71
+ # @param registry_key [Symbol] The registry key (e.g., :actions)
72
+ # @return [RegistrySettings] Settings for that registry key
73
+ #
74
+ # @example
75
+ # settings[:actions][:authorization][:method_name] = :authorize
76
+ def [](registry_key)
77
+ @storage[registry_key.to_sym] ||= RegistrySettings.new
78
+ end
79
+
80
+ # Converts to a nested Hash.
81
+ #
82
+ # @return [Hash] Deep nested hash of all settings
83
+ def to_h
84
+ @storage.transform_values(&:to_h)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Settings
5
+ # Collection of Setting objects for one registry key.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Groups extension settings by registry key (e.g., :actions).
10
+ # Provides auto-vivifying access to individual Setting objects.
11
+ # This is the middle layer in the settings hierarchy.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # settings = Stroma::Settings::RegistrySettings.new
17
+ # settings[:authorization][:method_name] = :authorize
18
+ # settings[:transactional][:enabled] = true
19
+ #
20
+ # settings.keys # => [:authorization, :transactional]
21
+ # settings.empty? # => false
22
+ # ```
23
+ #
24
+ # ## Integration
25
+ #
26
+ # Used by Collection as second-level container.
27
+ # Properly duplicated during class inheritance via initialize_dup.
28
+ class RegistrySettings
29
+ extend Forwardable
30
+
31
+ # @!method each
32
+ # Iterates over all extension settings.
33
+ # @yield [name, setting] Each extension name and its Setting
34
+ # @!method keys
35
+ # Returns all extension names.
36
+ # @return [Array<Symbol>] List of extension names
37
+ # @!method size
38
+ # Returns the number of extensions configured.
39
+ # @return [Integer] Number of extensions
40
+ # @!method empty?
41
+ # Checks if no extensions are configured.
42
+ # @return [Boolean] true if empty
43
+ # @!method map
44
+ # Maps over all extension settings.
45
+ # @yield [name, setting] Each extension name and its Setting
46
+ # @return [Array] Mapped results
47
+ def_delegators :@storage, :each, :keys, :size, :empty?, :map
48
+
49
+ # Creates a new registry settings container.
50
+ #
51
+ # @param storage [Hash] Initial storage (default: empty Hash)
52
+ def initialize(storage = {})
53
+ @storage = storage
54
+ end
55
+
56
+ # Creates a deep copy during inheritance.
57
+ #
58
+ # @param original [RegistrySettings] The original being duplicated
59
+ # @return [void]
60
+ def initialize_dup(original)
61
+ super
62
+ @storage = original.instance_variable_get(:@storage).transform_values(&:dup)
63
+ end
64
+
65
+ # Accesses or creates a Setting for an extension.
66
+ #
67
+ # Auto-vivifies a new Setting on first access.
68
+ #
69
+ # @param extension_name [Symbol] The extension name
70
+ # @return [Setting] The extension's setting container
71
+ #
72
+ # @example
73
+ # settings[:authorization][:method_name] = :authorize
74
+ def [](extension_name)
75
+ @storage[extension_name.to_sym] ||= Setting.new
76
+ end
77
+
78
+ # Converts to a nested Hash.
79
+ #
80
+ # @return [Hash] Nested hash of all settings
81
+ def to_h
82
+ @storage.transform_values(&:to_h)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ module Settings
5
+ # Dynamic key-value storage for extension configuration.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Provides a Hash-based container for storing extension-specific
10
+ # configuration data. Uses Forwardable delegation for consistent API.
11
+ # This is the leaf-level container in the settings hierarchy.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # setting = Stroma::Settings::Setting.new
17
+ # setting[:method_name] = :authorize
18
+ # setting[:method_name] # => :authorize
19
+ # setting.key?(:method_name) # => true
20
+ # ```
21
+ #
22
+ # ## Integration
23
+ #
24
+ # Used by RegistrySettings to store individual extension settings.
25
+ # Properly duplicated during class inheritance via initialize_dup.
26
+ class Setting
27
+ extend Forwardable
28
+
29
+ # @!method [](key)
30
+ # Retrieves a value by key.
31
+ # @param key [Symbol] The key to look up
32
+ # @return [Object, nil] The stored value or nil
33
+ # @!method []=(key, value)
34
+ # Stores a value by key.
35
+ # @param key [Symbol] The key to store under
36
+ # @param value [Object] The value to store
37
+ # @!method key?(key)
38
+ # Checks if a key exists.
39
+ # @param key [Symbol] The key to check
40
+ # @return [Boolean] true if key exists
41
+ # @!method keys
42
+ # Returns all stored keys.
43
+ # @return [Array<Symbol>] List of keys
44
+ # @!method each
45
+ # Iterates over all key-value pairs.
46
+ # @yield [key, value] Each stored pair
47
+ # @!method empty?
48
+ # Checks if no values are stored.
49
+ # @return [Boolean] true if empty
50
+ # @!method size
51
+ # Returns the number of stored values.
52
+ # @return [Integer] Number of entries
53
+ # @!method map
54
+ # Maps over all key-value pairs.
55
+ # @yield [key, value] Each stored pair
56
+ # @return [Array] Mapped results
57
+ def_delegators :@data, :[], :[]=, :key?, :keys, :each, :empty?, :size, :map
58
+
59
+ # Creates a new setting container.
60
+ #
61
+ # @param data [Hash] Initial data (default: empty Hash)
62
+ def initialize(data = {})
63
+ @data = data
64
+ end
65
+
66
+ # Creates a deep copy during inheritance.
67
+ #
68
+ # @param original [Setting] The original setting being duplicated
69
+ # @return [void]
70
+ def initialize_dup(original)
71
+ super
72
+ @data = deep_dup(original.instance_variable_get(:@data))
73
+ end
74
+
75
+ # Converts to a plain Hash.
76
+ #
77
+ # @return [Hash] Deep copy of internal data
78
+ def to_h
79
+ deep_dup(@data)
80
+ end
81
+
82
+ # Fetches a value with optional default.
83
+ #
84
+ # @param key [Symbol] The key to fetch
85
+ # @param args [Array] Optional default value
86
+ # @yield Optional block for default value
87
+ # @return [Object] The fetched value or default
88
+ #
89
+ # @example
90
+ # setting.fetch(:method_name, :default_method)
91
+ # setting.fetch(:method_name) { :computed_default }
92
+ def fetch(key, *args, &block)
93
+ @data.fetch(key.to_sym, *args, &block)
94
+ end
95
+
96
+ private
97
+
98
+ # Recursively duplicates nested Hash and Array structures.
99
+ #
100
+ # @param obj [Object] The object to duplicate
101
+ # @return [Object] Deep copy of the object
102
+ def deep_dup(obj)
103
+ case obj
104
+ when Hash then obj.transform_values { |v| deep_dup(v) }
105
+ when Array then obj.map { |v| deep_dup(v) }
106
+ else obj.respond_to?(:dup) ? obj.dup : obj
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stroma
4
+ # Holds the complete Stroma state for a service class.
5
+ #
6
+ # ## Purpose
7
+ #
8
+ # Central container that stores:
9
+ # - Hooks collection for before/after extension points
10
+ # - Settings collection for extension-specific configuration
11
+ #
12
+ # Each service class has its own State instance, duplicated during
13
+ # inheritance to ensure independent configuration.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Accessed via `stroma` method in classes that include Stroma::DSL.
18
+ # Library authors include Stroma::DSL in their DSL module:
19
+ #
20
+ # ```ruby
21
+ # # Library DSL module includes Stroma::DSL
22
+ # module MyLib::DSL
23
+ # def self.included(base)
24
+ # base.include(Stroma::DSL)
25
+ # end
26
+ # end
27
+ #
28
+ # # Library Base class includes library's DSL
29
+ # class MyLib::Base
30
+ # include MyLib::DSL
31
+ #
32
+ # stroma.hooks.before(:actions)
33
+ # stroma.settings[:actions][:authorization][:method_name]
34
+ # end
35
+ # ```
36
+ #
37
+ # ## Integration
38
+ #
39
+ # Stored as @stroma instance variable on each service class.
40
+ # Duplicated in DSL.inherited to provide inheritance isolation.
41
+ class State
42
+ # @!attribute [r] hooks
43
+ # @return [Hooks::Collection] The hooks collection for this class
44
+ # @!attribute [r] settings
45
+ # @return [Settings::Collection] The settings collection for this class
46
+ attr_reader :hooks, :settings
47
+
48
+ # Creates a new State with empty collections.
49
+ def initialize
50
+ @hooks = Hooks::Collection.new
51
+ @settings = Settings::Collection.new
52
+ end
53
+
54
+ # Creates a deep copy during inheritance.
55
+ #
56
+ # Ensures child classes have independent hooks and settings
57
+ # that don't affect the parent class.
58
+ #
59
+ # @param original [State] The original state being duplicated
60
+ # @return [void]
61
+ def initialize_dup(original)
62
+ super
63
+ @hooks = original.instance_variable_get(:@hooks).dup
64
+ @settings = original.instance_variable_get(:@settings).dup
65
+ end
66
+ end
67
+ end
@@ -3,7 +3,7 @@
3
3
  module Stroma
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 1
6
+ MINOR = 2
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '2.5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '13.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '13.2'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: rspec
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -79,7 +93,8 @@ dependencies:
79
93
  - - ">="
80
94
  - !ruby/object:Gem::Version
81
95
  version: '0.9'
82
- description: Stroma hook system
96
+ description: A structured approach to composing DSL modules for Ruby libraries with
97
+ optional extension hooks
83
98
  email:
84
99
  - profox.rus@gmail.com
85
100
  executables: []
@@ -89,7 +104,24 @@ files:
89
104
  - README.md
90
105
  - Rakefile
91
106
  - lib/stroma.rb
107
+ - lib/stroma/dsl.rb
92
108
  - lib/stroma/engine.rb
109
+ - lib/stroma/entry.rb
110
+ - lib/stroma/exceptions/base.rb
111
+ - lib/stroma/exceptions/invalid_hook_type.rb
112
+ - lib/stroma/exceptions/key_already_registered.rb
113
+ - lib/stroma/exceptions/registry_frozen.rb
114
+ - lib/stroma/exceptions/registry_not_finalized.rb
115
+ - lib/stroma/exceptions/unknown_hook_target.rb
116
+ - lib/stroma/hooks/applier.rb
117
+ - lib/stroma/hooks/collection.rb
118
+ - lib/stroma/hooks/factory.rb
119
+ - lib/stroma/hooks/hook.rb
120
+ - lib/stroma/registry.rb
121
+ - lib/stroma/settings/collection.rb
122
+ - lib/stroma/settings/registry_settings.rb
123
+ - lib/stroma/settings/setting.rb
124
+ - lib/stroma/state.rb
93
125
  - lib/stroma/version.rb
94
126
  homepage: https://github.com/servactory/stroma
95
127
  licenses:
@@ -115,5 +147,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
147
  requirements: []
116
148
  rubygems_version: 3.7.2
117
149
  specification_version: 4
118
- summary: Stroma hook system
150
+ summary: Foundation for building modular, extensible DSLs
119
151
  test_files: []