= Hexx
{}[https://rubygems.org/gems/hexx]
{}[https://travis-ci.org/nepalez/hexx]
{}[https://codeclimate.com/github/nepalez/hexx]
{}[https://gemnasium.com/nepalez/hexx]
{}[https://coveralls.io/r/nepalez/hexx]
{}[https://github.com/nepalez/hexx/blob/master/LICENSE.rdoc]
The base library for domain models.
== API
Includes classes and modules as below:
Hexx::Service:: The base class for service objects.
Hexx::Service::Message:: The message provided by service objects.
Hexx::Null:: The Null object.
Hexx::Coercible:: The module that makes model attributes coercible.
Hexx::Configurable:: The module to convert a core domain module to the
dependency injection framework.
Hexx::Dependable:: The module provides +depends_on+ class helper methodto
to implement the setter dependency injection.
The module is expected to be used in PORO domains for Ruby MRI 2.1+.
For usage in active record bases domains consider the
{ hexx-active_record }[https://github.com/nepalez/hexx-active_record]
gem extension.
== Installation
Add this line to your application's Gemfile:
gem "hexx", "~> 2.0"
And then execute:
$ bundle
Or install it yourself as:
$ gem install hexx
== Usage
=== Hexx::Configurable
Adds the +configure+ and +depends_on+ helpers to the module to convert it
to the {dependency injection container}[http://en.m.wikipedia.org/wiki/Dependency_injection].
Extend the base class of the gem and declare the module dependencies from
outer classes and modules with the +depend_on+ helper:
# lib/my_gem.rb
module MyGem
extend Hexx::Configurable
depend_on :get_item, :add_item
end
Inject the dependencies in the gem config with the +configure+ wrapper:
# config/dependencies.rb
MyGem.configure do |c|
c.get_item = OuterModule::Services::Get
c.add_item = OuterModule::Services::Add
end
Use the dependencies somewhere inside the code of the gem:
MyGem.get_item # => OuterModule::Services::Get
=== Hexx::Coercible
Adds the +attr_coerced+ class helper method to the PORO model.
Provide a value object that accepts 0..1 arguments.
# app/attributes/coercer.rb
class Coercer < MultiByte::Chars
def self.new(source = nil)
return unless source
end
def initialize(source)
# ...
end
end
Extend the model with a +Coercible+ module and declare its attributes
with the +attr_coerced+ helper.
# app/models/some_model.rb
class SomeModel
extend Hexx::Coercible
attr_coerced :name, type: Coercer
end
Both the getter and setter will return the coerced value, provided by
the +Coercer+ class.
object = SomeModel.new name: "Ivo"
object.name
# #
Be careful when designing a coercer class. Its constructor should accept both
the raw value ("Ivo") and the coerced one (#).
This is needed because the coercer works twofold - it coerces both the
setter and getter. The getter coercer will take the coerced value.
This feature is added for compatibility with +ActiveRecord+ attributes
whose getters gives raw values from a database.
*Note*: The coercer from the +hexx+ gem itself won't work for +ActiveRecord+ models.
Use the +hexx-active_record+ gem instead. The gem extends the +Coercible+ model
so that the +attr_coerced+ reloads +ActiveRecord+ attributes properly.
=== Hexx::Service
Inherit services from the Hexx::Service class.
The class implements a set of patterns:
* The {Service object pattern}[http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/] used to decouple business logics from both the models and web delivery mechanism (such as +Rails+).
* The {Observer pattern}[http://reefpoints.dockyard.com/2013/08/20/design-patterns-observer-pattern.html] to follow the {Tell, don't ask}[http://martinfowler.com/bliki/TellDontAsk.html] design princible.
The pattern is implemented with the help of {wisper}[http://www.github.com/krisleech/wisper] gem by Kris Leech.
* The {Setter-based dependency injection}[http://brandonhilkert.com/blog/a-ruby-refactor-exploring-dependency-injection-options/] to decouple the service from another services it uses.
A typical service object is shown below:
# app/services/add_item.rb
require 'hexx'
class AddItem < Hexx::Service
# Injects the dependency from the service for getting an item.
# Provides default implementation for the dependency, that could be
# redefined later (in a test suite etc.).
depends_on :get_item, default: GetItem
# Whitelists parameters and defines corresponding attributes.
# For example, the #name attribute is avalable.
allow_params :name
# Defines some validation using ActiveModel::Validations helpers.
validate :name, presence: true
# Runs a service
def run
run!
rescue Found
# Publishes notification in case the item exists.
publish :found, item
rescue => err
publish :error, err.messages
else
# The notification to be published if the #run! raises nothing.
publish :added, item
end
private
attr_accessor :item
# Errors to be raised by the #run! method call and captured in a #run.
class Found < StandardError; end
# The sequence of the service steps. Any step can raise error to
# be rescued in #run with publishing a corresponding notification.
def run!
find_item
add_item
end
def find_item
# The method runs another service and listens to its notifications
# via private callback methods available to that service only.
# The callback names should start from given prefix (:on_item_).
run_service get_item, :on_item, name: name
end
# The callback to listen to :found notification of the 'get_item' service.
def on_item_found(item, *)
@item = item
# Adds the Hexx::Message object of type "error" to the +messages+ array.
# The :not_found key will be translated in context of current service:
# {locale}.activemodule.messages.models.add_item.not_found
add_message "error", :not_found
fail Found # goes to publishing a result
end
# The callback to listen to :error notification of the 'get_item' service.
# that is expected to publish a list of error messages.
def on_item_error(*, messages)
# The helper raises Hexx::Service::Invalid exception where the messages
# are added to. The exception will be rescued by the #run method.
on_error(messages)
end
def add_item
# The escape re-raises any error as the Hexx::Service::Invalid
# with the array of Hexx::Service::Message messages.
escape { @item = Item.create! name: name }
end
end
A typical usage of the service (in a Rails controller):
# app/controllers/items_controller.rb
class ItemsController < ActionController::Base
# Creates an item with given name
def create
service = AddItem.new params.allow(:name)
service.subscribe self, prefix: :on
service.run
end
# Publishes a success message
def on_created(item, messages)
@item, @messages = item, messages
render "created", status: 201
end
# Responds with 304 (not changed)
def on_found(*)
render nothing: true, status: 304
end
# Publishes an error messages
def on_error(messages)
@messages = messages
render "error", status: 422
end
end
The controller knows nothing about the action itself. It only needs to
send the request to a corresponding service and sort out the notifications.
=== Hexx::Service::Message
The messages published by the service has two attributes: +type+ and +text+.
message = Hexx::Service::Message.new type: :error, text: "some error message"
message.type # => "error"
message.text # => "some error message"
Inside a service use the +add_message+ to add message to the +messages+ array:
add_message "error", "text"
messages # => [#]
=== Hexx::Null
The class implements the {Null object}[http://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object] pattern. The object:
* responds like +nil+ to <=>, +eq?+, +nil?+, +false?+, +true?+, +to_s+,
+to_i+, +to_f+, +to_c+, +to_r+, +to_nil+
* responds with +self+ to any other method call
Providing {this problem}[http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/], use double negation in logical expressions:
# Though:
Hexx::Null && true # => true
# But:
!!Hexx::Null && true # => false
=== Hexx::Dependable
This module is a part of Hexx::Service that provides
setter dependency declaration +depends_on+.
Extend the class and declare the dependency with optional default
implementation:
class MyClass
extend Hexx::Dependable
depends_on :another_class, default: AhotherClass
depends_on :one_more_class
end
Now the dependency can be injected afterwards:
object = MyClass
# The default implementation
object.another_class # => AnotherClass
# Inject another implementation
object.another_class = NewClass
object.another_class # => NewClass
# Reset it to default
object.another_class = nil
object.another_class # => AnotherClass
# Implementation is needed
object.one_more_class
# fails with a NotImplementedError
== License
The project is distributed under the {MIT LICENSE}[LICENSE.rdoc].