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"moduleMyLibSTROMA=Stroma::Matrix.define(:my_lib)doregister:inputs,MyLib::Inputs::DSLregister:actions,MyLib::Actions::DSLendprivate_constant:STROMAendmoduleMyLibclassBaseincludeSTROMA.dslendendCreate an intermediate class with lifecycle hooks:
classApplicationService < MyLib::Base# Add lifecycle hooks (optional)extensionsdobefore:actions,ApplicationService::Extensions::Rollbackable::DSLendendBuild services that inherit extension functionality:
classUserService < ApplicationService# DSL method from Rollbackable extensionon_rollback(...)input:email,type: Stringmake:create_userprivatedefcreate_user# implementationendendExtensions 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.
moduleAuthorizationdefself.included(base)base.extend(ClassMethods)base.include(InstanceMethods)endmoduleClassMethodsdefauthorize_with(method_name)stroma.settings[:actions][:authorization][:method_name]=method_nameendendmoduleInstanceMethodsdefcall(...)method_name=self.class.stroma.settings[:actions][:authorization][:method_name]send(method_name)ifmethod_namesuperendendendClassMethods 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.
classApplicationService < MyLib::Baseextensionsdobefore:actions,Authorizationendendbefore 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.
classUserService < ApplicationServiceauthorize_with:check_permissionsinput:email,type: Stringmake:create_userprivatedefcheck_permissions# authorization logicenddefcreate_user# runs only after check_permissions passesendendSettings 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.