Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,50 @@ RegisterUser.call(user) do |operation|
end
```

## Workflows

`Serviz` also provides a `Workflow` class that allows you to compose multiple service objects together using a clean, declarative DSL for orchestrating complex multi-step operations.

### Basic Workflow Usage

```ruby
class UserOnboarding < Serviz::Workflow
step RegisterUser
step SendWelcomeEmail, if: ->(last_step) { last_step.success? }
step LogOnboarding
end

# Usage
operation = UserOnboarding.call(user_params)
puts operation.success? # => true
puts operation.result # => result from LogOnboarding

# Handles failures gracefully
operation = UserOnboarding.call(invalid_params)
puts operation.failure? # => true
puts operation.errors # => ["Registration failed"]
```

### Advanced Workflow Features

- **Declarative step definition** using class-level `step` method declarations
- **Conditional execution** using the `if:` option to control whether steps run based on previous results
- **Error accumulation** from all failed steps in the workflow
- **Result chaining** where the last successful step's result becomes the workflow result
- **Full compatibility** with the existing Serviz interface (`success?`, `failure?`, `errors`, `result`)

### Custom Parameters

You can also pass custom parameters to individual steps:

```ruby
class OrderProcessing < Serviz::Workflow
step ValidateOrder
step ChargePayment, params: { gateway: 'stripe' }, if: ->(last_step) { last_step.success? }
step ShipOrder, if: ->(last_step) { last_step.success? }
end
```

## Development

Any kind of feedback, bug report or enhancement are really welcome!
Expand Down
39 changes: 2 additions & 37 deletions lib/serviz.rb
Original file line number Diff line number Diff line change
@@ -1,38 +1,3 @@
require 'serviz/version'

module Serviz
class Base
attr_accessor :errors, :result

def self.call(...)
instance = new(...)
instance.call

yield(instance) if block_given?

instance
end

def call
raise NotImplementedError
end

def errors
@errors ||= []
end

def error_messages(separator = ", ")
errors.join(separator)
end

def success?
!failure?
end
alias_method :ok?, :success?

def failure?
errors.any?
end
alias_method :error?, :failure?
end
end
require 'serviz/base'
require 'serviz/workflow'
36 changes: 36 additions & 0 deletions lib/serviz/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Serviz
class Base
attr_accessor :errors, :result

def self.call(...)
instance = new(...)
instance.call

yield(instance) if block_given?

instance
end

def call
raise NotImplementedError
end

def errors
@errors ||= []
end

def error_messages(separator = ", ")
errors.join(separator)
end

def success?
!failure?
end
alias_method :ok?, :success?

def failure?
errors.any?
end
alias_method :error?, :failure?
end
end
55 changes: 55 additions & 0 deletions lib/serviz/workflow.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Serviz
class Workflow < Base
def self.step(service_class, params: nil, if: nil)
steps << {
service_class: service_class,
params: params,
condition: binding.local_variable_get(:if)
}
end

def self.steps
@steps ||= []
end

def initialize(*args, **kwargs)
@last_step = nil
@args = args
@kwargs = kwargs
end

def call
self.class.steps.each do |step_config|
# Check if condition is provided and evaluate it
if step_config[:condition] && !step_config[:condition].call(@last_step)
next
end

# Determine parameters to use
operation = if step_config[:params]
# Use step-specific params
step_params = if step_config[:params].is_a?(Proc)
step_config[:params].call(self)
else
step_config[:params]
end
step_config[:service_class].call(**step_params)
else
# Use workflow args/kwargs
step_config[:service_class].call(*@args, **@kwargs)
end

# Accumulate errors if the service failed
if operation.failure?
self.errors.concat(operation.errors)
end

# Update last result and overall result
@last_step = operation
self.result = operation.result
end

self
end
end
end
File renamed without changes.
29 changes: 29 additions & 0 deletions spec/scenarios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,32 @@ def call

class NoCall < Serviz::Base
end

# Test services for Workflow
class Step1 < Serviz::Base
def initialize(some_flag: nil)
@some_flag = some_flag
end

def call
if @some_flag
self.result = "step1_#{@some_flag}"
else
self.errors << 'Step1 failed'
end
end
end

class Step2 < Serviz::Base
def initialize(some_flag: nil)
@some_flag = some_flag
end

def call
if @some_flag
self.result = "step2_#{@some_flag}"
else
self.errors << 'Step2 failed'
end
end
end
60 changes: 60 additions & 0 deletions spec/workflow_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
RSpec.describe Serviz::Workflow do
describe "basic workflow execution" do
it "executes steps in sequence and returns the last result" do
workflow = Class.new(Serviz::Workflow) do
step Step1, params: ->(instance) { { some_flag: instance.instance_variable_get(:@arg1) } }
step Step2, params: ->(instance) { { some_flag: instance.instance_variable_get(:@arg2) } }

def initialize(arg1, arg2)
super()
@arg1 = arg1
@arg2 = arg2
end
end

operation = workflow.call("test1", "test2")

expect(operation.success?).to eq true
expect(operation.result).to eq "step2_test2"
end

it "accumulates errors from failed steps" do
workflow = Class.new(Serviz::Workflow) do
step Step1, params: { some_flag: nil } # This will fail
step Step2, params: { some_flag: "test" }
end

operation = workflow.call

expect(operation.failure?).to eq true
expect(operation.errors).to include('Step1 failed')
end
end

describe "conditional execution" do
it "skips steps when condition is false" do
workflow = Class.new(Serviz::Workflow) do
step Step1, params: { some_flag: nil } # This will fail
step Step2, params: { some_flag: "test" }, if: ->(operation) { operation.success? }
end

operation = workflow.call

expect(operation.failure?).to eq true
expect(operation.errors).to eq(['Step1 failed'])
expect(operation.result).to be_nil
end

it "executes steps when condition is true" do
workflow = Class.new(Serviz::Workflow) do
step Step1, params: { some_flag: "test1" } # This will succeed
step Step2, params: { some_flag: "test2" }, if: ->(operation) { operation.success? }
end

operation = workflow.call

expect(operation.success?).to eq true
expect(operation.result).to eq "step2_test2"
end
end
end