danconia 0.2.8 → 0.4.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +104 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +1 -1
  7. data/Gemfile.lock +63 -43
  8. data/README.md +17 -3
  9. data/bin/console +7 -0
  10. data/danconia.gemspec +5 -1
  11. data/examples/bna.rb +35 -0
  12. data/examples/currency_layer.rb +7 -3
  13. data/examples/fixed_rates.rb +1 -3
  14. data/examples/single_currency.rb +2 -3
  15. data/lib/danconia/currency.rb +2 -2
  16. data/lib/danconia/exchange.rb +47 -0
  17. data/lib/danconia/exchanges/bna.rb +61 -0
  18. data/lib/danconia/exchanges/currency_layer.rb +14 -2
  19. data/lib/danconia/exchanges/fixed_rates.rb +2 -4
  20. data/lib/danconia/integrations/active_record.rb +13 -14
  21. data/lib/danconia/money.rb +30 -24
  22. data/lib/danconia/pair.rb +15 -0
  23. data/lib/danconia/serializable.rb +17 -0
  24. data/lib/danconia/stores/active_record.rb +29 -6
  25. data/lib/danconia/stores/in_memory.rb +4 -9
  26. data/lib/danconia/version.rb +1 -1
  27. data/lib/danconia.rb +7 -4
  28. data/shell.nix +10 -0
  29. data/spec/danconia/exchanges/bna_spec.rb +52 -0
  30. data/spec/danconia/exchanges/currency_layer_spec.rb +28 -33
  31. data/spec/danconia/exchanges/exchange_spec.rb +54 -0
  32. data/spec/danconia/exchanges/fixtures/bna/home.html +124 -0
  33. data/spec/danconia/exchanges/fixtures/currency_layer/failure.json +7 -0
  34. data/spec/danconia/exchanges/fixtures/currency_layer/success.json +8 -0
  35. data/spec/danconia/integrations/active_record_spec.rb +25 -5
  36. data/spec/danconia/money_spec.rb +41 -17
  37. data/spec/danconia/serializable_spec.rb +16 -0
  38. data/spec/danconia/stores/active_record_spec.rb +81 -15
  39. data/spec/danconia/stores/in_memory_spec.rb +18 -0
  40. data/spec/spec_helper.rb +2 -1
  41. metadata +76 -14
  42. data/lib/danconia/exchanges/exchange.rb +0 -33
  43. data/spec/danconia/exchanges/fixed_rates_spec.rb +0 -30
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  module Danconia
4
2
  describe Money do
5
3
  context 'instantiation' do
@@ -39,16 +37,15 @@ module Danconia
39
37
  end
40
38
 
41
39
  it 'should exchange the other currency if it is different' do
42
- expect(Money(1, 'ARS', exchange: fake_exchange(rate: 4)) + Money(1, 'USD')).to eq Money(5, 'ARS')
40
+ m1 = Money(1, 'ARS', exchange_opts: {exchange: fake_exchange(rate: 4)})
41
+ expect(m1 + Money(1, 'USD')).to eq Money(5, 'ARS')
43
42
  end
44
43
 
45
44
  it 'should return a new object with the same options' do
46
- e = fake_exchange
47
- m1 = Money(4, decimals: 3, exchange: e)
45
+ m1 = Money(4, decimals: 3)
48
46
  m2 = m1 * 2
49
47
  expect(m2).to_not eql m1
50
48
  expect(m2.decimals).to eq 3
51
- expect(m2.exchange).to eq e
52
49
  end
53
50
 
54
51
  it 'round should return a money object with the same currency' do
@@ -72,7 +69,7 @@ module Danconia
72
69
  end
73
70
 
74
71
  it 'should exchange to the source currency if they differ' do
75
- TestHelpers.with_rates 'USDARS' => 4 do |config|
72
+ TestHelpers.with_rates 'USDARS' => 4 do |_config|
76
73
  expect(Money(3, 'ARS') < Money(1, 'USD')).to be true
77
74
  expect(Money(4, 'ARS') < Money(1, 'USD')).to be false
78
75
  end
@@ -129,7 +126,8 @@ module Danconia
129
126
  end
130
127
 
