diff --git a/.gitignore b/.gitignore index a9c888f..01b37d0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,22 +28,22 @@ pkg # # For MacOS: # -#.DS_Store +.DS_Store # For TextMate -#*.tmproj -#tmtags +*.tmproj +tmtags # For emacs: -#*~ -#\#* -#.\#* +*~ +\#* +.\#* # For vim: -#*.swp +*.swp # For redcar: -#.redcar +.redcar # For rubinius: -#*.rbc +*.rbc diff --git a/Gemfile b/Gemfile index eedb107..c6e58f4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,21 @@ -source "http://rubygems.org" -# Add dependencies required to use your gem here. -# Example: -# gem "activesupport", ">= 2.3.5" +source :rubygems + +gem 'money' + +gem 'mongoid', "~> 3.0.0.rc" +gem 'moped' + +# gem 'mongoid', "~> 2.4" +# gem 'bson' -# Add dependencies to develop your gem here. -# Include everything needed to run rake, tests, features, etc. group :development do - gem "rspec", "~> 2.8.0" - gem "rdoc", "~> 3.12" - gem "bundler", "~> 1.0.0" - gem "jeweler", "~> 1.8.3" - gem "rcov", ">= 0" + gem "rspec", "~> 2.10" + gem "rdoc", ">= 3.12" + gem "bundler", "~> 1.1.0" + gem "jeweler", ">= 1.8.3" + gem "simplecov",">= 0.5" end + +group :test do + # gem 'bson_ext' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..80f02c8 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,59 @@ +GEM + remote: http://rubygems.org/ + specs: + activemodel (3.2.6) + activesupport (= 3.2.6) + builder (~> 3.0.0) + activesupport (3.2.6) + i18n (~> 0.6) + multi_json (~> 1.0) + builder (3.0.0) + diff-lcs (1.1.3) + git (1.2.5) + i18n (0.6.0) + jeweler (1.8.3) + bundler (~> 1.0) + git (>= 1.2.5) + rake + rdoc + json (1.7.3) + money (5.0.0) + i18n (~> 0.4) + json + mongoid (3.0.0.rc) + activemodel (~> 3.1) + moped (~> 1.0.0.rc) + origin (~> 1.0.0.rc) + tzinfo (~> 0.3.22) + moped (1.0.0.rc) + multi_json (1.3.6) + origin (1.0.1) + rake (0.9.2.2) + rdoc (3.12) + json (~> 1.4) + rspec (2.10.0) + rspec-core (~> 2.10.0) + rspec-expectations (~> 2.10.0) + rspec-mocks (~> 2.10.0) + rspec-core (2.10.1) + rspec-expectations (2.10.0) + diff-lcs (~> 1.1.3) + rspec-mocks (2.10.1) + simplecov (0.6.4) + multi_json (~> 1.0) + simplecov-html (~> 0.5.3) + simplecov-html (0.5.3) + tzinfo (0.3.33) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.1.0) + jeweler (>= 1.8.3) + money + mongoid (~> 3.0.0.rc) + moped + rdoc (>= 3.12) + rspec (~> 2.10) + simplecov (>= 0.5) diff --git a/README.rdoc b/README.md similarity index 68% rename from README.rdoc rename to README.md index c9da490..017d82b 100644 --- a/README.rdoc +++ b/README.md @@ -1,8 +1,28 @@ -= money-mongoid +# Money for mongoid -Description goes here. +Use the [money](https://github.com/RubyMoney/money) gem with Mongoid 2.x and 3.x. -== Contributing to money-mongoid +## Install + +in Gemfile + +```ruby +gem 'money-mongoid' +``` + +Bundle via Terminal: + +`$ bundle` + +## Usage + +```ruby +require 'money-mongoid' +`` + +See specs for usage examples, fx `money/mongoid/3x/money_spec.rb` + +## Contributing to money-mongoid * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. diff --git a/Rakefile b/Rakefile index 73c74fb..ff4fb75 100644 --- a/Rakefile +++ b/Rakefile @@ -17,8 +17,8 @@ Jeweler::Tasks.new do |gem| gem.name = "money-mongoid" gem.homepage = "http://github.com/kristianmandrup/money-mongoid" gem.license = "MIT" - gem.summary = %Q{TODO: one-line summary of your gem} - gem.description = %Q{TODO: longer description of your gem} + gem.summary = %Q{Mongoid support and integration for the money gem} + gem.description = %Q{Makes it easy to use money with mongoid} gem.email = "kmandrup@gmail.com" gem.authors = ["Kristian Mandrup"] # dependencies defined in Gemfile diff --git a/lib/money-mongoid.rb b/lib/money-mongoid.rb index e69de29..905ded7 100644 --- a/lib/money-mongoid.rb +++ b/lib/money-mongoid.rb @@ -0,0 +1 @@ +require 'money/mongoid' \ No newline at end of file diff --git a/lib/money/mongoid.rb b/lib/money/mongoid.rb new file mode 100644 index 0000000..8ede94b --- /dev/null +++ b/lib/money/mongoid.rb @@ -0,0 +1,16 @@ +require 'money/mongoid/core_ext' +require 'money/orm/generic' + +if Mongoid::VERSION > '3' + require "money/mongoid/3x/money" +else + require "money/mongoid/2x/money" +end + +module Mongoid + module Money + class << self + attr_accessor :default_polymorphic_money + end + end +end diff --git a/lib/money/mongoid/2x/money.rb b/lib/money/mongoid/2x/money.rb new file mode 100644 index 0000000..0ec5728 --- /dev/null +++ b/lib/money/mongoid/2x/money.rb @@ -0,0 +1,22 @@ +# extend Mongoid::Fields in Mongoid document (model) that wants to use Money +# this makes Money accessible as a type + +module Mongoid + module Fields + class Money + include Mongoid::Fields::Serializable + + def self.instantiate(name, options = {}) + super + end + + def deserialize(value) + ::Money.new value[:cents], value[:currency_iso] + end + + def serialize(value) + {:cents => value.cents, :currency_iso => value.currency.iso_code} + end + end + end +end diff --git a/lib/money/mongoid/3x/money.rb b/lib/money/mongoid/3x/money.rb new file mode 100644 index 0000000..77f8a29 --- /dev/null +++ b/lib/money/mongoid/3x/money.rb @@ -0,0 +1,38 @@ +module Mongoize + extend ActiveSupport::Concern + # See http://mongoid.org/en/mongoid/docs/upgrading.html + + def mongoize + {'cents' => cents, 'currency_iso' => currency.iso_code} + end + + module ClassMethods + def demongoize(value) + ::Money.new get_cents(value), get_currency(value) + end + + def evolve(object) + object.__evolve_to_money__.mongoize + end + + private + + def get_cents value + value[:cents] || value['cents'] + end + + def get_currency value + value[:currency_iso] || value['currency_iso'] + end + end +end + + + +class Money + include Mongoize + + def __evolve_to_money__ + self + end +end \ No newline at end of file diff --git a/lib/money/mongoid/core_ext.rb b/lib/money/mongoid/core_ext.rb new file mode 100644 index 0000000..2f5c23a --- /dev/null +++ b/lib/money/mongoid/core_ext.rb @@ -0,0 +1,2 @@ +require 'money/mongoid/core_ext/string' +require 'money/mongoid/core_ext/array' diff --git a/lib/money/mongoid/core_ext/array.rb b/lib/money/mongoid/core_ext/array.rb new file mode 100644 index 0000000..186b79e --- /dev/null +++ b/lib/money/mongoid/core_ext/array.rb @@ -0,0 +1,5 @@ +class Array + def exchange_to! currency + self.map!{|m| m.exchange_to!(currency) } + end +end \ No newline at end of file diff --git a/lib/money/mongoid/core_ext/string.rb b/lib/money/mongoid/core_ext/string.rb new file mode 100644 index 0000000..9ef38f7 --- /dev/null +++ b/lib/money/mongoid/core_ext/string.rb @@ -0,0 +1,6 @@ +# encoding: utf-8 +class String + def __evolve_to_money__ + Money.parse(self) + end +end diff --git a/lib/money/mongoid/macros.rb b/lib/money/mongoid/macros.rb new file mode 100644 index 0000000..a7803e3 --- /dev/null +++ b/lib/money/mongoid/macros.rb @@ -0,0 +1,29 @@ +module Mongoid + module Money + module Macros + def self.included(base) + base.send :include, ClassMethods + end + + module ClassMethods + def monetize + self.send :include, "Mongoid::Monetize".constantize + end + + def monetizable options = {} + self.send :include, "Mongoid::Monetizable".constantize + embedded_in options[:as], :polymorphic => true if options[:as] + end + end + end + end +end + +module Mongoid + module Document + module ClassMethods + include Money::Mongoid::Macros + end + end +end + diff --git a/lib/money/mongoid/monetizable.rb b/lib/money/mongoid/monetizable.rb new file mode 100644 index 0000000..f6e1a37 --- /dev/null +++ b/lib/money/mongoid/monetizable.rb @@ -0,0 +1,17 @@ +# Thanks to: https://gist.github.com/840500 + +module Mongoid + module Monetizable + include Money::Orm::Generic + + def self.included(base) + base.class_eval do + field :cents, :type => Integer, :default => 0 + field :currency_iso, :type => String, :default => ::Money.default_currency.iso_code + + validates_numericality_of :cents + end + base.extend Money::Orm::Generic::ClassMethods + end + end +end diff --git a/lib/money/mongoid/monetize.rb b/lib/money/mongoid/monetize.rb new file mode 100644 index 0000000..4d67760 --- /dev/null +++ b/lib/money/mongoid/monetize.rb @@ -0,0 +1,35 @@ +module Mongoid + module Monetize + def self.included(base) + base.extend ClassMethods + base.extend Money::Orm::Generic::ClassMethods + end + + module ClassMethods + def monetize_one name, options = {:as => :priced} + as_poly = options.delete(:as) || default_polymorphic_money + + unless as_poly + raise ArgumentError, "You must set an :as option indicating the polymorphic Monetizable model. See money-rails gem." + end + + self.embeds_one name.to_sym, options.merge(:as => as_poly) + end + alias_method :monetize, :monetize_one + + def monetize_many name, options = {:as => :priced} + as_poly = options.delete(:as) || default_polymorphic_money + unless as_poly + raise ArgumentError, "You must set an :as option indicating the polymorphic Monetizable model. See money-rails gem." + end + self.embeds_many name, options.merge(:as => as_poly) + end + + protected + + def default_polymorphic_money + Mongoid::Money.default_polymorphic_money + end + end # module + end +end diff --git a/lib/money/orm/generic.rb b/lib/money/orm/generic.rb new file mode 100644 index 0000000..c4a0f12 --- /dev/null +++ b/lib/money/orm/generic.rb @@ -0,0 +1,24 @@ +class Money + module Orm + module Generic + # Virtual price / currency attributes + module ClassMethods + def monetize_for *names + options = names.last.kind_of?(Hash) ? names.delete(names.last) : {:as => :priced} + names.each {|name| monetize name, options } + end + end + + def price + ::Money.new(self.price_pence, currency) + end + def price=(price) + self.price_pence = price.cents + end + + def currency + ::Money::Currency.new(currency_iso) + end + end + end +end \ No newline at end of file diff --git a/spec/money-mongoid_spec.rb b/spec/money-mongoid_spec.rb deleted file mode 100644 index 415b8ab..0000000 --- a/spec/money-mongoid_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') - -describe "MoneyMongoid" do - it "fails" do - fail "hey buddy, you should probably rename this file and start specing for real" - end -end diff --git a/spec/money/mongoid/2x/money_spec.rb b/spec/money/mongoid/2x/money_spec.rb new file mode 100644 index 0000000..018034a --- /dev/null +++ b/spec/money/mongoid/2x/money_spec.rb @@ -0,0 +1,14 @@ +require 'money/mongoid/spec_helper' +require 'money/mongoid/money' +require 'money/mongoid/2x/product' + +describe 'Mongoid custom Money type' do + subject { product } + + let(:product) do { Product.create :price => Money.new(3000, 'USD') } + + its(:price) { should be_a Money } + + specify { subject.price.cents.should == 3000 } + specify { subject.price.currency.iso_code.should == 'USD' } +end diff --git a/spec/money/mongoid/2x/product.rb b/spec/money/mongoid/2x/product.rb new file mode 100644 index 0000000..2d08435 --- /dev/null +++ b/spec/money/mongoid/2x/product.rb @@ -0,0 +1,6 @@ +class Product + include Mongoid::Document + extend Mongoid::Fields # to make Money available! + + field :price, :type => Money +end diff --git a/spec/money/mongoid/3x/money_spec.rb b/spec/money/mongoid/3x/money_spec.rb new file mode 100644 index 0000000..ccc7c3d --- /dev/null +++ b/spec/money/mongoid/3x/money_spec.rb @@ -0,0 +1,49 @@ +require 'money/mongoid/spec_helper' +require 'money/mongoid' +require 'money/mongoid/3x/product' + +describe 'Mongoid custom Money type' do + subject { product } + + let(:product) { Product.create(:price => Money.new(3000, 'USD')) } + let(:products) do + end + + its(:price) { should be_a Money } + + specify { subject.price.cents.should == 3000 } + specify { subject.price.currency.iso_code.should == 'USD' } + + it "should be searchable by price as money" do + Product.create(:price => Money.new(3000, 'USD')) + Product.where(price: Money.new(3000, 'USD')).count.should == 1 + end + + it "should be searchable by price as string" do + product + Product.where(price: "USD 30.00").count.should == 1 + end + + it "should be searchable by price using gte and a money value of same currency" do + product + Product.where(:price.gte => Money.new(2000, 'USD')).count.should == 1 + end + + it "should be searchable by price using gte and a money value of different currency" do + Money.add_rate("USD","EUR", 0.5) + Money.add_rate("EUR","USD", 2) + + 6.times do |n| + Product.create :price => Money.new(n * 500, 'USD') + end + + # So far only works by explicit conversion before search. Any solution? + Product.all.exchange_to! 'EUR' + + # puts Product.all.map{|p| p.price.inspect } + + # with our rates 20 EUR -> 40 USD + search_res = Product.where :price.gte => Money.new(2000, 'EUR') + search_res.count.should == 0 + end +end diff --git a/spec/money/mongoid/3x/product.rb b/spec/money/mongoid/3x/product.rb new file mode 100644 index 0000000..f0ac78b --- /dev/null +++ b/spec/money/mongoid/3x/product.rb @@ -0,0 +1,10 @@ +class Product + include Mongoid::Document + + field :price, :type => Money + + def exchange_to! currency_iso + self.price = self.price.exchange_to(currency_iso) + save + end +end diff --git a/spec/money/mongoid/models/account.rb b/spec/money/mongoid/models/account.rb new file mode 100644 index 0000000..feed17f --- /dev/null +++ b/spec/money/mongoid/models/account.rb @@ -0,0 +1,12 @@ +class Account + include Mongoid::Document + include Mongoid::Monetize + + monetize_for :utility, :services + + embeds_one :deposit, :as => :priced + monetize_one :rental_price + + monetize :rent + monetize_many :costs, :class_name => 'Price' +end diff --git a/spec/money/mongoid/models/price.rb b/spec/money/mongoid/models/price.rb new file mode 100644 index 0000000..1e85c3d --- /dev/null +++ b/spec/money/mongoid/models/price.rb @@ -0,0 +1,15 @@ +class Price + include Mongoid::Document + + monetizable_orm :mongoid, :as => :priced + + # embedded_in :priced, :polymorphic => true + + def self.for amount, currency = nil + currency = ::Money::Currency.new(currency || ::Money.default_currency) + money = Money.new(amount, currency) + + # see 'kristianmandrup' fork of money (money/macros) + Money::Macros.price_class.new :price => money + end +end diff --git a/spec/money/mongoid/monetize_spec.rb b/spec/money/mongoid/monetize_spec.rb new file mode 100644 index 0000000..bf55d0f --- /dev/null +++ b/spec/money/mongoid/monetize_spec.rb @@ -0,0 +1,87 @@ +require 'money/mongoid/spec_helper' + +describe Money::Mongoid::Monetizable do + before do + Money.default_currency = 'USD' + end + + describe "monetize" do + subject { account } + + let(:account) do + Account.create :rental_price => Price.for(3000, :usd), + :deposit => Price.for(150, :usd), + :rent => Price.for(100), + :utility => priced_at(200), + :services => priced_at(150) + end + + + it_behaves_like 'a generic account' + + context 'can add multiple costs' do + before do + account.costs << Price.for(100, :usd) + account.costs << Price.for(200) + end + + its(:costs) { should have(2).items } + end + + describe 'money macros' do + subject { account_2 } + + let(:account_2) do + Account.create :rental_price => priced_at(3000, :usd), + :deposit => priced_at(150, :usd), + :rent => priced_at(100) + end + + it_behaves_like 'a generic account' + end + + describe 'customize macro map' do + context 'set cost_class to Price' do + subject { cost_account } + + before do + MoneyRails::Moneys.cost_class = Price + end + + let(:cost_account) do + Account.create :rental_price => costing(3000, :usd) + end + + it "rental is money" do + subject.rental_price.price.should == Money.new(3000) + end + + it "price compare < Numeric" do + subject.rental_price.price.should < 3100 + end + + it "price compare == Numeric" do + subject.rental_price.price.should be_eql(3000) + end + + it "price compare > Numeric" do + subject.rental_price.price.should_not > 3001 + end + end + + context 'set all money classes to Price' do + subject { valued_at(4000) } + + before do + MoneyRails::Moneys.classes = Price + end + + specify { MoneyRails::Moneys.value_class.should == Price } + + specify { subject.should be_a Price } + specify { subject.price.should be_a Money } + specify { subject.price.should == Money.new(4000) } + end + end + end +end diff --git a/spec/money/mongoid/spec_helper.rb b/spec/money/mongoid/spec_helper.rb new file mode 100644 index 0000000..0916bb9 --- /dev/null +++ b/spec/money/mongoid/spec_helper.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'mongoid' +require 'money' + +require 'money/mongoid/version_setup' + +Mongoid.configure do |config| + Mongoid::VersionSetup.configure config +end + +if RUBY_VERSION >= '1.9.2' + YAML::ENGINE.yamler = 'syck' +end + +RSpec.configure do |config| + # config.mock_with(:mocha) + + config.before(:each) do + Mongoid.purge! + # Mongoid.database.collections.each do |collection| + # unless collection.name =~ /^system\./ + # collection.remove + # end + # end + end +end + +# require 'money/mongoid/models/price' +# require 'money/mongoid/models/account' + +# require 'money/shared/generic_account_ex' diff --git a/spec/money/mongoid/support/mongoid.yml b/spec/money/mongoid/support/mongoid.yml new file mode 100644 index 0000000..c05697f --- /dev/null +++ b/spec/money/mongoid/support/mongoid.yml @@ -0,0 +1,19 @@ +test: + database: money_rails_test + host: localhost + port: 27017 + # slaves: + # - host: localhost + # port: 27018 + # - host: localhost + # port: 27019 + # allow_dynamic_fields: false + # include_root_in_json: true + # parameterize_keys: false + # persist_in_safe_mode: false + # raise_not_found_error: false + # reconnect_time: 5 + # autocreate_indexes: false + # persist_types: false + # option_no_exist: false + # skip_version_check: false diff --git a/spec/money/mongoid/version_setup.rb b/spec/money/mongoid/version_setup.rb new file mode 100644 index 0000000..087e571 --- /dev/null +++ b/spec/money/mongoid/version_setup.rb @@ -0,0 +1,37 @@ +module Mongoid + module VersionSetup + def self.configure config + version = Mongoid::VERSION + case + when version < '2' + raise ArgumentError, "Mongoid versions < 2 not supported" + when version < '3' + version_2 config + when version > '3' + version_3 config + end + end + + def self.version_3 config + require 'moped' + config.connect_to('money_mongoid_test') + end + + def self.version_2 config + require 'bson' + + opts = YAML.load(File.read(File.dirname(__FILE__) + '/support/mongoid.yml'))["test"] + opts.delete("slaves") # no slaves support for version < 3 + name = opts.delete("database") + host = opts.delete("host") + port = opts.delete("port") + config.master = begin + Mongo::Connection.new(host, port, opts).db(name) + rescue Mongo::ConnectionFailure + Mongo::Connection.new(host, '27017', opts).db(name) + end + config.logger = nil + config.allow_dynamic_fields = true + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ddc9fc9..3d0c9a3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,4 @@ -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -$LOAD_PATH.unshift(File.dirname(__FILE__)) require 'rspec' -require 'money-mongoid' -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} +SPEC_DIR = File.dirname(__FILE__) -RSpec.configure do |config| - -end