From a5ee1a9c1e319d2ec729403df9bd6d4e221ac5a4 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 19:18:55 +0300 Subject: [PATCH 1/3] Add "Building Extensions" guide to README - Introduce a comprehensive guide for creating and utilizing extensions. - Detail the process of defining, registering, and using extensions with concise Ruby code examples. - Highlight the flexibility of extensions for lifecycle hooks like authorization. - Emphasize deep-copying of settings and independent configuration in subclasses. --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index 48d2072..3d4fc41 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,69 @@ 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 + # Class-level DSL method — available in service definitions + def self.included(base) + base.class_eval do + def self.authorize_with(method_name) + stroma.settings[:actions][:authorization][:method_name] = method_name + end + end + end + + # Instance-level hook — runs when the service is called + def call(...) + method_name = self.class.stroma.settings[:actions][:authorization][:method_name] + send(method_name) if method_name + super + end +end +``` + +`self.included` adds a class-level DSL method. The `call(...)` method reads the stored setting and delegates via `super`. + +### 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 From a5e64fdb8fe7f5c071617887815f9ad6c1bea5a8 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 19:29:42 +0300 Subject: [PATCH 2/3] Refactor `Authorization` module for improved extensibility - Split `ClassMethods` and `InstanceMethods` into separate modules to ensure clearer separation of concerns. - Update `self.included` to include the newly added modules for better modularity. - Adjust `call(...)` method to hook into the chain via `InstanceMethods` using `super`. - Lay groundwork for extending the implementation as the extension grows. --- README.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3d4fc41..d6ab14e 100644 --- a/README.md +++ b/README.md @@ -123,25 +123,28 @@ Extensions are standard Ruby modules that hook into the DSL lifecycle. Stroma pl ```ruby module Authorization - # Class-level DSL method — available in service definitions def self.included(base) - base.class_eval do - def self.authorize_with(method_name) - stroma.settings[:actions][:authorization][:method_name] = method_name - end + base.extend(ClassMethods) + base.include(InstanceMethods) + end + + module ClassMethods + def authorize_with(method_name) + stroma.settings[:actions][:authorization][:method_name] = method_name end end - # Instance-level hook — runs when the service is called - def call(...) - method_name = self.class.stroma.settings[:actions][:authorization][:method_name] - send(method_name) if method_name - super + module InstanceMethods + def call(...) + method_name = self.class.stroma.settings[:actions][:authorization][:method_name] + send(method_name) if method_name + super + end end end ``` -`self.included` adds a class-level DSL method. The `call(...)` method reads the stored setting and delegates via `super`. +`ClassMethods` provides the class-level DSL. `InstanceMethods` hooks into the call chain via `super`. Split them into separate files as the extension grows. ### Register the extension @@ -162,6 +165,7 @@ class UserService < ApplicationService authorize_with :check_permissions input :email, type: String + make :create_user private From cf9d7b1d6c7876a3e660702fe4b27f4978888fb8 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 19:32:22 +0300 Subject: [PATCH 3/3] Clarify `InstanceMethods` role in orchestration logic - Update README to refine explanation of `InstanceMethods` behavior. - Specify that `InstanceMethods` overrides the orchestrator method (`call`) and delegates via `super`. - Reinforce the recommendation to split `ClassMethods` and `InstanceMethods` into separate files for maintainability. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6ab14e..a4052f0 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ module Authorization end ``` -`ClassMethods` provides the class-level DSL. `InstanceMethods` hooks into the call chain via `super`. Split them into separate files as the extension grows. +`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