Verbalize
Usage
class Add
include Verbalize::Action
input :a, :b
def call
a + b
end
end
result = Add.call(a: 35, b: 7)
result.outcome # => :ok
result.value # => 42
result.succeeded? # => true
result.success? # alias for succeeded? => true
result.failed? # => false
result.failure? # alias for failed? => false
outcome, value = Add.call(a: 35, b: 7)
outcome # => :ok
value # => 42
class Add
include Verbalize::Action
input optional: [:a, :b]
def call
a + b
end
private
def a
@a ||= 35
end
def b
@b ||= 7
end
end
Add.call # => [:ok, 42]
Add.call(a: 660, b: 6) # => [:ok, 666]
Default Values
# You can define defaults via key/value pairs, as so:
class Add
include Verbalize::Action
# note that these values are evaluated at load-time as they are not wrapped
# in lambdas.
input optional: [a: 35, b: 7]
def call; a + b; end
end
# default values can be lazily loaded by passing in a lambda, e.g.:
class Tomorrow
include Verbalize::Action
input optional: [as_of: -> { Time.now }]
def call
as_of + 1
end
end
start_time = Tomorrow.call!
sleep(1)
end_time = Tomorrow.call!
end_time - start_time # ~1s; the default is executed each call.
class Divide
include Verbalize::Action
input :a, :b
def call
fail! 'You can’t divide by 0' if b == 0
a / b
end
end
result = Divide.call(a: 1, b: 0) # => [:error, 'You can’t divide by 0']
result.failed? # => true
Reflection
class Add
include Verbalize::Action
input :a, :b, optional: [:c, :d]
def call
a + b + c + d
end
private
def c
@c ||= 0
end
def d
@d ||= 0
end
end
Add.required_inputs # [:a, :b]
Add.optional_inputs # [:c, :d]
Add.inputs # [:a, :b, :c, :d]
Validation
class FloatAdd
include Verbalize::Action
input :a, :b
validate(:a) { |a| a.is_a?(Float) }
validate(:b) { |b| b.is_a?(Float) && b > 10.0 }
def call
a + b
end
end
FloatAdd.call!(a: 1, b: 1) # fails with Input "a" failed validation!
FloatAdd.call!(a: 1.0, b: 1.0) # fails with Input "b" failed validation!
FloatAdd.call!(a: 1.0, b: 12.0) # 13.0
Comparison/Benchmark
require 'verbalize'
require 'actionizer'
require 'interactor'
require 'benchmark/ips'
class RubyAdd
def self.call(a:, b:)
new(a: a, b: b).call
end
def initialize(a:, b:)
@a = a
@b = b
end
def call
a + b
end
private
attr_reader :a, :b
end
class VerbalizeAdd
include Verbalize::Action
input :a, :b
def call
a + b
end
end
class ActionizerAdd
include Actionizer
def call
output.sum = input.a + input.b
end
end
class InteractorAdd
include Interactor
def call
context.sum = context.a + context.b
end
end
Benchmark.ips do |x|
x.report('Ruby') { RubyAdd.call(a: 1, b: 2) }
x.report('Verbalize') { VerbalizeAdd.call(a: 1, b: 2) }
x.report('Actionizer') { ActionizerAdd.call(a: 1, b: 2) }
x.report('Interactor') { InteractorAdd.call(a: 1, b: 2) }
x.compare!
end
Warming up --------------------------------------
Ruby 63.091k i/100ms
Verbalize 40.521k i/100ms
Actionizer 5.226k i/100ms
Interactor 4.874k i/100ms
Calculating -------------------------------------
Ruby 751.604k (± 3.0%) i/s - 3.785M in 5.041472s
Verbalize 457.598k (± 6.1%) i/s - 2.310M in 5.072488s
Actionizer 54.874k (± 3.5%) i/s - 276.978k in 5.054541s
Interactor 52.294k (± 3.2%) i/s - 263.196k in 5.038365s
Comparison:
Ruby: 751604.0 i/s
Verbalize: 457597.9 i/s - 1.64x slower
Actionizer: 54873.6 i/s - 13.70x slower
Interactor: 52293.6 i/s - 14.37x slower
Testing
Happy Path
When testing positive cases of a Verbalize::Action
, it is recommended to test using the call!
class method and
assert on the result. This implicitly ensures a successful result, and your tests will fail with a bang! if
something goes wrong:
class MyAction
include Verbalize::Action
input :a
def call
fail!('#{a} is greater than than 100!') if a >= 100
a + 1
end
end
it 'returns the expected result' do
result = MyAction.call!(a: 50)
expect(result).to eq 51
end
Sad Path
When testing negative cases of a Verbalize::Action
, it is recommended to test using the call
non-bang
class method which will return a Verbalize::Failure
on failure.
Use of call!
here is not advised as it will result in an exception being thrown. Set assertions on both
the failure outcome and value:
class MyAction
include Verbalize::Action
input :a
def call
fail!('#{a} is greater than 100!') if a >= 100
a + 1
end
end
# rspec:
it 'fails when the input is out of bounds' do
result = MyAction.call(a: 1000)
expect(result).to be_failed
expect(result.failure).to eq '1000 is greater than 100!'
end
Stubbing Responses
When unit testing, it may be necessary to stub the responses of Verbalize actions. To correctly stub responses,
you should always stub the MyAction.perform
class method on the action class being stubbed per the
instructions below. Never stub the call
or call!
methods directly.
Stubbing .perform
will enable Verbalize
to wrap results correctly for references to either call
or call!
.
Stubbing Successful Responses
To simulate a successful response of the Verbalize::Action
being stubbed, you should stub the MyAction.perform
class method to return the value you expect the MyAction#call
instance method to return.
For example, if you expect the action to return the value 123
on success:
class Foo
def self.multiply_by(multiple)
result = MyAction.call(a: 1)
raise "I couldn't do the thing!" if result.failure?
result.value * multiple
end
end
# rspec:
describe Foo do
describe '#something' do
it 'does the thing when MyAction succeeds' do
# Simulate the successful result
allow(MyAction).to receive(:perform)
.with(a: 1)
.and_return(123)
result = described_class.multiply_by(100)
expect(result).to eq 12300
end
end
end
Stubbing Failure Responses
To simulate a failure response of the Verbalize::Action
being stubbed, you should stub the MyAction.perform
class method to throw ::Verbalize::THROWN_SYMBOL
with the message you expect MyAction#call
to throw
when the simulated failure occurs.
For example, when you expect the outer class to raise an exception when MyAction fails:
# See also: Foo class definition in Stubbing Successful Responses above
# rspec:
describe Foo do
describe '#multiply_by' do
it 'raises an error when MyAction fails' do
# Simulate the failure
allow(MyAction).to receive(:perform)
.with(a: 1)
.and_throw(::Verbalize::THROWN_SYMBOL, 'Y U NO!')
expect {
described_class.multiply_by(100)
}.to raise_error "I couldn't do the thing!"
end
end
end
Installation
Add this line to your application's Gemfile:
gem 'verbalize'
And then execute:
$ bundle
Or install it yourself as:
$ gem install verbalize
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.
Releasing
- Update CHANGELOG.md, and lib/verbalize/version.rb, commit, and push
- Create a new release on github
- Build the gem version:
gem build verbalize.gemspec
- Push the gem version to rubygems:
gem push verbalize-<VERSION>.gem
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/taylorzr/verbalize.
License
The gem is available as open source under the terms of the MIT License.