Policy ====== [![Gem Version](https://img.shields.io/gem/v/policy.svg?style=flat)][gem] [![Build Status](https://img.shields.io/travis/nepalez/policy/master.svg?style=flat)][travis] [![Dependency Status](https://img.shields.io/gemnasium/nepalez/policy.svg?style=flat)][gemnasium] [![Code Climate](https://img.shields.io/codeclimate/github/nepalez/policy.svg?style=flat)][codeclimate] [![Coverage](https://img.shields.io/coveralls/nepalez/policy.svg?style=flat)][coveralls] [![Inline docs](http://inch-ci.org/github/nepalez/policy.svg)][inch] [codeclimate]: https://codeclimate.com/github/nepalez/policy [coveralls]: https://coveralls.io/r/nepalez/policy [gem]: https://rubygems.org/gems/policy [gemnasium]: https://gemnasium.com/nepalez/policy [travis]: https://travis-ci.org/nepalez/policy [inch]: https://inch-ci.org/github/nepalez/policy A tiny library to implement a **Policy Object pattern**. **NOTE** the gem was re-written from scratch in v2.0.0 (see Changelog section below) Introduction ------------ The gem was inspired by: * the CodeClimate's blog post "[7 ways to decompose fat ActiveRecord module]". * the Chapter 10 of the book "[Domain-Driven Design]" by Eric Evans. A **Policy Object** (assertion, invariant) encapsulates a business rule in isolation from objects (such as entities or services) following it. Policy Objects can be combined by logical operators `and`, `or`, `xor`, `not` to provide complex policies. This approach gives a number of benefits: * It makes business rules **explicit** instead of spreading and hiding them inside application objects. * It allows definition of rules for **numerous attributes** at once that should correspond to each other in some way. * It makes the rules **simple** and **reusable** in various context and combinations. * It makes complex rules **testable** in isolation from their parts. [7 ways to decompose fat ActiveRecord module]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ [Domain-Driven Design]: http://www.amazon.com/dp/B00794TAUG/ Installation ------------ Add this line to your application's Gemfile: ```ruby gem "policy", ">= 1.0" ``` 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. ```ruby # The account has a limit class Account < Struct.new(:owner, :limit); end # The transaction belongs to account and has a sum (< 0 for withdrawals) class Transaction < Struct.new(:account, :sum); end # The transfer, connecting two separate transactions class Transfer < Struct.new(:withdrawal, :enrollment); end ``` What we need is to apply set of policies: **The sum of withdrawal's and enrollment's sums should be 0.** **The sum of withdrawal doesn't exceed the accounts' limit.** **The sum of transfers between client's own accounts can exceed the limit.** Let's do it with Policy Objects! ### Policy Declaration Define policies with `Policy::Base` module included. Tnen use [ActiveModel::Validations] methods to describe its rules: ```ruby # An arbitrary namespace for financial policies module Policies # Withdrawal from one account should be equal to enrollment to another class Consistency < Struct.new(:withdrawal, :enrollment) include Policy::Base validates :withdrawal, :enrollment, presence: true validates :total_sum, numericality: { equal_to: 0 } private def total_sum withdrawal.sum + enrollment.sum end end # The sum of withdrawal doesn't exceed the accounts' limit class Limited < Struct.new(:withdrawal) include Policy::Base validate :not_exceeds_the_limit private def not_exceeds_the_limit return if withdrawal.sum + withdrawal.limit > 0 errors.add :base, :exceeds_the_limit end end # The transfer is made between client's own accounts class InternalTransfer < Struct.new(:withdrawal, :enrollment) include Policy::Base validate :the_same_client private def the_same_client return if withdrawal.account.owner == enrollment.account.owner errors.add :base, :different_owners end end end ``` [Struct]: http://ruby-doc.org//core-2.2.0/Struct.html [ActiveModel::Validations]: http://apidock.com/rails/ActiveModel/Validations ### Combining Policies Use `and`, `or`, `xor` instance methods to provide complex policies from elementary ones. You can use factory methods: ```ruby module Policies module LimitedOrInternal def self.new(withdrawal, enrollment) InternalTransfer.new(withdrawal, enrollment).or Limited.new(withdrawal) end end end ``` As an alternative to instance methods, use the `Policy` module's methods: ```ruby def self.new(withdrawal, enrollment) Policy.or( InternalTransfer.new(withdrawal, enrollment), Limited.new(withdrawal) ) end ``` To provide negation use `and.not`, `or.not`, `xor.not` syntax: ```ruby first_policy.and.not(second_policy, third_policy) # this is equal to: Policy.and(first_policy, Policy.not(second_policy), Policy.not(third_policy)) ``` Policies can composed at any number of levels. ### Following Policies Include the `Policy::Follower` module to the policies follower class. Use the **class** method `.follows_policies` to declare policies (like ActiveModel::Validations `.validate` method does). ```ruby class Transfer < Struct.new(:withdrawal, :enrollment) include Policy::Follower follows_policies :consistent, :limited_or_internal private def consistent Policies::Consistency.new(withdrawal, enrollment) end def limited_or_internal Policies::LimitedOrInternal.new(withdrawal, enrollment) end end ``` Surely, you can skip creating `LimitedOrInternal` builder and combine policies for current class only: ```ruby def limited_or_internal limited.or internal end def limited Policies::Limited.new(withdrawal) end def internal Policies::Internal.new(withdrawal, enrollment) end ``` [builder]: http://sourcemaking.com/design_patterns/builder ### Checking Policies Use the **instance** method `follow_policies?` to check whether an instance follows policies. The method checks all policies and **raises** the `Policy::ViolationError` when the first followed policy is broken. ```ruby transfer = Transfer.new( Transaction.new(Account.new("Alice", 50), -100), Transaction.new(Account.new("Bob", 50), 100) ) transfer.follow_policies? # => because Alice's limit of 50 is exceeded ``` The method doesn't mutate the follower. It collects errors inside the exception `#errors` method, not the follower's one. ```ruby begin transfer.follow_policies? rescue ViolationError => err err.errors end ``` You can check subset of policies by calling the method with policy names: ```ruby transfer.follow_policies? :consistent # passes because the transfer is consistent: -100 + 100 = 0 # this doesn't check the :limited_or_internal policy ``` The method ignores policies, not declared by `.follows_policies` class method. The method has singular alias `follow_policy?(name)` that accepts one argument. Scaffolding ----------- You can scaffold the policy with its specification and necessary translations using the generator: ``` policy new consistency -n policies financial -a withdrawal enrollment -l fr de ``` For a list of available options call the generator with an `-h` option: ``` policy new -h ``` Changelog --------- Version 2 was redesigned and rewritten from scratch. The main changes are: * Instead of building policy with a `Policy.new` method, it is now created by including the `Policy::Base` module. In the previous version building a policy was needed to define an order of policy attributes. Now the definition of policy attributes is not the responsibility of the gem. * Instead of generating policies in a class scope (in the ActiveModel `validates` style), the `.follows_policy` refers to followers' instance methods (in the ActiveModel `validate` style). This allows combining policy objects with logical expressions. Policies themselves becames more DRY, granular and testable in isolation. * Instead of mutating the follower, `follow_policy?` method raises an exception. This allows follower to be immutable (frozen). The follower doesn't need to be messed with `ActiveModule::Validations` at all. This approach makes `follow_policy!` method unnecessary. Compatibility ------------- Tested under rubies, compatible with MRI 2.0+: * MRI rubies 2.0+ * Rubinius 2+ (2.0+ mode) * JRuby 9000 (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. [RSpec]: http://rspec.info/ [hexx-suit]: https://github.com/nepalez/hexx-suit/ [ActiveModel::Validations]: http://apidock.com/rails/v3.1.0/ActiveModel/Validations Contributing ------------ * Fork the project. * Read the [STYLEGUIDE](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).