danconia 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f379e57876333600210d3f3669e1a45654b8016238abd82df38eaf6674084c5c
4
- data.tar.gz: b85beca8d071d199c13771c14916efc26aac999df2d0dc7bb167597fb5822f29
3
+ metadata.gz: 38d88815b5c3c9155db3c64433d6469614656a2464ab6a0c0eae51b3429b084d
4
+ data.tar.gz: d8962d50c98a68e8acfbc2a1424e81b77b1d506d069d4e87c0fa32803482d2b0
5
5
  SHA512:
6
- metadata.gz: 5712b367efd1638949888f2e1fe254a174cd63c23d7e4d14ee8f9601380f4b0cf376b572642fad40d93b4d1d7b8333de2788eff5042fe29e79c36bb9d444315a
7
- data.tar.gz: b55eab153bed5d5572789b7ed4b7a3cbb6ef8907269a5babfdebe12c1947152baac4c5bde5fc0b4bada904323c6524ceafe0d7991aa64262123362ee22209764
6
+ metadata.gz: 69fa03e8ae6c01a37eaafaf9fe358b1e57eca34bd4e1113fb795002b80d4f5dc2a3934c06f1b3102a3c64e3403e16f3d6fd97c9f301b819f9de6b742c3ea088d
7
+ data.tar.gz: 4881c55540c4b0e51e6018a23aae7923effc31ea9694295aae07a08a62fa60b32d6943adea8bd3798d5349b6f15c97ab2948f6410593c6d03ceb35c136e12c9b
@@ -0,0 +1 @@
1
+ language: ruby
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- danconia (0.2.1)
4
+ danconia (0.2.2)
5
5
  activerecord (>= 3.0.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  TODO: Write a gem description
4
4
 
5
+ [![Build Status](https://travis-ci.org/eeng/danconia.svg?branch=master)](https://travis-ci.org/eeng/danconia)
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -0,0 +1,21 @@
1
+ require 'danconia'
2
+
3
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
4
+
5
+ ActiveRecord::Schema.define do
6
+ create_table :exchange_rates do |t|
7
+ t.string :pair, limit: 6
8
+ t.decimal :rate, precision: 12, scale: 6
9
+ t.index :pair, unique: true
10
+ end
11
+ end
12
+
13
+ Danconia.configure do |config|
14
+ config.default_exchange = Danconia::Exchanges::CurrencyLayer.new(access_key: ENV['ACCESS_KEY'], store: Danconia::Stores::ActiveRecord.new)
15
+ end
16
+
17
+ # Periodically do this
18
+ puts 'Updating dates with CurrencyLayer API...'
19
+ Danconia.config.default_exchange.update_rates!
20
+
21
+ puts Money(1, 'USD').exchange_to('EUR').inspect # => 0.854896 EUR
@@ -5,4 +5,4 @@ Danconia.configure do |config|
5
5
  config.default_exchange = Danconia::Exchanges::FixedRates.new(rates: {'USDARS' => 27.5, 'USDEUR' => 0.86})
6
6
  end
7
7
 
8
- puts Money(10, 'ARS').exchange_to('EUR').inspect
8
+ puts Money(10, 'ARS').exchange_to('EUR').inspect # => 0.31273 EUR
@@ -4,4 +4,8 @@ require 'danconia/currency'
4
4
  require 'danconia/money'
5
5
  require 'danconia/kernel'
6
6
  require 'danconia/integrations/active_record'
7
+ require 'danconia/exchanges/exchange'
7
8
  require 'danconia/exchanges/fixed_rates'
9
+ require 'danconia/exchanges/currency_layer'
10
+ require 'danconia/stores/in_memory'
11
+ require 'danconia/stores/active_record'
@@ -1,5 +1,3 @@
1
- require 'danconia/exchange'
2
-
3
1
  module Danconia
4
2
  class << self
5
3
  def config
@@ -2,7 +2,7 @@ module Danconia
2
2
  class Currency < Struct.new(:code, :symbol, :description, keyword_init: true)
3
3
  def self.find code, exchange
4
4
  return code if code.is_a? Currency
5
- new exchange.available_currencies.find { |c| c[:code] == code } || {code: code, symbol: '$'}
5
+ new exchange.currencies.find { |c| c[:code] == code } || {code: code, symbol: '$'}
6
6
  end
7
7
  end
8
8
  end
@@ -5,15 +5,11 @@ require 'json'
5
5
  module Danconia
6
6
  module Exchanges
7
7
  class CurrencyLayer < Exchange
8
- def initialize access_key:
9
- super()
8
+ def initialize access_key:, **args
9
+ super args
10
10
  @access_key = access_key
11
11
  end
12
12
 
13
- def update_rates!
14
- @store.save_rates fetch_rates
15
- end
16
-
17
13
  def fetch_rates
18
14
  response = JSON.parse Net::HTTP.get URI "http://www.apilayer.net/api/live?access_key=#{@access_key}"
19
15
  if response['success']
@@ -0,0 +1,32 @@
1
+ module Danconia
2
+ module Exchanges
3
+ class Exchange
4
+ attr_reader :store, :currencies
5
+
6
+ def initialize store: Stores::InMemory.new, currencies: []
7
+ @store = store
8
+ @currencies = currencies
9
+ end
10
+
11
+ def rate from, to
12
+ if from == 'USD' and direct_rate = @store.direct_rate(from, to)
13
+ direct_rate
14
+ elsif to == 'USD' and inverse_rate = @store.direct_rate(to, from)
15
+ (1.0 / inverse_rate).round 6
16
+ elsif from != 'USD' and to != 'USD' and from_in_usd = rate(from, 'USD') and to_per_usd = rate('USD', to)
17
+ (from_in_usd * to_per_usd).round 6
18
+ else
19
+ raise Errors::ExchangeRateNotFound.new(from, to)
20
+ end
21
+ end
22
+
23
+ def rates
24
+ @store.rates
25
+ end
26
+
27
+ def update_rates!
28
+ @store.save_rates fetch_rates
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,9 +1,14 @@
1
1
  module Danconia
2
2
  module Exchanges
3
3
  class FixedRates < Exchange
4
- def initialize rates: {}
5
- super()
6
- @store.save_rates rates
4
+ def initialize rates: {}, **args
5
+ super args
6
+ @rates = rates
7
+ update_rates!
8
+ end
9
+
10
+ def fetch_rates
11
+ @rates
7
12
  end
8
13
  end
9
14
  end
@@ -8,17 +8,21 @@ module Danconia
8
8
 
9
9
  def initialize amount, currency_code = nil, decimals: 2, exchange: Danconia.config.default_exchange
10
10
  @decimals = decimals
11
- @amount = parse(amount)
11
+ @amount = parse amount
12
12
  @currency = Currency.find(currency_code || Danconia.config.default_currency, exchange)
13
13
  @exchange = exchange
14
14
  end
15
15
 
16
- def to_s
16
+ def format decimals: @decimals
17
17
  ActiveSupport::NumberHelper.number_to_currency amount, precision: decimals, unit: currency.symbol
18
18
  end
19
19
 
20
+ def to_s
21
+ format
22
+ end
23
+
20
24
  def inspect
21
- "#<#{self.class.name} #{amount} #{currency.code}>"
25
+ "#{amount} #{currency.code}"
22
26
  end
23
27
 
24
28
  def == other
@@ -43,11 +47,17 @@ module Danconia
43
47
  end
44
48
 
45
49
  def exchange_to other_currency
46
- other_currency = Currency.find(other_currency, exchange)
47
- rate = exchange_rate(currency, other_currency)
50
+ other_currency = other_currency.presence && Currency.find(other_currency, exchange) || currency
51
+ rate = exchange_rate_to(other_currency.code)
48
52
  clone_with amount * rate, other_currency
49
53
  end
50
54
 
55
+ def exchange_rate_to to
56
+ from = currency.code
57
+ return 1 if from == to
58
+ exchange.rate from, to
59
+ end
60
+
51
61
  %w(+ - * /).each do |op|
52
62
  class_eval <<-EOR, __FILE__, __LINE__ + 1
53
63
  def #{op} other
@@ -65,6 +75,10 @@ module Danconia
65
75
  (self * 100).round
66
76
  end
67
77
 
78
+ def default_currency?
79
+ currency.code == Danconia.config.default_currency
80
+ end
81
+
68
82
  def method_missing method, *args
69
83
  if @amount.respond_to? method
70
84
  @amount.send method, *args
@@ -80,16 +94,11 @@ module Danconia
80
94
  private
81
95
 
82
96
  def parse object
83
- BigDecimal(object.to_s) rescue BigDecimal('0')
97
+ BigDecimal(object.to_s) rescue BigDecimal(0)
84
98
  end
85
99
 
86
100
  def clone_with amount, currency = @currency
87
101
  Money.new amount, currency, decimals: decimals, exchange: exchange
88
102
  end
89
-
90
- def exchange_rate from, to
91
- return 1 if from == to
92
- exchange.rate from.code, to.code
93
- end
94
103
  end
95
104
  end
@@ -0,0 +1,24 @@
1
+ module Danconia
2
+ module Stores
3
+ class ActiveRecord
4
+ def save_rates rates
5
+ ExchangeRate.transaction do
6
+ rates.each do |pair, rate|
7
+ ExchangeRate.where(pair: pair).first_or_initialize.update rate: rate
8
+ end
9
+ end
10
+ end
11
+
12
+ def direct_rate from, to
13
+ ExchangeRate.find_by(pair: [from, to].join)&.rate
14
+ end
15
+
16
+ def rates
17
+ ExchangeRate.all
18
+ end
19
+ end
20
+
21
+ class ExchangeRate < ::ActiveRecord::Base
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module Danconia
2
+ module TestHelpers
3
+ class << self
4
+ def with_config &block
5
+ old_config = Danconia.config.dup
6
+ Danconia.configure &block
7
+ Danconia.config = old_config
8
+ end
9
+
10
+ def with_rates rates
11
+ with_config do |config|
12
+ config.default_exchange = Exchanges::FixedRates.new rates: rates, currencies: config.default_exchange.currencies
13
+ yield
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module Danconia
2
- VERSION = '0.2.1'
2
+ VERSION = '0.2.2'
3
3
  end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'danconia/exchanges/currency_layer'
3
2
 
4
3
  module Danconia
5
4
  module Exchanges
@@ -9,11 +9,12 @@ module Danconia
9
9
  end
10
10
 
11
11
  it 'non numeric values are treated as zero' do
12
+ expect(Money(nil)).to eq Money(0)
12
13
  expect(Money('a')).to eq Money(0)
13
14
  end
14
15
 
15
16
  it 'should use the default currency if not specified' do
16
- with_config do |config|
17
+ TestHelpers.with_config do |config|
17
18
  config.default_currency = 'ARS'
18
19
  expect(Money(0).currency.code).to eq 'ARS'
19
20
 
@@ -71,9 +72,7 @@ module Danconia
71
72
  end
72
73
 
73
74
  it 'should exchange to the source currency if they differ' do
74
- with_config do |config|
75
- config.default_exchange = fake_exchange(rate: 4)
76
-
75
+ TestHelpers.with_rates 'USDARS' => 4 do |config|
77
76
  expect(Money(3, 'ARS') < Money(1, 'USD')).to be true
78
77
  expect(Money(4, 'ARS') < Money(1, 'USD')).to be false
79
78
  end
@@ -86,12 +85,18 @@ module Danconia
86
85
  end
87
86
  end
88
87
 
88
+ context 'format' do
89
+ it 'allow to override the number of decimals' do
90
+ expect(Money(3.561, decimals: 3).format(decimals: 1)).to eq '$3.6'
91
+ end
92
+ end
93
+
89
94
  context 'to_s' do
90
95
  it 'should add the currency symbol' do
91
96
  expect(Money(3.25).to_s).to eq '$3.25'
92
97
 
93
- with_config do |config|
94
- config.default_exchange = double 'exchange', available_currencies: [{code: 'EUR', symbol: '€'}, {code: 'JPY', symbol: '¥'}]
98
+ TestHelpers.with_config do |config|
99
+ config.default_exchange = Exchanges::FixedRates.new currencies: [{code: 'EUR', symbol: '€'}, {code: 'JPY', symbol: '¥'}]
95
100
 
96
101
  expect(Money(1, 'EUR').to_s).to eq '€1.00'
97
102
  expect(Money(1, 'JPY').to_s).to eq '¥1.00'
@@ -103,15 +108,11 @@ module Danconia
103
108
  expect(Money(3.256, decimals: 2).to_s).to eq '$3.26'
104
109
  expect(Money(3.2517, decimals: 3).to_s).to eq '$3.252'
105
110
  end
106
-
107
- it 'nil should be like zero' do
108
- expect(Money(nil).to_s).to eq '$0.00'
109
- end
110
111
  end
111
112
 
112
113
  context '.inspect' do
113
114
  it 'should display the object internals' do
114
- expect(Money(10.25, 'ARS', decimals: 3).inspect).to eq '#<Danconia::Money 10.25 ARS>'
115
+ expect(Money(10.25, 'ARS', decimals: 3).inspect).to eq '10.25 ARS'
115
116
  end
116
117
  end
117
118
 
@@ -121,8 +122,7 @@ module Danconia
121
122
  end
122
123
 
123
124
  it 'should use the default exchange if not set' do
124
- with_config do |config|
125
- config.default_exchange = Exchanges::FixedRates.new(rates: {'USDEUR' => 3, 'USDARS' => 4})
125
+ TestHelpers.with_rates 'USDEUR' => 3, 'USDARS' => 4 do
126
126
  expect(Money(2, 'USD').exchange_to('EUR')).to eq Money(6, 'EUR')
127
127
  expect(Money(2, 'USD').exchange_to('ARS')).to eq Money(8, 'ARS')
128
128
  end
@@ -143,6 +143,21 @@ module Danconia
143
143
  expect(m2.decimals).to eq 0
144
144
  expect(m1).to eq Money(1, 'USD')
145
145
  end
146
+
147
+ it 'if the destination currency is blank should not do the conversion' do
148
+ expect(Money(1, 'USD').exchange_to('')).to eq Money(1, 'USD')
149
+ expect(Money(1, 'ARS').exchange_to('')).to eq Money(1, 'ARS')
150
+ end
151
+ end
152
+
153
+ context 'default_currency?' do
154
+ it 'is true if the currency is the configured default' do
155
+ TestHelpers.with_config do |config|
156
+ config.default_currency = 'ARS'
157
+ expect(Money(1, 'ARS')).to be_default_currency
158
+ expect(Money(1, 'USD')).to_not be_default_currency
159
+ end
160
+ end
146
161
  end
147
162
 
148
163
  context 'delegation' do
@@ -153,7 +168,7 @@ module Danconia
153
168
  end
154
169
 
155
170
  def fake_exchange args = {}
156
- double 'exchange', args.reverse_merge(rate: nil, available_currencies: [])
171
+ double 'exchange', args.reverse_merge(rate: nil, currencies: [])
157
172
  end
158
173
  end
159
174
  end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ module Danconia
4
+ module Stores
5
+ describe ActiveRecord, active_record: true do
6
+ context 'save_rates' do
7
+ it 'should create or update the rates' do
8
+ ExchangeRate.create! pair: 'USDEUR', rate: 2
9
+ expect { subject.save_rates 'USDEUR' => 3, 'USDARS' => 4 }.to change { ExchangeRate.count }.by 1
10
+ expect(subject.rates.map { |e| [e.pair, e.rate] }).to eq({'USDEUR' => 3, 'USDARS' => 4}.to_a)
11
+ end
12
+ end
13
+
14
+ context '#direct_rate' do
15
+ it 'should find the rate for the pair' do
16
+ ExchangeRate.create! pair: 'USDEUR', rate: 2
17
+ expect(subject.direct_rate('USD', 'EUR')).to eq 2
18
+ expect(subject.direct_rate('USD', 'ARS')).to eq nil
19
+ end
20
+ end
21
+
22
+ before do
23
+ ::ActiveRecord::Schema.define do
24
+ create_table :exchange_rates do |t|
25
+ t.string :pair, limit: 6
26
+ t.decimal :rate, precision: 12, scale: 6
27
+ t.index :pair, unique: true
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,10 +1,10 @@
1
1
  require 'danconia'
2
+ require 'danconia/test_helpers'
2
3
  require 'webmock/rspec'
3
4
 
4
5
  Dir["#{__dir__}/support/*.rb"].each { |f| require f }
5
6
 
6
7
  RSpec.configure do |config|
7
8
  config.filter_run_when_matching :focus
8
- config.include Helpers
9
9
  config.include DatabaseCleaner
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: danconia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emmanuel Nicolau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-22 00:00:00.000000000 Z
11
+ date: 2018-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -118,6 +118,7 @@ files:
118
118
  - ".gitignore"
119
119
  - ".rspec"
120
120
  - ".ruby-version"
121
+ - ".travis.yml"
121
122
  - Gemfile
122
123
  - Gemfile.lock
123
124
  - Guardfile
@@ -125,27 +126,30 @@ files:
125
126
  - README.md
126
127
  - Rakefile
127
128
  - danconia.gemspec
128
- - examples/basic.rb
129
+ - examples/currency_layer.rb
130
+ - examples/fixed_rates.rb
129
131
  - lib/danconia.rb
130
132
  - lib/danconia/config.rb
131
133
  - lib/danconia/currency.rb
132
134
  - lib/danconia/errors/api_error.rb
133
135
  - lib/danconia/errors/exchange_rate_not_found.rb
134
- - lib/danconia/exchange.rb
135
136
  - lib/danconia/exchanges/currency_layer.rb
137
+ - lib/danconia/exchanges/exchange.rb
136
138
  - lib/danconia/exchanges/fixed_rates.rb
137
139
  - lib/danconia/integrations/active_record.rb
138
140
  - lib/danconia/kernel.rb
139
141
  - lib/danconia/money.rb
142
+ - lib/danconia/stores/active_record.rb
140
143
  - lib/danconia/stores/in_memory.rb
144
+ - lib/danconia/test_helpers.rb
141
145
  - lib/danconia/version.rb
142
146
  - spec/danconia/exchanges/currency_layer_spec.rb
143
147
  - spec/danconia/exchanges/fixed_rates_spec.rb
144
148
  - spec/danconia/integrations/active_record_spec.rb
145
149
  - spec/danconia/money_spec.rb
150
+ - spec/danconia/stores/active_record_spec.rb
146
151
  - spec/spec_helper.rb
147
152
  - spec/support/database.rb
148
- - spec/support/helpers.rb
149
153
  homepage: ''
150
154
  licenses: []
151
155
  metadata: {}
@@ -174,6 +178,6 @@ test_files:
174
178
  - spec/danconia/exchanges/fixed_rates_spec.rb
175
179
  - spec/danconia/integrations/active_record_spec.rb
176
180
  - spec/danconia/money_spec.rb
181
+ - spec/danconia/stores/active_record_spec.rb
177
182
  - spec/spec_helper.rb
178
183
  - spec/support/database.rb
179
- - spec/support/helpers.rb
@@ -1,32 +0,0 @@
1
- require 'danconia/stores/in_memory'
2
-
3
- module Danconia
4
- class Exchange
5
- attr_reader :store
6
-
7
- def initialize store: Stores::InMemory.new
8
- @store = store
9
- end
10
-
11
- def rate from, to
12
- if from == 'USD' and direct_rate = @store.direct_rate(from, to)
13
- direct_rate
14
- elsif to == 'USD' and inverse_rate = @store.direct_rate(to, from)
15
- (1.0 / inverse_rate).round 6
16
- elsif from != 'USD' and to != 'USD' and from_in_usd = rate(from, 'USD') and to_per_usd = rate('USD', to)
17
- (from_in_usd * to_per_usd).round 6
18
- else
19
- raise Errors::ExchangeRateNotFound.new(from, to)
20
- end
21
- end
22
-
23
- def rates
24
- @store.rates
25
- end
26
-
27
- # Currently is only needed for money formatting
28
- def available_currencies
29
- []
30
- end
31
- end
32
- end
@@ -1,7 +0,0 @@
1
- module Helpers
2
- def with_config &block
3
- old_config = Danconia.config.dup
4
- Danconia.configure &block
5
- Danconia.config = old_config
6
- end
7
- end