# ServiceObjects

[![Gem Version](https://img.shields.io/gem/v/service_objects.svg?style=flat)][gem]
[![Build Status](https://img.shields.io/travis/nepalez/service_objects/master.svg?style=flat)][travis]
[![Dependency Status](https://img.shields.io/gemnasium/nepalez/service_objects.svg?style=flat)][gemnasium]
[![Code Climate](https://img.shields.io/codeclimate/github/nepalez/service_objects.svg?style=flat)][codeclimate]
[![Coverage](https://img.shields.io/coveralls/nepalez/service_objects.svg?style=flat)][coveralls]
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)][license]

[gem]: https://rubygems.org/gems/service_objects
[travis]: https://travis-ci.org/nepalez/service_objects
[gemnasium]: https://gemnasium.com/nepalez/service_objects
[codeclimate]: https://codeclimate.com/github/nepalez/service_objects
[coveralls]: https://coveralls.io/r/nepalez/service_objects
[license]: file:LICENSE

The module implements two design patterns:

* The [Interactor pattern] to decouple business logics from both models and delivery mechanisms, such as [Rails].
* The [Observer pattern] to follow the [Tell, don't Ask] design princible.
  The pattern is implemented with the help of [wisper] gem by [Kris Leech].

[Rails]: http://rubyonrails.org/
[Interactor pattern]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
[Observer pattern]: http://reefpoints.dockyard.com/2013/08/20/design-patterns-observer-pattern.html
[Tell, don't Ask]: http://martinfowler.com/bliki/TellDontAsk.html
[wisper]: http://www.github.com/krisleech/wisper
[Kris Leech]: http://www.github.com/krisleech

The module API provides 3 classes:

* `ServiceObjects::Base` - for service objects.
* `ServiceObjects::Listener` - for decorating objects with methods called by service notificiations.
* `ServiceObjects::Message` - for messages published by service objects.

## Installation

Add this line to your application's Gemfile:

```ruby
    gem "service_objects"
```

And then execute:

```
    bundle
```

Or install it yourself as:

```
    gem install service_objects
```

## Usage

The basic usage of the services by example of Rails controller:

```ruby
# lib/my_gem.rb
require "service_objects"

# app/services/add_foo.rb
class AddFoo < ServiceObjects::Base

  # whitelists parameters and defines #params, #bar and #baz
  allows_params :bar, :baz

  # declares external dependencies
  depends_on :find_foo, default: FindFoo

  # calls the service, sorts out and reports its results
  def run
    run!
  rescue Invalid => err
    publish :error, err.messages
  else
    publish :created, @foo, messages
  ensure
    self
  end

  private

  # business logic lives here
  def run!
    # ... usage of the external service find_foo postponed
    add_foo
  end

  # rescues errors and re-raises them as Invalid with list of #messages
  def add_foo
    escape { @foo ||= Foo.create! bar: bar, baz: baz }
  end
end
```

```ruby
# app/controllers/foos_controller.rb
class FoosController < ApplicationController
  
  def create
    # Create the service object with necessary parameters.
    # All the business logic is encapsulated inside the service,
    # and the controller knows nothing what the service will do.
    service  = AddFoo.new params.allow(:bar, :baz)
    # Subscribe the listener for the service's notifications
    service.subscribe listener, prefix: :on
    # Run the service
    service.run
    # If the service doesn't call any listener method,
    # then a listener provides some default actions
    listener.finalize
  end

  private

  # The listener decorates the controller with methods
  # to listen the service object's notifications
  # (see FoosListener#otherwise method below).
  def listener
    @listener ||= FoosListener.new self
  end

  # The class to work out service object notifications
  # The #render method is delegated to the controller
  class FoosListener < ServiceObjects::Listener

    # The method to be called when a service publishes
    # the 'added' notification.
    def on_added(foo, *, messages)
      render "created", locals: { foo: foo, messages: messages }
    end

    # The method to be called when a service publishes
    # the 'error' notification.
    def on_error(*, messages)
      render "error", locals: { messages: messages }
    end

    # The method is called by the #finalize in case no methods has been called
    # by the service object.
    #
    # This allows to focuse only on a subset of service notifications above.
    def otherwise
      render "no_result"
    end
  end
end
```

The service can notify several listeners (controller itself, mailer etc.).

## Base

The `ServiceObjects::Base` provides base class for services.

```ruby
require "service_objects"

class AddFoo < ServiceObjects::Base
end
```

### Parameters declaration

Define allowed parameters for objects:

```ruby
class AddFoo < ServiceObjects::Base
  allows_params :bar, :baz
end
```

Parameters are whitelisted and assigned to `#params` hash (all keys are *symbolized*).

Attributes are also defined as aliases for corresponding params, so that `#bar` and `#bar=` are equivalent to `#params[:bar]`, `#params[:bar]=`.

**Note**:
The service ignores parameters except for explicitly declared. The client can give relevant data to the service, and leave the latter to figure them out by itself.

### Validation

The `ServiceObject::Base` includes [ActiveModel::Validations] with methods `.validates`, `.validate`, `#errors`, `#valid?` and `#invalid?`. Use them to add action context - specific validations.

The method `#validate!` raises the `ServiceObject::Invalid` if validation fails.

```ruby
class AddFoo < ServiceObjects::Base
  allows_params :bar, :baz

  validates :bar, presence: true
  validates :baz, numericality: { greater_than: 1 }, allow_nil: true

  # ...

  def run!
    # ...
    validate!
    # ...
  end
end
```

**Note:** You aren't restricted in selecting time for validation. Prepare attributes (either "real" or [virtual]) and run `#validate!` when necessary.

[ActiveModel::Validations]: http://api.rubyonrails.org/classes/ActiveModel/Validations.html
[virtual]: http://railscasts.com/episodes/16-virtual-attributes?view=asciicast

### Dependencies declaration

As a rule, services uses each other to keep the code DRY. For example, the service that *adds* a new foo (whatever it means) can use another service to *find* an existing foo.

To made all that dependencies injectable via [setter injection], define them explicitly:

```ruby
class AddFoo < ServiceObjects::Base
  # ...

  # Providing the FindFoo is available at the moment AddFoo being defined:
  depends_on :find_foo, default: FindFoo
end
```

Default value can be either assigned or skipped. In the last case the [Null Object] will be assigned by default.

The class method is public to postpone the default implementation until it is defined:

```ruby
class AddFoo < ServiceObjects::Base
  # ...
  depends_on :find_foo
end

# later
FindFoo = Class.new
AddFoo.depends_on :find_foo, default: FindFoo
```

This provides the instance attribute `#find_foo`. You can inject the dependency to the via setter:

```ruby
service = AddFoo.new bar: "bar", baz: "baz"
service.find_foo = GetFoo
```

[setter injection]: http://brandonhilkert.com/blog/a-ruby-refactor-exploring-dependency-injection-options/
[Null Object]: https://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object

### Run method

It is expected the `run` method to provide all the necessary staff and notify listeners via `#publish` method.

See [wisper] for details on `#publish` and `#subscribe` methods.

```ruby
class AddFoo < ServiceObjects::Base
  # ...

  # The method contains the reporting logic only
  def run
    run!
  rescue Found
    publish :found, @foo, messages
  rescue Invalid => err
    publish :error, err.messages
  else
    publish :added, @foo, messages
  ensure
    self
  end

  # ...

  private

  Found = Class.new(RuntimeError) # the internal message

  # Business logic lives here
  def run!
    get_foo
    create_foo
  end

  def get_foo
    # ... finds and assigns @foo somehow
    fail Found if @foo
  end

  def add_foo
    # ...
  end
end
```

There are some helper available:
* `messages` - an array of collected service messages
* `add_message` - adds the new message to the array
* `escape` - rescues from `StandardErrors` and re-raises them as `ServiceObject::Invalid` with collection of `#messages`.

**Note** Following [command-query separation] the `#run` method (being a command) returns `self`.

[command-query separation]: http://en.wikipedia.org/wiki/Command-query_separation

### External services

External services should be used in just the same way as in the controller example.

```ruby
class AddFoo < ServiceObjects::Base
  depends_on :find_foo, default: FindFoo

  # ...

  def get_foo
    service = find_foo.new params
    service.subscribe listener, prefix: :on
    service.run

    # the method runs #otherwise callback in case
    # no other notificaton has been received
    listener.finalize
  end

  # decorates the service with methods to listen to external service
  def listener
    @listener ||= FindListener.new self
  end

  class FindListener < ServiceObjects::Listener
    def on_found(foo, *)
      __getobj__.foo = foo
    end

    def on_error(*, messages)
      __getobj__.messages = messages
    end

    def otherwise
      add_message "complain", "haven't been informed"
    end
  end
end
```

Here the `#get_foo` runs the external service and listens to its notifications. Instead of the long syntax above, you can use a shortcut:

```ruby
# ...
def get_foo
  run_service find_foo.new(params), listener, prefix: :on
end
```

## Listener

The listener is a [decorator] that:

* defines callbacks to listen to service notifications.
* delegates all undefined methods to the encapsulated object (available via [__getobj__] instance method).
* defines the `#finalize` method to run `#otherwise` callback in case no other methods has been checked (via `#respond_to?` method).

```ruby
class FooListener < ServiceObjects::Listener
  def on_success(*)
    "Notified on success"
  end
  
  def otherwise
    "Hasn't been notified"
  end
end

listener = FooListener.new
listener.finalize
# => "Hasn't been notified"

listener.respond_to? :on_error
# => false
listener.finalize
# => "Hasn't been notified"

listener.respond_to? :on_success
# => true
listener.finalize
# => nil
```

[decorator]: http://nithinbekal.com/posts/ruby-decorators/
[__getobj__]: http://ruby-doc.org//stdlib-2.1.0/libdoc/delegate/rdoc/SimpleDelegator.html#method-i-__getobj__

## Message

The `ServiceObjects::Base#messages` collects messages with text, type and optional priority:

```ruby
message = ServiceObjects::Message.new priority: 0, type: "bar", text: "foo"
message.priority    # => 0.0
message.type        # => "info"
message.text        # => "some text"
message.to_h        # => { type: "bar", type: "foo" }
message.to_json     # => "{\"type\":\"bar\",\"text\":"\foo\"}"
```

When a priority hasn't been defined explicitly, it is set to `-1.0` for errors, and to `0.0` otherwise. Messages are sorted by priority, type and text in a "natural" order.

Use the `#add_message` helper to add a message to the collection:

```ruby
service = ServiceObjects::Base.new
service.send :add_message type: "info", text: "some text"
service.send :messages
# => [<Message type="info" text="some text" priority=0.0>]
```

When a `text:` value is a symbol, it is translated in the scope of current service class:

```yaml
# config/locales/en.yml
---
en:
  activemodel:
    messages:
      models:
        foo:
          excuse: # the type of the message
            not_informed: "I haven't been informed on the %{subject}"
```

```ruby
service.send :add_message type: "excuse", text: :not_informed, subject: "issue"
# => [<Message text="I haven't been informed on the issue" ...>]
```

## Compatibility

Tested under MRI rubies >= 2.1

RSpec 3.0+ used for testing

Collection of testing, debugging and code metrics is defined
in the [hexx-suit](https://github.com/nepalez/hexx-suit) gem.

To run tests use `rake test`, to run code metrics use `rake check`. All the metric settings are collected in the `config/metrics` folder.

## Contributing

* Fork the project.
* Read the [Styleguide](file:config/metrics/STYLEGUIDE).
* Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a
  future version unintentionally.
* Commit, do not mess with Rakefile or version
  (if you want to have your own version, that is fine but bump version
  in a commit by itself I can ignore when I pull)
* Send me a pull request. Bonus points for topic branches.

## License

See [MIT LICENSE][license]