Overcoming complexity of service objects with dry libraries
Tags: ruby, rails
Published: 2021-09-02 19:35:47

## Introduction

Almost everyone in the rails community(and not only) talks about the slim controllers: "Let's move everything to the service objects and functions, keep controllers as simple as possible". It's a great concept, and I fully support it. Service objects and functions for controllers' methods(not only but mainly for them) usually have two additional parts besides business logic:
* input validation;
* errors passing.

They aren't a big problem if your function does not call other functions and does not have nested objects as the input arguments. The problem that they are love to grow and mysteriously interact with each other. 

In this post, I want to share the way how you can overcome these problems. Grab yourself a cup of your favourite beverage ☕ and join this journey.  

## Time for experiments

Imagine that you are designing internal software for a laboratory. It should run experiments from the passed arguments. Arguments should be validated to get a sensible response instead of meaningless 500 errors.

### Without dry libraries

➡️ Let's start from the controller for the experiment. The controller should only run the function and render results or errors:

```ruby
class ExperimentsController
  def create
    result = ConductExperiment.call(experiment_params)

    if result[:success]
      render json: result[:result]
    else
      render json: result[:errors], status: :bad_request
    end
  end

  private

  def experiment_params
    params.require(:experiment).permit(
      :name,
      :duration,
      :power_consumption
    )
  end
end
``` 

➡️ Next, add the service object. It is a good practice to move the repeated part to the base object:

```ruby
class ApplicationService
  def initialize(*params, &block)
    @params = params
    @block = block
  end

  def call
    validation_result = validate_params
    if validation_result[:success]
      execute
    else
      validation_result
    end
  end

  def validate_params
    { success: true }
  end

  def self.call(*params, &block)
    new(*params, &block).call
  end
end

```

```ruby
class ConductExperiment < ApplicationService
  def initialize(params)
    super
    @params = params
  end

  def validate_params
    errors = []
    if @params[:duration].blank?
      errors << 'Duration should be present'
      return { success: false, errors: errors }
    end

    if @params[:duration] < 1
      errors << 'Duration should be positive number'
      return { success: false, errors: errors }
    end

    { success: true }
  end

  def execute
    if Random.rand > 0.3
      { success: true, result: 'Success!' }
    else
      { success: false, errors: ['Definitely not success.'] }
    end
  end
end

```

It already looks cumbersome, don't ya?😕 But all of a sudden, users of our API requested to add the type validation to all fields and a new entity - research, that will accept an array of experiments and checks that summary power consumption is less than a defined limit. Sounds scary 😲. Doing this will be really hard with the current configuration, so let's add some dry libraries and look at how easy it will be.

