https://github.com/servactory/stroma
A foundation for building modular, extensible DSLs in Ruby
https://github.com/servactory/stroma
dsl gem modular ruby ruby-dsl ruby-gem
Last synced: 28 days ago
JSON representation
A foundation for building modular, extensible DSLs in Ruby
- Host: GitHub
- URL: https://github.com/servactory/stroma
- Owner: servactory
- License: mit
- Created: 2026-01-06T10:43:43.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-02-13T21:27:12.000Z (29 days ago)
- Last Synced: 2026-02-13T22:52:11.537Z (29 days ago)
- Topics: dsl, gem, modular, ruby, ruby-dsl, ruby-gem
- Language: Ruby
- Homepage:
- Size: 62.5 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
A foundation for building modular, extensible DSLs in Ruby.
## ๐ก Why Stroma?
Building modular DSLs shouldn't require reinventing the wheel. Stroma provides a structured approach for library authors to compose DSL modules with:
- ๐ **Module Registration** - Register DSL modules at boot time, compose them into a unified interface
- ๐งฑ **Structured Composition** - Include all registered modules automatically via single DSL entry point
- ๐๏ธ **Inheritance Safe** - Per-class state isolation with automatic deep copying
- ๐ช **Extension Hooks** - Optional before/after hooks for user customization
- โ๏ธ **Extension Settings** - Three-level hierarchical storage for extension configuration
- ๐ **Thread Safe** - Immutable registry after finalization, safe concurrent reads
## ๐งฌ Concept
Stroma is a foundation for library authors building DSL-driven frameworks (service objects, form objects, decorators, etc.).
**Core lifecycle:**
1. **Define** - Create a Matrix with DSL modules at boot time
2. **Include** - Classes include the matrix's DSL to gain all modules
3. **Extend** (optional) - Add cross-cutting logic via `before`/`after` hooks
## ๐ Quick Start
### Installation
```ruby
spec.add_dependency "stroma", ">= 0.4"
```
### Define your library's DSL
```ruby
module MyLib
STROMA = Stroma::Matrix.define(:my_lib) do
register :inputs, MyLib::Inputs::DSL
register :actions, MyLib::Actions::DSL
end
private_constant :STROMA
end
```
### Create base class
```ruby
module MyLib
class Base
include STROMA.dsl
end
end
```
### Usage
Create an intermediate class with lifecycle hooks:
```ruby
class ApplicationService < MyLib::Base
# Add lifecycle hooks (optional)
extensions do
before :actions, ApplicationService::Extensions::Rollbackable::DSL
end
end
```
Build services that inherit extension functionality:
```ruby
class UserService < ApplicationService
# DSL method from Rollbackable extension
on_rollback(...)
input :email, type: String
make :create_user
private
def create_user
# implementation
end
end
```
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.
## ๐งฉ Building Extensions
Extensions are standard Ruby modules that hook into the DSL lifecycle. Stroma places them at the correct position in the method chain, so `super` naturally flows through all registered extensions.
### Define an extension
```ruby
module Authorization
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
def authorize_with(method_name)
stroma.settings[:actions][:authorization][:method_name] = method_name
end
end
module InstanceMethods
def call(...)
method_name = self.class.stroma.settings[:actions][:authorization][:method_name]
send(method_name) if method_name
super
end
end
end
```
`ClassMethods` provides the class-level DSL. `InstanceMethods` overrides the orchestrator method defined by your library (here `call`) and delegates via `super`. Split them into separate files as the extension grows.
### Register the extension
```ruby
class ApplicationService < MyLib::Base
extensions do
before :actions, Authorization
end
end
```
`before` places the module so its `call` executes **before** the `:actions` entry. Use `after` for post-processing. Multiple modules in one call: `before :actions, ModA, ModB`.
### Use in a service
```ruby
class UserService < ApplicationService
authorize_with :check_permissions
input :email, type: String
make :create_user
private
def check_permissions
# authorization logic
end
def create_user
# runs only after check_permissions passes
end
end
```
Settings and hooks are deep-copied on inheritance โ each subclass has independent configuration.
## ๐ Projects Using Stroma
- [Servactory](https://github.com/servactory/servactory) โ Service objects framework for Ruby applications
## ๐ค Contributing
We welcome contributions! Check out our [Contributing Guide](https://github.com/servactory/stroma/blob/main/CONTRIBUTING.md) to get started.
**Ways to contribute:**
- ๐ Report bugs and issues
- ๐ก Suggest new features
- ๐ Improve documentation
- ๐งช Add test cases
- ๐ง Submit pull requests
## ๐ Acknowledgments
Special thanks to all our [contributors](https://github.com/servactory/stroma/graphs/contributors)!
## ๐ License
Stroma is available as open source under the terms of the [MIT License](./LICENSE).