-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e57f4c8
commit 758c5b1
Showing
6 changed files
with
467 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
--- | ||
title: Configuration | ||
layout: gem-single | ||
name: dry-operation | ||
--- | ||
|
||
By default, `dry-operation` automatically wraps the `#call` method of your operations with failure tracking and error handling. This means you can use `#step` directly in your `#call` method without explicitly wrapping it in an otherwise necessary `steps do ... end` block. | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
def call(input) | ||
# This works automatically | ||
user = step create_user(input) | ||
step notify(user) | ||
user | ||
end | ||
end | ||
``` | ||
|
||
### Customizing wrapped methods | ||
|
||
You can customize which methods get automatically wrapped using the `.operate_on` class method: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
# Wrap both #call and #process methods | ||
operate_on :call, :process | ||
|
||
def call(input) | ||
step validate(input) | ||
end | ||
|
||
def process(input) | ||
step transform(input) | ||
end | ||
end | ||
``` | ||
|
||
### Disabling automatic wrapping | ||
|
||
If you want complete control over method wrapping, you can disable the automatic wrapping entirely using `.skip_prepending`. In that case, you'll need to wrap your methods manually with `steps do ... end`: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
skip_prepending | ||
|
||
def call(input) | ||
# Now you must explicitly wrap steps | ||
steps do | ||
user = step create_user(input) | ||
step notify(user) | ||
user | ||
end | ||
end | ||
end | ||
``` | ||
|
||
### Inheritance behaviour | ||
|
||
Both `.operate_on` and `.skip_prepending` configurations are inherited by subclasses. This means: | ||
|
||
- If a parent class configures certain methods to be wrapped, subclasses will inherit that configuration | ||
|
||
- If a parent class skips prepending, subclasses will also skip prepending | ||
|
||
- Subclasses can override their parent's configuration by calling `.operate_on` or `.skip_prepending` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
title: Design Pattern | ||
layout: gem-single | ||
name: dry-operation | ||
--- | ||
|
||
`dry-operation` implements a pattern that closely resembles monadic composition, particularly the `Result` monad, and the Railway-oriented Programming pattern. Understanding these monadic concepts can provide deeper insight into how `dry-operation` works and why it's designed this way. | ||
|
||
### Monadic Composition | ||
|
||
In functional programming, a monad is a structure that represents computations defined as sequences of steps. A key feature of monads is their ability to chain operations, with each operation depending on the result of the previous one. | ||
|
||
`dry-operation` emulates this monadic behavior through its `#step` method and the overall structure of operations. | ||
|
||
In monadic terms, the `#step` method in `Dry::Operation` acts similarly to the `bind` operation: | ||
|
||
1. It takes a computation that may succeed or fail (returning `Success` or `Failure`). | ||
|
||
1. If the computation succeeds, it extracts the value and passes it to the next step. | ||
|
||
1. If the computation fails, it short-circuits the entire operation, skipping subsequent steps. | ||
|
||
This behavior allows for clean composition of operations while handling potential failures at each step. | ||
|
||
### Railway-oriented Programming | ||
|
||
The design of `dry-operation` closely follows the concept of Railway-oriented Programming, a way of structuring code that's especially useful for dealing with a series of operations that may fail. | ||
|
||
In this model: | ||
|
||
- The "happy path" (all operations succeed) is one track of the railway. | ||
|
||
- The "failure path" (any operation fails) is another track. | ||
|
||
Each step is like a switch on the railway, potentially diverting from the success track to the failure track. | ||
|
||
`dry-operation` implements this pattern by allowing the success case to continue down the method, while immediately returning any failure, effectively "switching tracks". |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
--- | ||
title: Error Handling | ||
layout: gem-single | ||
name: dry-operation | ||
--- | ||
When using `dry-operation`, errors are handled through the `Failure` type from [`dry-monads`](/gems/dry-monads/). Each step in your operation should return either a `Success` or `Failure` result. When a step returns a `Failure`, the operation short-circuits, skipping the remaining steps and returning the failure immediately. | ||
|
||
You'll usually handle the failure from the call site, where you can pattern match on the result to handle success and failure cases. However, sometimes it's useful to encapsulate some error handling logic within the operation itself. | ||
|
||
### Global error handling | ||
|
||
You can define a global failure handler by implementing an `#on_failure` method in your operation class: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
def call(input) | ||
attrs = step validate(input) | ||
user = step persist(attrs) | ||
step notify(user) | ||
user | ||
end | ||
|
||
def on_failure(failure) | ||
# Log or handle the failure globally | ||
logger.error("Operation failed: #{failure}") | ||
end | ||
end | ||
``` | ||
|
||
The `#on_failure` method can optionally accept a second argument that indicates which method encountered the failure, allowing you more granular control over error handling: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
def call(input) | ||
attrs = step validate(input) | ||
user = step persist(attrs) | ||
step notify(user) | ||
user | ||
end | ||
|
||
def on_failure(failure, step_name) | ||
case step_name | ||
when :validate | ||
logger.error("Validation failed: #{failure}") | ||
when :persist | ||
logger.error("Persistence failed: #{failure}") | ||
when :notify | ||
logger.error("Notification failed: #{failure}") | ||
end | ||
end | ||
end | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
--- | ||
title: Extensions | ||
layout: gem-single | ||
name: dry-operation | ||
--- | ||
|
||
### ROM | ||
|
||
The `ROM` extension adds transaction support to your operations when working with [`rom-rb.org`](https://rom-rb.org). When a step returns a `Failure`, the transaction will automatically roll back, ensuring data consistency. | ||
|
||
First, make sure you have `rom-sql` installed: | ||
|
||
```ruby | ||
gem 'rom-sql' | ||
``` | ||
|
||
Include the extension in your operation class and provide access to the ROM container: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
include Dry::Operation::Extensions::ROM | ||
|
||
attr_reader :rom | ||
|
||
def initialize(rom:) | ||
@rom = rom | ||
super() | ||
end | ||
|
||
def call(input) | ||
transaction do | ||
user = step create_user(input) | ||
step assign_role(user) | ||
user | ||
end | ||
end | ||
|
||
private | ||
|
||
def create_user(input) | ||
Success(rom.relations[:users].command(:create).(input)) | ||
end | ||
|
||
def assign_role(user) | ||
Success(rom.relations[:roles].command(:create).(user_id: user.id, name: 'member')) | ||
end | ||
end | ||
``` | ||
|
||
By default, the `:default` gateway will be used. You can specify a different gateway either when including the extension: | ||
|
||
```ruby | ||
include Dry::Operation::Extensions::ROM[gateway: :my_gateway] | ||
``` | ||
|
||
Or at runtime: | ||
|
||
```ruby | ||
transaction(gateway: :my_gateway) do | ||
# ... | ||
end | ||
``` | ||
|
||
### Sequel | ||
|
||
The `Sequel` extension provides transaction support for operations using [`sequel` databases](http://sequel.jeremyevans.net). It will automatically roll back the transaction if any step returns a Failure. | ||
|
||
Make sure you have sequel installed: | ||
|
||
```ruby | ||
gem 'sequel' | ||
``` | ||
|
||
Include the extension in your operation class and provide access to the Sequel database object through a `#db` method: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
include Dry::Operation::Extensions::Sequel | ||
|
||
attr_reader :db | ||
|
||
def initialize(db:) | ||
@db = db | ||
super() | ||
end | ||
|
||
def call(input) | ||
transaction do | ||
user_id = step create_user(input) | ||
step create_profile(user_id) | ||
user_id | ||
end | ||
end | ||
|
||
private | ||
|
||
def create_user(input) | ||
Success(db[:users].insert(input)) | ||
end | ||
|
||
def create_profile(user_id) | ||
Success(db[:profiles].insert(user_id: user_id)) | ||
end | ||
end | ||
``` | ||
|
||
You can pass options to the transaction either when including the extension: | ||
|
||
```ruby | ||
include Dry::Operation::Extensions::Sequel[isolation: :serializable] | ||
``` | ||
|
||
Or at runtime: | ||
|
||
```ruby | ||
transaction(isolation: :serializable) do | ||
# ... | ||
end | ||
``` | ||
|
||
⚠️ Warning: The ``:savepoint` option for nested transactions is not yet fully supported. | ||
|
||
### ActiveRecord | ||
|
||
The `ActiveRecord` extension adds transaction support for operations using [`activerecord`](https://api.rubyonrails.org/classes/ActiveRecord). Like the other database extensions, it will roll back the transaction if any step returns a `Failure`. | ||
|
||
Make sure you have activerecord installed: | ||
|
||
```ruby | ||
gem 'activerecord' | ||
``` | ||
|
||
Make sure you have activerecord installed: | ||
|
||
```ruby | ||
class CreateUser < Dry::Operation | ||
include Dry::Operation::Extensions::ActiveRecord | ||
|
||
def call(input) | ||
transaction do | ||
user = step create_user(input) | ||
step create_profile(user) | ||
user | ||
end | ||
end | ||
|
||
private | ||
|
||
def create_user(input) | ||
Success(User.create!(input)) | ||
end | ||
|
||
def create_profile(user) | ||
Success(user.create_profile!) | ||
end | ||
end | ||
``` | ||
|
||
By default, `ActiveRecord::Base` is used to initiate transactions. You can specify a different class either when including the extension: | ||
|
||
```ruby | ||
include Dry::Operation::Extensions::ActiveRecord[User] | ||
``` | ||
|
||
Or at runtime: | ||
|
||
```ruby | ||
transaction(User) do | ||
# ... | ||
end | ||
``` | ||
|
||
This is particularly useful when working with multiple databases in `ActiveRecord`. | ||
|
||
You can also provide default transaction options when including the extension: | ||
|
||
```ruby | ||
include Dry::Operation::Extensions::ActiveRecord[isolation: :serializable] | ||
``` | ||
|
||
You can override these options at runtime: | ||
|
||
```ruby | ||
transaction(isolation: :serializable) do | ||
# ... | ||
end | ||
``` | ||
|
||
⚠️ Warning: The `:requires_new` option for nested transactions is not yet fully supported. |
Oops, something went wrong.