= Hexx {Gem Version}[https://rubygems.org/gems/hexx] {Bild Status}[https://travis-ci.org/nepalez/hexx] {Code Metrics}[https://codeclimate.com/github/nepalez/hexx] {Dependency Status}[https://gemnasium.com/nepalez/hexx] {Coverage Status}[https://coveralls.io/r/nepalez/hexx] {License}[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].