DEPRECATION NOTICE
This gem is no longer actively maintained. As a replacement, you can use Resol as well as gems from smart-rb family.
Description
Polist is a set of simple tools for creating business logic layer of your applications:
-
Polist::Service
is a simple class designed for creating service classes. -
Polist::Builder
is a builder system based onUber::Builder
. -
Polist::Struct
is a small utility that helps generating simpleStruct
-like object initializers.
Installation
Simply add gem "polist"
to your Gemfile.
Using Polist::Service
class MyService < Polist::Service
def call
if params[:ok]
success!(code: :cool)
else
fail!(code: :not_cool)
end
end
end
service = MyService.run(ok: true)
service.success? #=> true
service.response #=> { code: :cool }
service = MyService.run(ok: false)
service.success? #=> false
service.response #=> { code: :not_cool }
The only parameter that is passed to the service is called params
by default. If you want more params, feel free to define your own initializer and call the service accordingly:
class MyService < Polist::Service
def initialize(a, b, c)
# ...
end
end
MyService.call(1, 2, 3)
Unlike .run
, .call
will raise Polist::Service::Failure
exception on failure:
begin
MyService.call(ok: false)
rescue Polist::Service::Failure => error
error.response #=> { code: :not_cool }
end
Note that .run
and .call
are just shortcuts for MyService.new(...).run
and MyService.new(...).call
with the only difference that they always return the service instance instead of the result of #run
or #call
. Unlike #call
though, #run
is not intended to be overwritten in subclasses.
Using blocks in #call and #run methods.
You can use yield in #call
. And then call ::run
or ::call
class methods with block. For example, we have the class:
class BlockFun < Polist::Service
def call
success!(yield(1, 2))
end
end
Then we can use it like this:
service = BlockFun.call { |a, b| a + b }
p service.response # => 3
Behind the scenes it just catches passed block in class methods ::run
and ::call
, converts it to proc and then passes proc to instance method #call
and #run
by converting it back to block. So, for example, if you want to pass this block to private methods, you can write code like this:
class AnotherBlockFun < Polist::Service
def call(&block)
success!(block_caller(&block))
end
private
def block_caller
yield 1, 2
end
end
service = AnotherBlockFun.call { |a, b| a + b }
p service.response # => 3
Using Form objects
Sometimes you want to use some kind of params parsing and/or validation, and you can do that with the help of Polist::Service::Form
class. It uses tainbox gem under the hood.
class MyService < Polist::Service
class Form < Polist::Service::Form
attribute :param1, :String
attribute :param2, :Integer
attribute :param3, :String, default: "smth"
attribute :param4, :String
validates :param4, presence: true
end
def call
p form.valid?
p [form.param1, form.param2, form.param3]
end
# The commented code is just the default implementation and can be simply overwritten
# private
# def form
# @form ||= self.class::Form.new(form_attributes.to_snake_keys)
# end
# def form_attributes
# params
# end
end
MyService.call(param1: "1", param2: "2") # prints false and then ["1", 2, "smth"]
The #form
method is there just for convinience and by default it uses what #form_attributes
returns as the attributes for the default form class which is the services' Form
class. You are free to use as many different form classes as you want in your service.
Using Polist::Builder
The build logic is based on Uber::Builder but it allows recursive builders. See the example:
Can be used with Polist::Service
or any other Ruby class.
class User
include Polist::Builder
builds do |role|
case role
when /admin/
Admin
end
end
attr_accessor :role
def initialize(role)
self.role = role
end
end
class Admin < User
builds do |role|
SuperAdmin if role == "super_admin"
end
class SuperAdmin < Admin
def super?
true
end
end
def super?
false
end
end
User.build("user") # => #<User:... @role="user">
User.build("admin") # => #<Admin:... @role="admin">
User.build("admin").super? # => false
User.build("super_admin") # => #<Admin::SuperAdmin:... @role="super_admin">
User.build("super_admin").super? # => true
Admin.build("smth") # => #<Admin:... @role="admin">
SuperAdmin.build("smth") # => #<Admin::SuperAdmin:... @role="admin">
Using Polist::Struct
Works pretty much the same like Ruby Struct
class, but you don't have to subclass it.
Can be used with Polist::Service
or any other class that don't have initializer specified.
class Point
include Polist::Struct
struct :x, :y
end
a = Point.new(15, 25)
a.x # => 15
a.y # => 25
b = Point.new(15, 25, 35) # raises ArgumentError: struct size differs
c = Point.new(15)
c.x # => 15
c.y # => nil
Using Middlewares
If you have some common things to be done in more than one service, you can define a middleware and register it inside the said services.
Every middleware takes the service into it's constructor and executes #call
. Thus every middleware has to implement #call
method and has a #service
attribute reader.
Middlewares delegate #success!
, #fail!
, #error!
, #form
, #form_attributes
to the service class they are registered in.
Every middleware should be a subclass of Polist::Service::Middleware
. Middlewares are run before the service itself is run.
To register a middleware one should use .register_middleware
class method on a service. More than one middleware can be registered for one service.
For example:
class MyMiddleware < Polist::Service::Middleware
def call
fail!(code: :not_cool) if service.fail_on_middleware?
end
end
class MyService < Polist::Service
register_middleware MyMiddleware
def call
success!(code: :cool)
end
def fail_on_middleware?
true
end
end
service = MyService.run
service.success? #=> false
service.response #=> { code: :not_cool }
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/polist.
License
Released under MIT License.
Authors
Created by Yuri Smirnov.