-
Notifications
You must be signed in to change notification settings - Fork 0
Plugins
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
.
Provides assert
method that can be used for different logic checks during command execution.
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
-
assert(&block)
- yields a block. If block returns falsy value, fails execution. If block passes, sets execution state to "successful", and assigns result totrue
unless those values have already been assigned.
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.
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.
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).
-
context
- returns current execution context assigned byZen::Service.with_context
method. If command haslocal_context
defined (viawith_context
method), will return local context merged into global context, if possible. -
with_context(context)
- returns unexecuted copy of a command withlocal_context
defined as passedcontext
.
Simple plugin that will prevent re-execution of service if it already has been executed, and will immediately return result.
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
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.
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>
-
error
- returns error instance that was rescued during execution -
error?
- returnstrue
if service execution ended up with rescued exception.
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:
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.