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:
- 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
๐ Quick Start
Installation
spec.add_dependency "stroma", ">= 0.4"
Define your library's DSL
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
module MyLib class Base include STROMA.dsl end end
Usage
Create an intermediate class with lifecycle hooks:
class ApplicationService < MyLib::Base # Add lifecycle hooks (optional) extensions do before :actions, ApplicationService::Extensions::Rollbackable::DSL end end
Build 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 end
Extensions allow you to add cross-cutting concerns like transactions, authorization, and rollback support. See extension examples 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
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
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
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 โ Service objects framework for Ruby applications
๐ค Contributing
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
๐ Acknowledgments
Special thanks to all our contributors!
๐ License
Stroma is available as open source under the terms of the MIT License.