131
128
  it 'should allow to pass the exchange to the instance' do
132
- expect(Money(2, 'USD', exchange: fake_exchange(rate: 3)).exchange_to('ARS')).to eq Money(6, 'ARS')
129
+ m = Money(2, 'USD', exchange_opts: {exchange: fake_exchange(rate: 3)})
130
+ expect(m.exchange_to('ARS')).to eq Money(6, 'ARS')
133
131
  end
134
132
 
135
133
  it 'should allow to pass the exchange when converting' do
@@ -153,8 +151,8 @@ module Danconia
153
151
  end
154
152
 
155
153
  it 'should return a new object with the same opts' do
156
- m1 = Money(1, 'USD', decimals: 0, exchange: fake_exchange(rate: 3))
157
- m2 = m1.exchange_to('ARS')
154
+ m1 = Money(1, 'USD', decimals: 0)
155
+ m2 = m1.exchange_to('ARS', exchange: fake_exchange(rate: 3))
158
156
  expect(m2).to_not eql m1
159
157
  expect(m2.decimals).to eq 0
160
158
  expect(m1).to eq Money(1, 'USD')
@@ -164,6 +162,38 @@ module Danconia
164
162
  expect(Money(1, 'USD').exchange_to('')).to eq Money(1, 'USD')
165
163
  expect(Money(1, 'ARS').exchange_to('')).to eq Money(1, 'ARS')
166
164
  end
165
+
166
+ context 'opts' do
167
+ let(:exchange) do
168
+ Class.new(Exchange) do
169
+ def rates opts
170
+ case opts[:type]
171
+ when 'divisa' then {'USDARS' => 7}
172
+ when 'billete' then {'USDARS' => 8}
173
+ else {}
174
+ end
175
+ end
176
+ end.new
177
+ end
178
+
179
+ it 'allows to specify opts to pass to the exchange (filters for example)' do
180
+ expect(Money(1, 'USD').exchange_to('ARS', type: 'divisa', exchange: exchange)).to eq Money(7, 'ARS')
181
+ expect { Money(1, 'USD').exchange_to('ARS', exchange: exchange) }.to raise_error Errors::ExchangeRateNotFound
182
+ end
183
+
184
+ it 'should be preserved after operations' do
185
+ m1 = Money(1, 'USD').exchange_to('ARS', type: 'divisa', exchange: exchange)
186
+ m2 = m1 + Money(2, 'USD')
187
+
188
+ expect(m2).to eq Money(21, 'ARS')
189
+ expect(m1 < Money(2, 'USD')).to eq true
190
+ end
191
+
192
+ it 'should use the instance exchange_opts by default' do
193
+ m = Money(1, 'USD', exchange_opts: {exchange: exchange, type: 'billete'})
194
+ expect(m.exchange_to('ARS')).to eq Money(8, 'ARS')
195
+ end
196
+ end
167
197
  end
168
198
 
169
199
  context 'default_currency?' do
@@ -183,14 +213,8 @@ module Danconia
183
213
  end
184
214
  end
185
215
 
186
- context 'to_json' do
187
- it 'should delegate to the amount' do
188
- expect(Money(1).to_json).to eq '"1.0"'
189
- end
190
- end
191
-
192
216
  def fake_exchange args = {}
193
- double 'Danconia::Exchanges::Exchange', args.reverse_merge(rate: nil)
217
+ double 'Danconia::Exchange', args.reverse_merge(rate: nil)
194
218
  end
195
219
  end
196
220
  end
@@ -0,0 +1,16 @@
1
+ module Danconia
2
+ describe Serializable do
3
+ context 'marshalling' do
4
+ it 'money objects support dump and load' do
5
+ expect(Marshal.load(Marshal.dump(Money(5, 'USD')))).to eq Money(5, 'USD')
6
+ expect(Marshal.load(Marshal.dump(Money(3.2, 'ARS')))).to eq Money(3.2, 'ARS')
7
+ end
8
+ end
9
+
10
+ context 'to_json' do
11
+ it 'should delegate to the amount' do
12
+ expect(Money(1).to_json).to eq %({"amount":"1.0","currency":"USD"})
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,31 +1,97 @@
1
- require 'spec_helper'
2
-
3
1
  module Danconia
