An open API service indexing awesome lists of open source software.

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

Awesome Lists containing this project

README

          






Stroma



A foundation for building modular, extensible DSLs in Ruby.


Gem version
Release Date
Downloads
Ruby version

## ๐Ÿ’ก 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).