The full version is [here](https://github.com/evanilukhin/dry_validations_monads_example).

### Update validations

➡️ First, we need to upgrade the validation process. For this will be used the gem [dry-validation](https://dry-rb.org/gems/dry-validation). It adds validations that are expressed through contract objects. 

> A contract specifies a schema with basic type checks and any additional rules that should be applied. Contract rules are applied only once the values they rely on have been successfully verified by the schema.

➡️ Begin by updating the `ApplicationService`. We replace `validate_params` method with `validator`, where can be defined either class name of the contract or contract itself. In the `call`, add the call of the `validator` and change the `validation_result` check, respectively. 

```ruby
class ApplicationService
  def initialize(*params, &block)
    @params = params
    @block = block
  end

  def call
    validation_result = validator.new.call(@params) unless validator.nil?
    if validator.nil? || validation_result.success?
      execute
    else
      { success: false, errors: validation_result.errors.to_h }
    end
  end

  def validator
    nil
  end

  def self.call(*params, &block)
    new(*params, &block).call
  end
end
```

➡️ Next, add the class where will be defined validations. Contracts have two parts:
* [schema(or params)](https://dry-rb.org/gems/dry-validation/1.6/schemas/) where are defined basic type checks  
* [rules](https://dry-rb.org/gems/dry-validation/1.6/rules/) for complex checks which are applied after schema validations. 

⚠️ Be careful with optional values. You should check that value is passed before the checking.

```ruby
class ConductExperimentContract < Dry::Validation::Contract
  schema do
    required(:duration).value(:integer) # in seconds
    optional(:title).value(:string)
    optional(:power_consumption).value(:integer) # in MW
  end

  rule(:duration) do
    key.failure('should be positive') unless value.positive?
  end

  rule(:power_consumption) do
    key.failure('should be positive') if value && !value.positive?
  end
end
```

➡️ In the `ConductExperiment` class, just replace the validation with the previously defined validator class.

```ruby
class ConductExperiment < ApplicationService
  def initialize(params)
    super
    @params = params
  end

  def validator
    ConductExperimentContract
  end

  def execute
    if Random.rand > 0.3
      { success: true, result: 'Success!' }
    else
      { success: false, errors: ['Definitely not success.'] }
    end
  end
end
```

Looks great! Time to add functionality for the research process. 

This class will be finished successfully if all experiments are finished successfully or will return an error message from the first failed experiment.

```ruby
class ConductResearch < ApplicationService
  def initialize(params)
    super
    @params = params
  end

  def validator
    ConductResearchContract
  end

  def execute
    result = []
    @params[:experiments].each do |experiment_params|
      experiment_result = ConductExperiment.call(experiment_params)

      return experiment_result unless experiment_result[:success]

      result << experiment_result[:result]
    end
    { success: true, result: result }
  end
end
```

➡️ The most interesting part is the `ConductResearchContract`. It validates each element of the experiments array for compliance with the schema defined in the `ConductExperimentContract`. Sadly, [now](https://github.com/dry-rb/dry-validation/issues/593), to run rules for each experiment, you must run them manually like in the `rule(:experiments)`.

```ruby
class ConductResearchContract < Dry::Validation::Contract
  MAX_POWER_CONSUMPTION = 30

  schema do
    required(:title).value(:string)
    required(:experiments).array(ConductExperimentContract.schema).value(min_size?: 1)
  end

  rule(:experiments).each do
    result = ConductExperimentContract.new.call(value)
    unless result.success?
      meta_hash = { text: 'contain bad example' }.merge(result.errors.to_h)
      key.failure(meta_hash)
    end
  end

  rule do
    total_power_consumption = values[:experiments].reduce(0) do |sum, experiment|
      experiment[:power_consumption].nil? ? sum : sum + experiment[:power_consumption]
    end

    if total_power_consumption > MAX_POWER_CONSUMPTION
      key(:experiments).failure("total energy consumption #{total_power_consumption} MW exceeded limit #{MAX_POWER_CONSUMPTION} MW")
    end
  end
end
```
✨ We did it! It validates everything and works great. But we can make it even better.

The result version of this example is [here](https://github.com/evanilukhin/dry_validations_monads_example/tree/add_dry_validation).

### Make it simpler with monads

In this part I won't explain what are monads and how this library works, thats very good explained in the [documentation](https://dry-rb.org/gems/dry-monads/1.3/) and in thesecool posts: [Functional Ruby with `dry-monad`s](https://humanreadablemag.com/issues/0/articles/functional-ruby-with-dry-monads/),  [Improve Your Services Using Dry-rb Stack](https://www.netguru.com/blog/improve-your-services-using-dry-rb) and [Five common issues with services and dry-monads](https://www.davydovanton.com/2020/05/19/five-common-issues-with-services-and-dry-monads/). Here I will just demonstrate how it can improve the existing code. 

➡️ Firstly add `dry-monads` gem to the Gemset.
```ruby
# ...
gem 'dry-monads'
# ...
```
➡️ After this add the add [the behaviour of monads](https://dry-rb.org/gems/dry-validation/1.5/extensions/#monads) to contracts. For this, you should run the command `Dry::Validation.load_extensions(:monads)`. For rails applications I usually create file `config/initializers/dry.rb` where store all global configs for dry gems.

➡️ Next update service objects. Replace these ifs with the [do notation](https://dry-rb.org/gems/dry-monads/1.3/do-notation/) syntax.

```ruby
class ApplicationService
  include Dry::Monads[:result, :do]

  def initialize(*params, &block)
    @params = params
    @block = block
  end

  def call
    yield validator.new.call(@params) unless validator.nil?
    execute
  end

  def validator
    nil
  end

  def self.call(*params, &block)
    new(*params, &block).call
  end
end
```

➡️  In the `ConductExperiment`, wrap results to the `Dry::Monads::Result` monads 

```ruby
class ConductExperiment < ApplicationService
  def initialize(params)
    super
    @params = params
  end

  def validator
    ConductExperimentContract
  end

  def execute
    if Random.rand > 0.3
      Success('Success!')
    else
      Failure('Definitely not success.')
    end
  end
end
```

and replace that ugly construction with temporary variables with this neat form in the `ConductResearch` service.

```ruby
class ConductResearch < ApplicationService
  def initialize(params)
    super
    @params = params
  end

  def validator
    ConductResearchContract
  end

  def execute
    result =
      @params[:experiments].map do |experiment_params|
        yield ConductExperiment.call(experiment_params)
      end
    Success(result)
  end
end
```

➡️ Because failure objects are different, result messages should also be handled differently. 
  
```ruby
class ExperimentsController < ApplicationController
  def create
    result = ConductExperiment.call(experiment_params.to_h)
    if result.success?
      render json: result.value!
    else
      error_object =
        if result.failure.is_a?(Dry::Validation::Result)
          result.failure.errors(full: true).to_h
        else
          result.failure
        end

      render(json: error_object, status: :bad_request)
    end
  end

  private

  def experiment_params
    params.require(:experiment).permit(
      :name,
      :duration,
      :power_consumption
    )
  end
end
```

That's all!🎆 Compare it with the previous version. I think you will agree - it looks more readable.

Result version is available [here](https://github.com/evanilukhin/dry_validations_monads_example/tree/add_dry_monads).

---

I hope this post was helpful for you. Have a nice day!