# Verbalize

[![Build Status](https://circleci.com/gh/taylorzr/verbalize.svg?style=shield&circle-token=58dcd03ffacd1c21e57766a0fba6b2008bafd777)](https://circleci.com/gh/taylorzr/verbalize/tree/master) [![Coverage Status](https://coveralls.io/repos/github/taylorzr/verbalize/badge.svg?branch=master)](https://coveralls.io/github/taylorzr/verbalize?branch=master)

## Usage

```ruby
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
```

```ruby
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]
```

```ruby
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
```

## Comparison/Benchmark
```ruby
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    53.203k i/100ms
           Verbalize    27.518k i/100ms
          Actionizer     4.933k i/100ms
          Interactor     4.166k i/100ms
Calculating -------------------------------------
                Ruby    544.025k (±13.7%) i/s -      2.660M in   5.011207s
           Verbalize    278.738k (± 8.0%) i/s -      1.403M in   5.074676s
          Actionizer     49.571k (± 7.0%) i/s -    246.650k in   5.006194s
          Interactor     45.896k (± 7.1%) i/s -    229.130k in   5.018389s

Comparison:
                Ruby:   544025.0 i/s
           Verbalize:   278737.9 i/s - 1.95x  slower
          Actionizer:    49571.1 i/s - 10.97x  slower
          Interactor:    45896.1 i/s - 11.85x  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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
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](https://rubygems.org).

## 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](http://opensource.org/licenses/MIT).