4
2
  module Stores
5
3
  describe ActiveRecord, active_record: true do
4
+ before do
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.string :rate_type
10
+ t.date :date
11
+ end
12
+ end
13
+ end
14
+
6
15
  context 'save_rates' do
7
16
  it 'should create or update the rates' do
8
17
  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).to eq('USDEUR' => 3, 'USDARS' => 4)
18
+
19
+ expect do
20
+ subject.save_rates [{pair: 'USDEUR', rate: 3}, {pair: 'USDARS', rate: 4}]
21
+ end.to change { ExchangeRate.count }.by 1
22
+
23
+ expect(subject.rates).to match [
24
+ include(pair: 'USDEUR', rate: 3),
25
+ include(pair: 'USDARS', rate: 4)
26
+ ]
27
+ end
28
+
29
+ it 'allows to specify other keys to use as unique' do
30
+ store = ActiveRecord.new(unique_keys: %i[pair rate_type])
31
+ store.save_rates [
32
+ {pair: 'USDARS', rate: 3, rate_type: 'billetes'},
33
+ {pair: 'USDARS', rate: 4, rate_type: 'divisas'}
34
+ ]
35
+ store.save_rates [
36
+ {pair: 'USDARS', rate: 33, rate_type: 'billetes'}
37
+ ]
38
+ expect(subject.rates).to match [
39
+ include(pair: 'USDARS', rate: 33, rate_type: 'billetes'),
40
+ include(pair: 'USDARS', rate: 4, rate_type: 'divisas')
41
+ ]
42
+ end
43
+
44
+ it 'ignores fields not present in the database table' do
45
+ subject.save_rates [{pair: 'USDEUR', rate: 3, non_existant: 'ignoreme'}]
11
46
  end
12
47
  end
13
48
 
14
- context '#direct_rate' do
15
- it 'should find the rate for the pair' do
49
+ context 'rates' do
50
+ it 'returns an array like the one it received' do
16
51
  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
52
+ ExchangeRate.create! pair: 'USDARS', rate: 40
53
+
54
+ expect(subject.rates).to match [
55
+ include(pair: 'USDEUR', rate: 2),
56
+ include(pair: 'USDARS', rate: 40)
57
+ ]
58
+ end
59
+
60
+ it 'allows to pass filters' do
61
+ store = ActiveRecord.new(unique_keys: %i[pair date])
62
+ store.save_rates [
63
+ {pair: 'USDEUR', rate: 10, date: Date.new(2020, 1, 1)},
64
+ {pair: 'USDEUR', rate: 20, date: Date.new(2020, 1, 2)},
65
+ {pair: 'USDEUR', rate: 30, date: Date.new(2020, 1, 3)}
66
+ ]
67
+
68
+ expect(store.rates.size).to eq 3
69
+ expect(store.rates(date: Date.new(2020, 1, 2))).to match [include(rate: 20)]
19
70
  end
20
71
  end
21
72
 
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
73
+ context 'special date field' do
74
+ let(:store) do
75
+ store = ActiveRecord.new(unique_keys: %i[pair date], date_field: :date)
76
+ store.save_rates [
77
+ {pair: 'USDEUR', rate: 10, date: Date.new(2000, 1, 1)},
78
+ {pair: 'USDEUR', rate: 20, date: Date.new(2000, 1, 2)},
79
+ {pair: 'USDEUR', rate: 30, date: Date.new(2000, 1, 4)}
80
+ ]
81
+ store
82
+ end
83
+
84
+ it 'calling #rates with a particular date when there are rates for that date' do
85
+ expect(store.rates(date: Date.new(2000, 1, 2))).to match [include(rate: 20)]
86
+ end
87
+
88
+ it 'calling #rates with a particular date when there are not rates for that date should return the previous' do
89
+ expect(store.rates(date: Date.new(2000, 1, 3))).to match [include(rate: 20)]
90
+ expect(store.rates(date: Date.new(1999, 12, 31))).to eq []
91
+ end
92
+
93
+ it 'calling #rates without a particular date, uses today' do
94
+ expect(store.rates).to match [include(rate: 30)]
29
95
  end
