ClearLogic
Service object based on dry-rb
Installation
gem 'clear_logic'
Usage
- Interface
- Types
- Context
- Stride
- Result
- Logger
- Matcher
Interface
DSL for building class initializer with params and options based on dry-initializer
class MyService < ClearLogic::Service
context :name, Dry::Types['strict.string'], as: :param
context :test, Dry::Types['strict.bool'], default: -> { false }
stride :some_step
def some_step(context)
context.name # => first argument
success(context)
end
end
In console:
result = MyService.call('Liskov', test: true)
result.success? # => true
result.context.name # => 'Liskov'
result.context.test # => true
result = MyService.call('Lenon')
result.success? # => true
result.context.name # => 'Lenon'
result.context.test # => false
Types
You can register own class
types.
In your initializer:
ActiveRecord::Base.connection.tables.map { |x| x.classify.safe_constantize }.compact.each do |model|
ClearLogic::Types.register(model, prefix: 'model')
end
In your initializer:
Dry::Types['model.user']
Context
In each step you work with context
. Context have next methods which are available to you:
args
[]
[]=
exit_success?
catched_error?
failure_error?
to_h
And next accessors:
catched_error
failure_error
service
exit_success
step
class MyService < ClearLogic::Service
context :name, Dry::Types['strict.string'], as: :param
stride :some_step
def some_step(context)
fetch_user
success(context)
end
private
def fetch_user
context[:user] = User.find_by(name: context.name)
end
end
In console:
result = MyService.call('Buscemi')
result.context.name # => 'Buscemi'
result.context.step # => :some_step # return last step
result.context.args # => ['Buscemi']
result.context[:some_option] # => true
result.context.to_h # => {:catched_error=>nil, :failure_error=>nil, :service=>MyService, :exit_success=>nil, :step=>:some_step, :options=>{:some_option=>true}, :args=>['Buscemi']}
result.context.exit_success? # => false
result.context.catched_error? # => false
result.context.failure_error? # => false
Stride
Business transaction DSL based on dry-transaction
Catch failed step
class MyService < ClearLogic::Service
stride :first, failure: :some_rollback
stride :second
def first(context)
# ...
failure(context)
end
def second(context)
# ...
success(context)
end
def some_rollback(context)
# Do sth if first step is failed
success(context)
end
end
In console:
result = MyService.call
result.success? # => true
Catch specific errors
class MyService < ClearLogic::Service
stride :first
stride :second, rescue: { StandardError => :some_rescue }
def first(context)
# ...
success(context)
end
def second(context)
# ...
raise StandardError
end
def some_rescue(context)
# Do sth if first step is raised error from `rescue` option
failure(context)
end
end
In console:
result = MyService.call
result.success? # => false
result.context.catched_error? # => true
result.context.catched_error.class # => StandardError
Exit success from process
class MyService < ClearLogic::Service
stride :first
stride :second, rescue: { StandardError => :some_rescue }
def first(context)
context[:test] = 'first'
exit_success(context)
end
def second(context)
context[:test] = 'second'
success(context)
end
end
In console:
result = MyService.call
result.success? # => true
result.context[:test] # => 'first'
result.exit_success? # => true'
Result
Result with status of failed execution based on dry-monads
Default errors
Aside from default monads methods success
and failure
you have next failure methods which give you status of failed execution:
unauthorized
forbidden
not_found
invalid
class MyService < ClearLogic::Service
stride :first
stride :second
def first(context)
# ...
not_found(context)
end
def second(context)
# If will not be handled
failure(context)
end
end
In console:
result = MyService.call
result.failure? # => false
result.context.failure_error? # => true
result.context.failure_error.status # => :not_found
Custom errors
You also can use own errors
class MyService < ClearLogic::Service
errors :failed_logic
stride :first
stride :second
def first(context)
# ...
failed_logic(context)
end
def second(context)
# If will not be handled
success(context)
end
end
In console:
result = MyService.call
result.failure? # => false
result.context.failure_error? # => true
result.context.failure_error.status # => :failed_logic
Logger
DSL for insert log context state on each step
Default logger
class MyService < ClearLogic::Service
stride :first, log: true
stride :second
stride :third, log: true
def first(context)
context[:test] = ['first']
success(context)
end
def second(context)
context[:test] += ['second']
success(context)
end
def third(context)
context[:test] += ['third']
success(context)
end
end
In console:
result = MyService.call
In log/my_service.log
# Logfile created on 2019-06-06 10:28:42 +0300 by logger.rb/61378
[19-06-06 10:28:42.087 #14159#79000] INFO -- : {
"catched_error": null,
"failure_error": null,
"service": "LoggerService",
"exit_success": null,
"step": "first_log",
"options": {
"test": [
"first"
]
},
"args": [
]
}
[19-06-06 10:28:42.087 #14159#79000] INFO -- : {
"catched_error": null,
"failure_error": null,
"service": "LoggerService",
"exit_success": null,
"step": "third_log",
"options": {
"test": [
"first",
"second",
"third"
]
},
"args": [
]
}
Custom logger
class MyLogger < ::Logger
def format_message(severity, time, progname, context)
context[:test][0] + "\n"
end
end
class MyService < ClearLogic::Service
logger MyLogger
stride :first, log: true
def first(context)
context[:test] = ['first']
success(context)
end
def second(context)
context[:test] += ['second']
success(context)
end
end
In log/my_service.log
# Logfile created on 2019-06-06 10:28:42 +0300 by logger.rb/61378
first
first
Matcher
Service for handling failed result based on dry-matcher
class MyService < ClearLogic::Service
context :user_id, Dry::Types['strict.integer']
errors :custom_error
stride :find_user
stride :check_policy
def initilize
end
def find_user(context)
return not_found(context) unless fetched_user
success(context)
end
def check_policy(context)
return forbidden(context) unless user_policy.has_ability?
success(context)
end
def fetched_user
@fetched_user ||= User.find_by(id: context.user_id)
end
def user_policy
@user_policy ||= PolicyUser.call(fetched_user)
end
end
In matcher you can check custom errors and default result errors
In console:
result = MyService.call(user_id: 1)
match_result = ClearLogic::Matcher.call(result) do |on|
on.failure(:unauthorized) { :unauthorized_result }
on.failure(:forbidden) { :forbidden_result }
on.failure(:not_found) { :not_found_result }
on.failure(:invalid) { :invalid_result }
on.failure(:custom_error) { :custom_error_result }
on.failure { :failure_result }
on.success { :success_result }
end
match_result # => :not_found_result
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/bezrukavyi/clear_logic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ClearLogic project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.