A foundation for building modular, extensible DSLs in Ruby.
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
Stroma is a foundation for library authors building DSL-driven frameworks (service objects, form objects, decorators, etc.).
Core lifecycle:
- Define - Create a Matrix with DSL modules at boot time
- Include - Classes include the matrix's DSL to gain all modules
- Extend (optional) - Add cross-cutting logic via
before/afterhooks
spec.add_dependency "stroma", ">= 0.4"module MyLib
STROMA = Stroma::Matrix.define(:my_lib) do
register :inputs, MyLib::Inputs::DSL
register :actions, MyLib::Actions::DSL
end
private_constant :STROMA
endmodule MyLib
class Base
include STROMA.dsl
end
endCreate an intermediate class with lifecycle hooks:
class ApplicationService < MyLib::Base
# Add lifecycle hooks (optional)
extensions do
before :actions, ApplicationService::Extensions::Rollbackable::DSL
end
endBuild services that inherit extension functionality:
class UserService < ApplicationService
# DSL method from Rollbackable extension
on_rollback(...)
input :email, type: String
make :create_user
private
def create_user
# implementation
end
endExtensions allow you to add cross-cutting concerns like transactions, authorization, and rollback support. See extension examples for implementation details.
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.
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
endClassMethods 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.
class ApplicationService < MyLib::Base
extensions do
before :actions, Authorization
end
endbefore 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.
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
endSettings and hooks are deep-copied on inheritance β each subclass has independent configuration.
- Servactory β Service objects framework for Ruby applications
We welcome contributions! Check out our Contributing Guide to get started.
Ways to contribute:
- π Report bugs and issues
- π‘ Suggest new features
- π Improve documentation
- π§ͺ Add test cases
- π§ Submit pull requests
Special thanks to all our contributors!
Stroma is available as open source under the terms of the MIT License.