Skip to content

Plugins

Artem Kuzko edited this page Mar 24, 2021 · 11 revisions

Table of Contents

zen-service is built with extensions in mind. Even core functionality is organized in plugins that are used in base Zen::Service class. Bellow you can read about optional functionality that is shipped with zen-service.

Assertions

Provides assert method that can be used for different logic checks during command execution.

Usage

class MyService < Zen::Service
  use :assertions
  attributes :foo

  def execute!
    result { foo * 2 }
    assert { foo > 4 }
  end
end

command = MyService.new(2)
command.execute.success? # => false

API

Instance Methods

  • assert(&block) - yields a block. If block returns falsy value, fails execution. If block passes, sets execution state to "successful", and assigns result to true unless those values have already been assigned.

Context

Allows you to set an execution context for the duration of a block that will be available to any service that uses this plugin via context method.

Usage

To provide a context, use Zen::Service.with_context method:

# application_controller.rb
around_action :with_context

def with_context
  Zen::Service.with_context(current_user: current_user) do
    yield
  end
end

And use context in your services:

class Posts::Archive < Zen::Service
  use :context
  attributes :post

  def execute!
    post.update(archived: true, archived_by: context[:current_user])
  end
end

you can also set a local context for instantiated service:

service = Posts::Archive.new(post)
service.context # => nil
service_with_user = service.with_context(current_user: admin)
service_with_user.context # => {:current_user => <User record>}
# previous command remains untouched:
service.context # => nil

It is generally recommended to use Hashie::Mash object as context for convenient access to it's content.

API

Service Methods

Zen::Service.with_context(context, &block) - sets service execution context for the duration of the block. If within that block Zen::Service.with_context is called again, new context will be merged into previous one, if latter responds to :merge. Otherwise, new context replaces previous (for the duration of the block).

Instance Methods

  • context - returns current execution context assigned by Zen::Service.with_context method. If command has local_context defined (via with_context method), will return local context merged into global context, if possible.

  • with_context(context) - returns unexecuted copy of a command with local_context defined as passed context.

Execution Cache

Simple plugin that will prevent re-execution of service if it already has been executed, and will immediately return result.

Usage

class MyService < Zen::Service
  use :execution_cache

  def execute!
    some_heavy_calculations
  end
end

service = MyService.new
service.execute # runs some heavy calculations
service.execute # does not run some heavy calculations

NOTE: If command is executed with a block, it will always be executed:

class MyService < Zen::Service
  use :execution_cache

  def execute!
    yield 2
  end
end

service = MyService.new
service.execute { |n| n + 2 }.result # => 4
service.execute { |n| n + 3 }.result # => 5

Policies

Allows you to define permission checks within a service that can be used in other services for checks and guard violations. Much like pundit Policies (hence the name), but more. Where pundit governs only authorization logic, zen-service's "policy" services can have any denial reason you find appropriate, and declare logic for different denial reasons in single place. It also defines #execute! method that will result in hash with all permission checks.

Usage

class Posts::Policies < Zen::Service
  use :policies

  attributes :post, :user

  deny_with :unauthorized do
    def publish?
      # only author can publish a post
      post.author_id == user.id
    end

    def delete?
      publish?
    end
  end

  deny_with :unprocessable_entity do
    def delete?
      # disallow to destroy posts that are older than 1 hour
      (post.created_at + 1.hour).past?
    end
  end
end

policies = Posts::Policies.new(outdated_post, user)
policies.can?(:publish)     # => true
policies.can?(:delete)      # => false
policies.why_cant?(:delete) # => :unprocessable_entity
policies.guard!(:delete)    # => raises Zen::Service::Plugins::Policies::GuardViolationError, :unprocessable_entity
policies.execute.result     # => {'publish' => true, 'delete' => false}

### API

#### Instance Methods

- `can?(action)` - returns `true` if `action` is permitted by policy. Returns `false` otherwise.
- `why_cant?(action)` - returns policy denial reason for that action. If action is permitted, returns `nil`.
- `guard!(action)` - raises an error if action is not permitted by the policy check.

## Rescue

Provides `:rescue` execution option. If set to `true`, any error occurred during command execution will not be raised outside. If `:status` plugin is used by the service, will set execution status to `:error`.

### Usage

```rb
class MyService < Zen::Service
  use :status
  use :rescue

  def execute!
    fail RuntimeError, 'oops'
  end
end

service = MyService.new.execute(rescue: true)
service.success? # => false
service.error? # => true
service.status # => :error
service.error # => #<RuntimeError: oops>

API

Instance Methods

  • error - returns error instance that was rescued during execution
  • error? - returns true if service execution ended up with rescued exception.

Status

Adds status execution state property to the service, as well as helper methods and behavior to set it. status property is not bound to the "success" flag of execution state and can have any value depending on your needs. It is up to you to setup which statuses correspond to successful execution and which are not. Generated status helper methods allow to atomically and more explicitly assign both status and result at the same time:

Usage

class Posts::Update < Zen::Service
  use :status,
    success: [:ok],
    failure: [:unprocessable_entity]

  attributes :post, :params

  delegate :errors, to: :post

  def execute!
    if post.update(params)
      ok { post.as_json }
    else
      unprocessable_entity
    end
  end
end

service = Posts::Update.(post, post_params)
# in case params were valid you will have:
service.success? # => true
service.status # => :ok
service.result # => {'id' => 1, ...}

Note that just like success, failure, or result methods, status helpers accept result value as result of yielded block.