Hexx

Gem Version Bild
Status Code Metrics Dependency Status Coverage Status License

The base library for domain models.

API

Includes classes and modules as below:

Hexx::Service

The base class for service objects.

Hexx::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 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.

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
# #<Coercer @wrapped_string="Ivo" >

Be careful when designing a coercer class. Its constructor should accept both the raw value ("Ivo") and the coerced one (#<Coercer @wrapped_string = "Ivo">). 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:

A typical service object is shown below:

# app/services/add_item.rb
require 'hexx'
class AddItem < Hexx::Service

  # 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

  # Declares specific exceptions to be raised by the #run! method
  # and processed by the #run differently.
  #
  # All other exceptions will be re-raised as Hexx::Service::Invalid
  # and processed by publishing the :error notification.
  raises :Found

  # 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 GetItem, :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::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 = item
    self.messages.concat 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::Message

The messages published by the service has two attributes: type and text.

message = Hexx::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::Message @type="error", @text="text" >]

Hexx::Dependable

The module provides the depends_on class helper for setter-based dependency injection. It allows decoupling the service from another services it uses.

Extend the service class and declare the dependencies with an optional default implementation (see example above):

class AddItem < Hexx::Service
  extend Hexx::Dependable

  depends_on :get_item, default: GetItem

  # ...

  def find_item
    run_service get_item, :on_item, name: name
  end
end

Now the dependency can be injected afterwards:

# The default implementation
service = AddItem.new
service.get_item # => GetItem

# Change it to other implementation
service.get_item = FindItem
service.get_item # => FindItem

# Reset it to default by assigning +nil+
service.get_item = nil
service.get_item # => GetItem

It is possible to test a service in isolation from its dependencies.

# spec/services/my_service_spec.rb
describe AddService do

  describe "#run" do

    # Mock a service objects to publish expected notifications
    let(:object) { Hexx::Service }
    before { allow(object).to receive(:run) { publish :not_found } }

    # Inject a class dependency
    before { service.get_item = class_double "Hexx::Service", new: object }

    # ...
  end
end

Hexx::Name

The module provides the base class for name constructors for various instances.

It declares helpers:

  • object

    the attribute for the object to be named

  • locale

    the locale to name the object in

  • scope

    the current translation scope

  • +t(value, options)

    the translator of the value in current scope and locale

Inherit the class and reload its #for instance method:

module Names
  class Hash < Hexx::Name
    def for
      user ? t(:user, value: user) : t(:empty)
    end

    def user
      value = object[:user]
      value ? value.upcase : nil
    end
  end
end

Add the necessary translations:

# config/locales/ru.yml
ru:
  activemodel:
    names/hash:
      user:  "чувак %{value}"
      empty: "н/д"

# config/locales/en.yml
en:
  activemodel:
    names/hash:
      user: "dude %{value}"
      empty: "-"

Then use its for class method to construct names:

Names::Hash.for { user: "Ivan" }, locale: :en
# => "dude IVAN"

Names::Hash.for { user: nil }, locale: :ru
# => "н/д"

Hexx::Null

The class implements the 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, use double negation in logical expressions:

# Though:
Hexx::Null && true   # => true

# But:
!!Hexx::Null && true # => false

License

The project is distributed under the MIT LICENSE.