Policy

Gem Version Build Status Dependency Status Code Climate Coverage Inline docs

A tiny library to implement a Policy Object pattern.

The gem was inspired by:

A Policy Object encapsulates a business rule in isolation from objects (such as entities or services) following it.

This separation provides a number of benefits:

  • It makes business rules explicit instead of spreading and hiding them inside application objects.
  • It makes the rules reusable in various context (think of the transaction consistency both in bank transfers and cach machine withdrawals).
  • It allows definition of rules for numerous attributes that should correspond to each other in some way.
  • It makes complex rules testable in isolation from even more complex objects.

Installation

Add this line to your application's Gemfile:

gem "policy"

And then execute:

$ bundle

Or install it yourself as:

$ gem install policy

Usage

The Model for Illustration

Suppose an over-simplified model of bank account transactions and account-to-account transfers.

# The account transaction (either enrollment or witdrawal)
class Transaction < Struct.new(:sum); end

# The transfer, connecting two separate transactions
# (maybe this isn't an optimal model, but helpful for the subject)
class Transfer < Struct.new(:withdrawal, :enrollment); end

What we need is to apply the simple policy (invariant):

The sum of withdrawal's and enrollment's sums should be 0.

Let's do it with Policy Objects!

Policy Declaration

Define policies with a list of necessary attributes like using Struct.

Tnen use ActiveModel::Validations methods to describe its rules:

# An arbitrary namespace for financial policies
module Policies::Financial

  # Withdrawal from one account should be equal to enrollment to another
  class Consistency < Policy.new(:withdrawal, :enrollment)

    validates :withdrawal, :enrollment, presence: true
    validates :total_sum, numericality: { equal_to: 0 }

    private

    def total_sum
      withdrawal.sum + enrollment.sum
    end
  end
end

Note a policy knows nothing about the complex nature of its attributes until their quack like transactions with #sum method defined.

Scaffolding a Policy

You can scaffold the policy with its specification and necessary translations using the generator:

policy new

For a list of available options call the generator with a -h option:

policy new -h

Following a Policy

Include the Policy::Follower module to the class and apply policies to corresponding attributes with follow_policy class method.

class Transfer < Struct.new(:withdrawal, :enrollment)
  include Policy::Follower # also includes ActiveModel::Validations

  follow_policy Policies::Financial::Consistency, :withdrawal, :enrollment
end

The order of attributes should correspond to the policy definition.

You can swap attributes (this is ok for our example)...

follow_policy Policies::Financial::Consistency, :enrollment, :withdrawal

...or use the same attribute several times when necessary (not in our example, though):

follow_policy Policies::Financial::Consistency, :withdrawal, :withdrawal

Applied policies can be grouped by namespaces (useful when the object should follow many policies):

use_policies Policies::Financial do
  follow_policy :Consistency, :withdrawal, :enrollment
end

Policies Verification

To verify object use #follow_policies? or #follow_policies! instance methods.

Transaction = Struct.new(:account, :sum)
withdrawal  = Transaction.new(, -100)
enrollment  = Transaction.new(, 1000)

transfer = Transfer.new withdrawal, enrollment

transfer.follow_policies?
# => false

transfer.follow_policies!
# => raises <Policy::ViolationError>

The policies are verified one-by-one until the first break - in just the same order they were declared.

Asyncronous Verification

Define names for policies using as: option. The names should be unique in the class' scope:

class Transfer < Struct.new(:withdrawal, :enrollment)
  include Policy::Follower

  use_policies Policies::Financial do
    follow_policy :Consistency, :withdrawal, :enrollment, as: :consistency
  end
end

Check policies by names (you can also use singular forms follow_policy? and follow_policy!):

# Checks only consistency and skips all other policies
transfer.follow_policy? :consistency
transfer.follow_policy! :consistency

The set of policies can be checked at once:

transaction.follow_policies? :consistency, ...

Now the policies are verified one-by-one in given order (it may differ from the order of policies declaration) until the first break.

Compatibility

Tested under rubies, compatible with MRI 2.0+:

  • MRI rubies 2.0+
  • Rubinius 2+ (2.0+ mode)
  • JRuby head (2.0+ mode)

Rubies with API 1.9 are not supported.

Uses ActiveModel::Validations - tested for 3.1+

Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.

Contributing

  • Fork the project.
  • Read the 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.