Skip to content

Commit

Permalink
Add documentation for the dry-rb.org website (#26)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Tim Riley <tim@riley.id.au>
  • Loading branch information
waiting-for-dev and timriley authored Nov 1, 2024
1 parent e57f4c8 commit 62c1b1b
Show file tree
Hide file tree
Showing 5 changed files with 451 additions and 0 deletions.
64 changes: 64 additions & 0 deletions docsite/source/configuration.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
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](docs::error-handling). This is what allows you to use `#step` directly in your `#call` method.

```ruby
class CreateUser < Dry::Operation
def call(input)
# Step handling works in #call by default
user = step create_user(input)
step notify(user)
user
end
end
```

### Customizing wrapped methods

You can customize which methods can handled steps using the `.operate_on` class method:

```ruby
class MyOperation < Dry::Operation
# Handle steps in 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` and manage error handling yourself.

```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` again
36 changes: 36 additions & 0 deletions docsite/source/design-pattern.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
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.

By expressing this behaviour via `#step`, dry-operation lets you intermingle ordinary Ruby code in between steps as required.

### Railway Oriented Programming

The design of dry-operation closely follows the concept of [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/), 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".
65 changes: 65 additions & 0 deletions docsite/source/error-handling.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
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. This method is only called to perform desired side effects and it won't affect the operation's return value.

```ruby
class CreateUser < Dry::Operation
def initialize(logger:)
@logger = logger
end

def call(input)
attrs = step validate(input)
user = step persist(attrs)
step notify(user)
user
end

private

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 initialize(logger:)
@logger = logger
end

def call(input)
attrs = step validate(input)
user = step persist(attrs)
step notify(user)
user
end

private

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
```
171 changes: 171 additions & 0 deletions docsite/source/extensions.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: Extensions
layout: gem-single
name: dry-operation
---

### ROM

The `ROM` extension adds transaction support to your operations when working with the [ROM](https://rom-rb.org) database persistence toolkit. 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"
```

Require and include the extension in your operation class and provide access to the ROM container through a `#rom` method:

```ruby
require "dry/operation/extensions/rom"

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

# ...
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 when using the [Sequel](http://sequel.jeremyevans.net) database toolkit. It will automatically roll back the transaction if any step returns a `Failure`.

Make sure you have sequel installed:

```ruby
gem "sequel"
```

Require and include the extension in your operation class and provide access to the Sequel database object through a `#db` method:

```ruby
require "dry/operation/extensions/sequel"

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

# ...
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 supported.

### ActiveRecord

The `ActiveRecord` extension adds transaction support for operations using the [ActiveRecord](https://api.rubyonrails.org/classes/ActiveRecord) ORM. 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"
```

Require and include the extension in your operation class:

```ruby
require "dry/operation/extensions/active_record"

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

# ...
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.
Loading

0 comments on commit 62c1b1b

Please sign in to comment.