30
96
  end
31
97
  end
@@ -0,0 +1,18 @@
1
+ module Danconia
2
+ module Stores
3
+ describe InMemory do
4
+ context 'rates' do
5
+ it 'filters rates by type' do
6
+ subject.save_rates [
7
+ {pair: 'USDARS', rate: 3, rate_type: 'divisas'},
8
+ {pair: 'USDARS', rate: 4, rate_type: 'billetes'}
9
+ ]
10
+
11
+ expect(subject.rates(rate_type: 'divisas')).to match [include(rate: 3)]
12
+ expect(subject.rates(rate_type: 'billetes')).to match [include(rate: 4)]
13
+ expect(subject.rates).to match [include(rate: 3), include(rate: 4)]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  require 'danconia'
2
2
  require 'danconia/test_helpers'
3
+ require 'danconia/integrations/active_record'
3
4
  require 'webmock/rspec'
4
5
 
5
- Dir["#{__dir__}/support/*.rb"].each { |f| require f }
6
+ Dir["#{__dir__}/support/*.rb"].sort.each { |f| require f }
6
7
 
7
8
  RSpec.configure do |config|
8
9
  config.filter_run_when_matching :focus
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: danconia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emmanuel Nicolau
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-31 00:00:00.000000000 Z
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.0.0
19
+ version: '4.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 3.0.0
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: sqlite3
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -108,14 +122,45 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: nokogiri
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.80.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.80.0
111
153
  description: Multi-currency money library backed by BigDecimal
112
154
  email: emmanicolau@gmail.com
113
- executables: []
155
+ executables:
156
+ - console
114
157
  extensions: []
115
158
  extra_rdoc_files: []
116
159
  files:
160
+ - ".envrc"
117
161
  - ".gitignore"
118
162
  - ".rspec"
163
+ - ".rubocop.yml"
119
164
  - ".ruby-version"
120
165
  - ".travis.yml"
121
166
  - Gemfile
@@ -124,7 +169,9 @@ files:
124
169
  - LICENSE.txt
125
170
  - README.md
126
171
  - Rakefile
172
+ - bin/console
127
173
  - danconia.gemspec
174
+ - examples/bna.rb
128
175
  - examples/currency_layer.rb
129
176
  - examples/fixed_rates.rb
130
177
  - examples/single_currency.rb
@@ -133,28 +180,38 @@ files:
133
180
  - lib/danconia/currency.rb
134
181
  - lib/danconia/errors/api_error.rb
135
182
  - lib/danconia/errors/exchange_rate_not_found.rb
183
+ - lib/danconia/exchange.rb
184
+ - lib/danconia/exchanges/bna.rb
136
185
  - lib/danconia/exchanges/currency_layer.rb
137
- - lib/danconia/exchanges/exchange.rb
138
186
  - lib/danconia/exchanges/fixed_rates.rb
139
187
  - lib/danconia/integrations/active_record.rb
140
188
  - lib/danconia/kernel.rb
141
189
  - lib/danconia/money.rb
190
+ - lib/danconia/pair.rb
191
+ - lib/danconia/serializable.rb
142
192
  - lib/danconia/stores/active_record.rb
143
193
  - lib/danconia/stores/in_memory.rb
144
194
  - lib/danconia/test_helpers.rb
145
195
  - lib/danconia/version.rb
196
+ - shell.nix
197
+ - spec/danconia/exchanges/bna_spec.rb
146
198
  - spec/danconia/exchanges/currency_layer_spec.rb
147
- - spec/danconia/exchanges/fixed_rates_spec.rb
199
+ - spec/danconia/exchanges/exchange_spec.rb
200
+ - spec/danconia/exchanges/fixtures/bna/home.html
201
+ - spec/danconia/exchanges/fixtures/currency_layer/failure.json
202
+ - spec/danconia/exchanges/fixtures/currency_layer/success.json
148
203
  - spec/danconia/integrations/active_record_spec.rb
149
204
  - spec/danconia/money_spec.rb
205
+ - spec/danconia/serializable_spec.rb
150
206
  - spec/danconia/stores/active_record_spec.rb
207
+ - spec/danconia/stores/in_memory_spec.rb
151
208
  - spec/spec_helper.rb
