├── .yardopts ├── lib ├── encapsulate_as_money │ ├── version.rb │ └── activemodel_integration.rb └── encapsulate_as_money.rb ├── Gemfile ├── gemfiles ├── rails-5.2.gemfile ├── rails-6.0.gemfile └── rails-6.1.gemfile ├── Rakefile ├── script └── setup.sh ├── Appraisals ├── spec ├── spec_helper.rb ├── integration │ └── activemodel_spec.rb ├── integration_helper.rb └── encapsulate_as_money_spec.rb ├── .gitignore ├── .github └── workflows │ └── test.yml ├── LICENSE.txt ├── encapsulate_as_money.gemspec └── README.md /.yardopts: -------------------------------------------------------------------------------- 1 | - LICENSE.txt 2 | -------------------------------------------------------------------------------- /lib/encapsulate_as_money/version.rb: -------------------------------------------------------------------------------- 1 | module EncapsulateAsMoney 2 | VERSION = '0.3' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in encapsulate_as_money.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /gemfiles/rails-5.2.gemfile: -------------------------------------------------------------------------------- 1 | 2 | source "https://rubygems.org" 3 | 4 | gem "activemodel", "~> 5.2.4" 5 | 6 | gemspec path: "../" 7 | -------------------------------------------------------------------------------- /gemfiles/rails-6.0.gemfile: -------------------------------------------------------------------------------- 1 | 2 | source "https://rubygems.org" 3 | 4 | gem "activemodel", "~> 6.0.3" 5 | 6 | gemspec path: "../" 7 | -------------------------------------------------------------------------------- /gemfiles/rails-6.1.gemfile: -------------------------------------------------------------------------------- 1 | 2 | source "https://rubygems.org" 3 | 4 | gem "activemodel", "~> 6.1.1" 5 | 6 | gemspec path: "../" 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | require "yard" 4 | 5 | task :default => :spec 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | YARD::Rake::YardocTask.new 10 | -------------------------------------------------------------------------------- /script/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This command will setup your local dev environment, including" 4 | echo " * bundle install" 5 | echo 6 | 7 | echo "Bundling..." 8 | bundle install --binstubs bin --path .bundle 9 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails-3_2' do 2 | gem 'activemodel', '3.2.22.2' 3 | end 4 | 5 | appraise 'rails-4_0' do 6 | gem 'activemodel', '4.0.13' 7 | end 8 | 9 | appraise 'rails-5_0' do 10 | gem 'activemodel', '5.0.6' 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'encapsulate_as_money' 3 | require 'rspec/given' 4 | require 'integration_helper' if ENV['APPRAISAL_INITIALIZED'] || ENV['TRAVIS'] 5 | 6 | RSpec.configure do |config| 7 | config.color = true 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | bin/ 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | *.bundle 20 | *.gemfile.lock 21 | *.so 22 | *.o 23 | *.a 24 | mkmf.log 25 | -------------------------------------------------------------------------------- /lib/encapsulate_as_money/activemodel_integration.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'active_model' 3 | 4 | if [4,5].include?(ActiveModel::VERSION::MAJOR) 5 | module ActiveModel::Validations::Clusivity 6 | private 7 | 8 | def inclusion_method(enumerable) 9 | if enumerable.first.is_a?(Money) 10 | :cover? 11 | else 12 | return :include? unless enumerable.is_a?(Range) 13 | case enumerable.first 14 | when Numeric, Time, DateTime 15 | :cover? 16 | else 17 | :include? 18 | end 19 | end 20 | end 21 | end 22 | end 23 | rescue LoadError 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tests 3 | on: [ push, pull_request ] 4 | jobs: 5 | test: 6 | name: 'Test { ruby: ${{ matrix.ruby }}, gems: ${{ matrix.gemfile }} }' 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby: [ '2.6', '2.7', '3.0' ] 11 | gemfile: [ rails-5.2, rails-6.0, rails-6.1 ] 12 | env: 13 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Ruby ${{ matrix.ruby }} 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ${{ matrix.ruby }} 20 | bundler-cache: true 21 | - name: RSpec 22 | run: bundle exec rspec 23 | -------------------------------------------------------------------------------- /spec/integration/activemodel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ActiveModel integration", skip: !ENV['APPRAISAL_INITIALIZED'] && !ENV['TRAVIS'] do 4 | describe '.validates_inclusion_of' do 5 | it "validates money correctly" do 6 | expect(Price.new(amount: Money.new(0))).not_to be_valid 7 | expect(Price.new(amount: Money.new(1))).to be_valid 8 | expect(Price.new(amount: Money.new(2))).to be_valid 9 | expect(Price.new(amount: Money.new(3))).not_to be_valid 10 | end 11 | 12 | it "validates numbers correctly" do 13 | expect(Price.new(qty: 0)).not_to be_valid 14 | expect(Price.new(qty: 1)).to be_valid 15 | expect(Price.new(qty: 2)).to be_valid 16 | expect(Price.new(qty: 3)).not_to be_valid 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/integration_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | 3 | class Price 4 | extend EncapsulateAsMoney 5 | 6 | case ActiveModel::VERSION::MAJOR 7 | when 3 then include ActiveModel::AttributeMethods 8 | when 4 then include ActiveModel::Model 9 | when 5 then include ActiveModel::Model 10 | else raise NotImplementedError, ActiveModel::VERSION::MAJOR 11 | end 12 | 13 | include ActiveModel::Validations 14 | 15 | encapsulate_as_money :amount 16 | validates_inclusion_of :amount, in: Money.new(1)..Money.new(2), allow_nil: true 17 | validates_inclusion_of :qty, in: 1..2, allow_nil: true 18 | 19 | def initialize(attributes = {}) 20 | @amount = attributes[:amount] 21 | @qty = attributes[:qty] 22 | end 23 | 24 | def amount 25 | @amount 26 | end 27 | 28 | def amount=(amount) 29 | @amount = amount 30 | end 31 | 32 | def qty 33 | @qty 34 | end 35 | 36 | def qty=(qty) 37 | @qty = qty 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/encapsulate_as_money.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "encapsulate_as_money/version" 3 | require "encapsulate_as_money/activemodel_integration" 4 | require "money" 5 | 6 | module EncapsulateAsMoney 7 | def encapsulate_as_money(*attributes) 8 | options = extract_options(attributes) 9 | attributes.each do |attribute| 10 | encapsulate_attribute_as_money(attribute, options[:preserve_nil]) 11 | end 12 | end 13 | 14 | private 15 | 16 | def encapsulate_attribute_as_money(attribute, preserve_nil = true) 17 | if preserve_nil 18 | define_method attribute do 19 | Money.new(super()) if super() 20 | end 21 | 22 | define_method "#{attribute}=" do |money| 23 | super(money && money.fractional) 24 | end 25 | else 26 | define_method attribute do 27 | Money.new(super() || 0) 28 | end 29 | 30 | define_method "#{attribute}=" do |money| 31 | num = (money && money.fractional) || 0 32 | super(num) 33 | end 34 | end 35 | end 36 | 37 | def extract_options(args) 38 | args.last.is_a?(Hash) ? args.pop : {} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Envato 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /encapsulate_as_money.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "encapsulate_as_money/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "encapsulate_as_money" 8 | spec.version = EncapsulateAsMoney::VERSION 9 | spec.authors = ["Anthony Sellitti", "Orien Madgwick", "Keith Pitt", "Martin Jagusch", "Mark Turnley", "Pete Yandall"] 10 | spec.email = ["anthony.sellitti@envato.com", "_@orien.io", "me@keithpitt.com", "_@mj.io", "mark@envato.com", "pete@envato.com"] 11 | spec.summary = "Represent model attributes as Money instances" 12 | spec.homepage = "https://github.com/envato/encapsulate_as_money" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "money", ">= 5.1.0" 21 | 22 | spec.add_development_dependency "bundler" 23 | spec.add_development_dependency "rake" 24 | spec.add_development_dependency "rspec" 25 | spec.add_development_dependency "rspec-given" 26 | spec.add_development_dependency "yard" 27 | spec.add_development_dependency "appraisal" 28 | spec.add_development_dependency "pry" 29 | end 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EncapsulateAsMoney 2 | 3 | [![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/encapsulate_as_money/blob/master/LICENSE.txt) 4 | [![Gem Version](https://img.shields.io/gem/v/encapsulate_as_money.svg?maxAge=2592000)](https://rubygems.org/gems/encapsulate_as_money) 5 | [![Gem Downloads](https://img.shields.io/gem/dt/encapsulate_as_money.svg?maxAge=2592000)](https://rubygems.org/gems/encapsulate_as_money) 6 | [![Build Status](https://github.com/envato/encapsulate_as_money/workflows/tests/badge.svg?branch=master)](https://github.com/envato/encapsulate_as_money/actions?query=branch%3Amaster+workflow%3Atests) 7 | 8 | Want your model attribute to be a [Money](https://github.com/RubyMoney/money) 9 | instance? EncapsulateAsMoney provides a simple way to get this done! 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'encapsulate_as_money' 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | ## Usage 22 | 23 | ### Rails 24 | 25 | Add the `encapsulate_as_money` method to the Active Record base class. 26 | 27 | ```ruby 28 | ActiveRecord::Base.extend(EncapsulateAsMoney) 29 | ``` 30 | 31 | Now say you have the model: 32 | 33 | ```ruby 34 | class MyModel < ActiveRecord::Base 35 | encapsulate_as_money :amount 36 | end 37 | ``` 38 | 39 | Which is based on the database table: 40 | 41 | ```ruby 42 | create_table "my_models" do |table| 43 | table.integer "amount" 44 | end 45 | ``` 46 | 47 | Now we can create and save an instance like: 48 | 49 | ```ruby 50 | MyModel.create!(amount: 5.dollars) 51 | ``` 52 | 53 | This will create a row as such: 54 | 55 | | id | amount | 56 | | --:| ------:| 57 | | 1 | 500 | 58 | 59 | Note the value is represented as cents. 60 | 61 | Once persisted we can find the value like: 62 | 63 | ```ruby 64 | MyModel.find_by_id(1).amount #=> 5.dollars 65 | ``` 66 | 67 | Note that it uses the default Money currency. 68 | 69 | ## Contributing 70 | 71 | 1. Fork it ( https://github.com/envato/encapsulate_as_money/fork ) 72 | 2. Create your feature branch (`git checkout -b my-new-feature`) 73 | 3. Commit your changes (`git commit -am 'Add some feature'`) 74 | 4. Push to the branch (`git push origin my-new-feature`) 75 | 5. Create a new Pull Request 76 | -------------------------------------------------------------------------------- /spec/encapsulate_as_money_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "spec_helper" 3 | 4 | describe EncapsulateAsMoney do 5 | 6 | Given(:model_base_class_with_attr) { 7 | Class.new { 8 | extend EncapsulateAsMoney 9 | attr_accessor :attribute 10 | } 11 | } 12 | Given(:model_instance) { model_class.new } 13 | 14 | describe "encapsulating the attribute as money" do 15 | When(:model_class) { 16 | Class.new(model_base_class_with_attr) { 17 | encapsulate_as_money :attribute 18 | } 19 | } 20 | 21 | describe "reading" do 22 | Given!(:init_attr_value) { model_instance.instance_variable_set :@attribute, initial_attr_value } 23 | 24 | context "initial value is nil" do 25 | Given(:initial_attr_value) { nil } 26 | Then { model_instance.attribute == Money.zero } 27 | end 28 | 29 | context "initial value is 0" do 30 | Given(:initial_attr_value) { 0 } 31 | Then { model_instance.attribute == Money.zero } 32 | end 33 | 34 | context "initial value is 1" do 35 | Given(:initial_attr_value) { 1 } 36 | Then { model_instance.attribute == Money.new(1) } 37 | end 38 | end 39 | 40 | describe "writing" do 41 | 42 | context "a value of $1" do 43 | When { model_instance.attribute = Money.new(1_00) } 44 | Then { model_instance.instance_variable_get(:@attribute) == 1_00 } 45 | end 46 | 47 | context "a value of $0" do 48 | When { model_instance.attribute = Money.zero } 49 | Then { model_instance.instance_variable_get(:@attribute) == 0 } 50 | end 51 | 52 | context "a value of nil" do 53 | When { model_instance.attribute = nil } 54 | Then { model_instance.instance_variable_get(:@attribute) == 0 } 55 | end 56 | end 57 | end 58 | 59 | describe "encapsulating the attribute as money, preserving nil" do 60 | When(:model_class) { 61 | Class.new(model_base_class_with_attr) { 62 | encapsulate_as_money :attribute, :preserve_nil => true 63 | } 64 | } 65 | 66 | describe "reading" do 67 | Given!(:init_attr_value) { model_instance.instance_variable_set :@attribute, initial_attr_value } 68 | 69 | context "initial value is nil" do 70 | Given(:initial_attr_value) { nil } 71 | Then { model_instance.attribute == nil } 72 | end 73 | 74 | context "initial value is 0" do 75 | Given(:initial_attr_value) { 0 } 76 | Then { model_instance.attribute == Money.zero } 77 | end 78 | 79 | context "initial value is 1" do 80 | Given(:initial_attr_value) { 1 } 81 | Then { model_instance.attribute == Money.new(1) } 82 | end 83 | end 84 | 85 | describe "writing" do 86 | 87 | context "a value of $1" do 88 | When { model_instance.attribute = Money.new(1_00) } 89 | Then { model_instance.instance_variable_get(:@attribute) == 1_00 } 90 | end 91 | 92 | context "a value of $0" do 93 | When { model_instance.attribute = Money.zero } 94 | Then { model_instance.instance_variable_get(:@attribute) == 0 } 95 | end 96 | 97 | context "a value of nil" do 98 | When { model_instance.attribute = nil } 99 | Then { model_instance.instance_variable_get(:@attribute).nil? } 100 | end 101 | end 102 | end 103 | end 104 | --------------------------------------------------------------------------------