Keka
Keka (Japanese for 'result') is a wrapper that represents the result of a particular execution, along with any message returned.
Installation
gem 'keka'
Usage
Below is an example of how the various methods can come together.
class Order
def refund(cancel_delivery = true)
Keka.run do
# returns an err keka with provided msg if !refundable?
Keka.err_unless!(refundable?, 'Payment is no longer refundable.')
# returns an err keka with value from provided block if refund does not exist
Keka.err_unless!(payment.refund) { CustomErrorClass.new(message: 'Already been refunded') }
# execute statements if nothing 'return' from above
do_something_else
# if cancel_delivery
# => returns an err keka with provided msg if !remove_delivery_assignment
Keka.err_unless!(remove_delivery_assignment, 'Refunded but failed to remove delivery.') if cancel_delivery
# returns an ok keka if nothing 'return' from above
end
end
private
def remove_delivery_assignment
Keka.run do
# returns an ok keka if already_removed?
Keka.ok_if! already_removed?
# returns an err keka with no msg if !remove!
Keka.err_unless! remove!
# returns an ok keka if nothing 'return' from above
end
end
end
class CustomErrorClass
def initialize(message:)
@message = message
Airbrake.notify('Received refund request for already refunded method')
end
end
class SomeController
def some_action
keka = @order.refund
if keka.ok?
head :ok
else
render json: { error: keka.msg }, status: 422
end
end
end
Of course, you can also use .err_unless!
, .err_if!
, and .ok_if!
outside
of the Keka.run
block.
Passing a block
You can also pass a block to be executed if the result short circuits the execution. This is very useful for when you want to return a more complex msg
than a String. For example, if you want to return an error class that validates certain properties upon initialization, then you will want to create the instance of error class in the block.
NOTE: This block will only run if there is no msg
provided.
Keka.err_if!(true) { raise Error } # Raises error
Keka.err_if!(false) { raise Error } # Does not raises error
Keka.err_if!(true, 'msg') { raise Error } # Does not raises error
Keka.err_if!(false, 'msg') { raise Error } # Does not raises error
Abort Unconditionally
Sometimes you know you want to abort, but you also need to a few things before aborting, such as saving the error result to database, logging, or submitting a metric to your monitoring service. You can use .err!
or .ok!
methods to abort from the current Keka.run
block. Both methods can be invoked with or without a message argument.
def refund
Keka.run do
processor_response = payment.refund
unless processor_response.success
payment.log_processor_errors(processor_response.errors)
Keka.err! processor_response.errors
end
end
end
Handle Exceptions
Before version 0.2.0, handling exceptions in .run
block is a bit tricky. You might do something like this
def validate_purchase(item_ids)
Keka.run do
Item.find(item_ids)
rescue ActiveRecord::RecordNotFound
Keka.err_if! true, 'Some item is unavailable'
end
end
After version 0.2.0, you can simply
# * Returns ok result if no exception is raised.
# * Returns err result if ActiveRecord::RecordNotFound is raised, with msg set
# to 'Some item is unavailable'.
# * Raises if any other non-keka exception is thrown.
def validate_purchase(item_ids)
Keka.rescue_with(ActiveRecord::RecordNotFound, 'Some item is unavailable')
.run { Item.find(item_ids) }
end
You can also chain .rescue_with
def validate_purchase(store_id, new_item_payload)
Keka.rescue_with(ActiveRecord::RecordNotFound, 'Some item is unavailable')
.rescue_with(ActiveRecord::RecordInvalid, 'Invalid payload')
.run do
store = Store.find(store_id)
store.items.create!(new_item_payload)
end
end
Note, by design, .rescue_with
only rescues from descendants of StandardError. This will NOT work.
def invalid_example
# The .rescue_with does NOTHING here. This method will raise a NoMemoryError exception.
Keka.rescue_with(NoMemoryError, 'oops')
.run { raise NoMemoryError.new }
end
ActiveRecord (ActiveModel) support
One of the most common boundary conditions is validation.
class User
validates :name, :city, presence: true
end
if User.new(name: nil, city: nil).valid?
# do something
else
# return errors
end
Keka supports ActiveModel::Errors
as msg
argument to return full message and errors per fields
user = User.new(name: nil, city: nil)
result = Keka.run do
Keka.err_unless! user.valid?, user.errors
end
puts result.ok?
# => false
puts result.msg
# => "Name can't be blank, City can't be blank"
puts result.errors
# => {name: ["can't be blank"], city: ["can't be blank"]}
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/zinosama/keka.
License
The gem is available as open source under the terms of the MIT License.