152
209
  - spec/support/database.rb
153
210
  homepage: https://github.com/eeng/danconia
154
211
  licenses:
155
212
  - MIT
156
213
  metadata: {}
157
- post_install_message:
214
+ post_install_message:
158
215
  rdoc_options: []
159
216
  require_paths:
160
217
  - lib
@@ -169,16 +226,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
226
  - !ruby/object:Gem::Version
170
227
  version: '0'
171
228
  requirements: []
172
- rubyforge_project:
173
- rubygems_version: 2.7.3
174
- signing_key:
229
+ rubygems_version: 3.2.26
230
+ signing_key:
175
231
  specification_version: 4
176
232
  summary: Multi-currency money library backed by BigDecimal
177
233
  test_files:
234
+ - spec/danconia/exchanges/bna_spec.rb
178
235
  - spec/danconia/exchanges/currency_layer_spec.rb
179
- - spec/danconia/exchanges/fixed_rates_spec.rb
236
+ - spec/danconia/exchanges/exchange_spec.rb
237
+ - spec/danconia/exchanges/fixtures/bna/home.html
238
+ - spec/danconia/exchanges/fixtures/currency_layer/failure.json
239
+ - spec/danconia/exchanges/fixtures/currency_layer/success.json
180
240
  - spec/danconia/integrations/active_record_spec.rb
181
241
  - spec/danconia/money_spec.rb
242
+ - spec/danconia/serializable_spec.rb
182
243
  - spec/danconia/stores/active_record_spec.rb
244
+ - spec/danconia/stores/in_memory_spec.rb
183
245
  - spec/spec_helper.rb
184
246
  - spec/support/database.rb
@@ -1,33 +0,0 @@
1
- module Danconia
2
- module Exchanges
3
- class Exchange
4
- attr_reader :store
5
-
6
- def initialize store: Stores::InMemory.new
7
- @store = store
8
- end
9
-
10
- def rate from, to
11
- if from == to
12
- 1.0
13
- elsif from == 'USD' and direct_rate = @store.direct_rate(from, to)
14
- direct_rate
15
- elsif to == 'USD' and inverse_rate = @store.direct_rate(to, from)
16
- (1.0 / inverse_rate).round 6
17
- elsif from != 'USD' and to != 'USD' and from_in_usd = rate(from, 'USD') and to_per_usd = rate('USD', to)
18
- (from_in_usd * to_per_usd).round 6
19
- else
20
- raise Errors::ExchangeRateNotFound.new(from, to)
21
- end
22
- end
23
-
24
- def rates
25
- @store.rates
26
- end
27
-
28
- def update_rates!
29
- @store.save_rates fetch_rates
30
- end
31
- end
32
- end
33
- end
@@ -1,30 +0,0 @@
1
- require 'spec_helper'
2
-
3
- module Danconia
4
- module Exchanges
5
- describe FixedRates do
6
- context 'rate' do
7
- it 'returns the exchange rate value for the supplied currencies' do
8
- exchange = FixedRates.new rates: {'USDEUR' => 3, 'USDARS' => 4}
9
- expect(exchange.rate 'USD', 'EUR').to eq 3
10
- expect(exchange.rate 'USD', 'ARS').to eq 4
11
- end
12
-
13
- it 'returns nil if not found' do
14
- expect { subject.rate 'USD', 'EUR' }.to raise_error Errors::ExchangeRateNotFound
15
- end
16
-
17
- it 'if the direct conversion is not found, tries to find the inverse' do
18
- exchange = FixedRates.new rates: {'USDEUR' => 3}
19
- expect(exchange.rate 'EUR', 'USD').to eq (1.0 / 3).round 6
20
- end
21
-
22
- it 'if not direct nor inverse conversion is found and both are different than USD, tries to convert through USD' do
23
- exchange = FixedRates.new rates: {'USDEUR' => 3, 'USDARS' => 6}
24
- expect(exchange.rate 'EUR', 'ARS').to be_within(0.00001).of 2
25
- expect(exchange.rate 'ARS', 'EUR').to be_within(0.00001).of 0.5
26
- end
27
- end
28
- end
29
- end
30
- end