danconia 0.2.9 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7427977cd93825975491eddf43bcd9e5fda9389102d2c2b32c16efdfb11f590
4
- data.tar.gz: 15bccf8fc66c26679cc101d5f27dd9aabc5398b28fe1a04a00b167500993eedf
3
+ metadata.gz: d059c26cfe3dfb8fdd9da2a14f14d4207f49d39e57a8ac11624e4069f1612632
4
+ data.tar.gz: 2a7022742e0c9fdb318d57bf916828c18eab1c040aa7ec571912e243aef14028
5
5
  SHA512:
6
- metadata.gz: 95d1edfd74b3388998c3251ac204a61a10a7454e0eda2fc2ca3b6105f95d9ead47c511a67790e82d34d4d54f934db83cc7326da53abef51c074846571b28a819
7
- data.tar.gz: 033bb6ae1f171059bceb87495b9490b2d760f6371d71ec74f8a6a494f628ab52302f91f19f2a0e9f344c9c4775fecd0027c071e0cc21db51568e1efd80ea8c70
6
+ metadata.gz: 6c3ad4d5a0d58ca3fb6be34d557dfec5ef0578adf627edc2de3c7cee8b68eab84aafcebbf58c59e5622974dc8b2b7e609dd58e1d7dcf502eacd2090610ed7b4a
7
+ data.tar.gz: 439f6bb772fdef77bc221c6e4167a053b6366ff39fe8b20c3b6f81393901bc7c8364c1c326e357c0d2160ecd7bb50734cbdf1ae37eeb169bd2232b93e13e5181
@@ -0,0 +1,104 @@
1
+ Style/Documentation:
2
+ Enabled: false
3
+
4
+ Style/MethodDefParentheses:
5
+ Enabled: false
6
+
7
+ Style/ParallelAssignment:
8
+ Enabled: false
9
+
10
+ Style/FrozenStringLiteralComment:
11
+ Enabled: false
12
+
13
+ Style/FormatStringToken:
14
+ Enabled: false
15
+
16
+ Style/EmptyMethod:
17
+ Enabled: false
18
+
19
+ Style/GuardClause:
20
+ Enabled: false
21
+
22
+ Style/CommentAnnotation:
23
+ Enabled: false
24
+
25
+ Style/AsciiComments:
26
+ Enabled: false
27
+
28
+ Style/StabbyLambdaParentheses:
29
+ EnforcedStyle: require_no_parentheses
30
+
31
+ Style/MultilineIfModifier:
32
+ Enabled: false
33
+
34
+ Style/SafeNavigation:
35
+ Enabled: false
36
+
37
+ Style/FormatString:
38
+ Enabled: false
39
+
40
+ Style/AndOr:
41
+ Enabled: false
42
+
43
+ Style/Next:
44
+ Enabled: false
45
+
46
+ Style/NumericPredicate:
47
+ Enabled: false
48
+
49
+ Style/SymbolArray:
50
+ Enabled: false
51
+
52
+ Style/MutableConstant:
53
+ Enabled: false
54
+
55
+ Style/RescueModifier:
56
+ Enabled: false
57
+
58
+ Layout/LineLength:
59
+ Max: 120
60
+
61
+ Layout/SpaceInsideHashLiteralBraces:
62
+ Enabled: false
63
+
64
+ Layout/MultilineMethodCallIndentation:
65
+ Enabled: false
66
+
67
+ Layout/TrailingEmptyLines:
68
+ Enabled: false
69
+
70
+ Layout/FirstArrayElementIndentation:
71
+ EnforcedStyle: consistent
72
+
73
+ Layout/ParameterAlignment:
74
+ EnforcedStyle: with_fixed_indentation
75
+
76
+ Layout/SpaceInLambdaLiteral:
77
+ Enabled: false
78
+
79
+ Layout/EmptyLineAfterGuardClause:
80
+ Enabled: false
81
+
82
+ Metrics:
83
+ Enabled: false
84
+
85
+ Bundler:
86
+ Enabled: false
87
+
88
+ Lint/AssignmentInCondition:
89
+ Enabled: false
90
+
91
+ Lint/ShadowingOuterLocalVariable:
92
+ Enabled: false
93
+
94
+ Lint/AmbiguousRegexpLiteral:
95
+ Enabled: false
96
+
97
+ Lint/AmbiguousOperator:
98
+ Enabled: false
99
+
100
+ Lint/AmbiguousBlockAssociation:
101
+ Enabled: false
102
+
103
+ Naming/AccessorMethodName:
104
+ Enabled: false
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- danconia (0.2.9)
5
- activerecord (>= 3.0.0)
4
+ danconia (0.3.0)
5
+ activesupport (>= 4.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -20,6 +20,7 @@ GEM
20
20
  zeitwerk (~> 2.2, >= 2.2.2)
21
21
  addressable (2.5.2)
22
22
  public_suffix (>= 2.0.2, < 4.0)
23
+ ast (2.4.1)
23
24
  coderay (1.1.2)
24
25
  concurrent-ruby (1.1.7)
25
26
  crack (0.4.3)
@@ -44,25 +45,34 @@ GEM
44
45
  hashdiff (0.3.7)
45
46
  i18n (1.8.5)
46
47
  concurrent-ruby (~> 1.0)
48
+ jaro_winkler (1.5.4)
47
49
  listen (3.1.5)
48
50
  rb-fsevent (~> 0.9, >= 0.9.4)
49
51
  rb-inotify (~> 0.9, >= 0.9.7)
50
52
  ruby_dep (~> 1.2)
51
53
  lumberjack (1.0.13)
52
54
  method_source (0.9.0)
55
+ mini_portile2 (2.4.0)
53
56
  minitest (5.14.2)
54
57
  nenv (0.3.0)
58
+ nokogiri (1.10.10)
59
+ mini_portile2 (~> 2.4.0)
55
60
  notiffany (0.1.1)
56
61
  nenv (~> 0.1)
57
62
  shellany (~> 0.0)
63
+ parallel (1.19.2)
64
+ parser (2.7.1.4)
65
+ ast (~> 2.4.1)
58
66
  pry (0.11.3)
59
67
  coderay (~> 1.1.0)
60
68
  method_source (~> 0.9.0)
61
69
  public_suffix (3.0.2)
70
+ rainbow (3.0.0)
62
71
  rake (13.0.1)
63
72
  rb-fsevent (0.10.3)
64
73
  rb-inotify (0.9.10)
65
74
  ffi (>= 0.5.0, < 2)
75
+ rexml (3.2.4)
66
76
  rspec (3.7.0)
67
77
  rspec-core (~> 3.7.0)
68
78
  rspec-expectations (~> 3.7.0)
@@ -76,6 +86,15 @@ GEM
76
86
  diff-lcs (>= 1.2.0, < 2.0)
77
87
  rspec-support (~> 3.7.0)
78
88
  rspec-support (3.7.1)
89
+ rubocop (0.80.1)
90
+ jaro_winkler (~> 1.5.1)
91
+ parallel (~> 1.10)
92
+ parser (>= 2.7.0.1)
93
+ rainbow (>= 2.2.2, < 4.0)
94
+ rexml
95
+ ruby-progressbar (~> 1.7)
96
+ unicode-display_width (>= 1.4.0, < 1.7)
97
+ ruby-progressbar (1.10.1)
79
98
  ruby_dep (1.5.0)
80
99
  safe_yaml (1.0.4)
81
100
  shellany (0.0.1)
@@ -84,6 +103,7 @@ GEM
84
103
  thread_safe (0.3.6)
85
104
  tzinfo (1.2.7)
86
105
  thread_safe (~> 0.1)
106
+ unicode-display_width (1.6.1)
87
107
  webmock (3.4.2)
88
108
  addressable (>= 2.3.6)
89
109
  crack (>= 0.3.2)
@@ -94,11 +114,14 @@ PLATFORMS
94
114
  ruby
95
115
 
96
116
  DEPENDENCIES
117
+ activerecord (>= 4.0)
97
118
  danconia!
98
119
  guard
99
120
  guard-rspec
121
+ nokogiri
100
122
  rake
101
123
  rspec
124
+ rubocop (~> 0.80.0)
102
125
  sqlite3
103
126
  webmock
104
127
 
data/README.md CHANGED
@@ -1,9 +1,19 @@
1
1
  # Danconia
2
2
 
3
- A very simple money library for Ruby, backed by BigDecimal (no conversion to cents, i.e. "infinite precision") with support for external exchange rates services.
3
+ A very simple money library for Ruby, backed by BigDecimal, with multi-currency support.
4
4
 
5
5
  [![Build Status](https://travis-ci.org/eeng/danconia.svg?branch=master)](https://travis-ci.org/eeng/danconia)
6
6
 
7
+ ## Features
8
+
9
+ * Backed by BigDecimal (no conversion to cents is done, i.e. "infinite precision")
10
+ * Multi-currency support
11
+ * Pluggable exchange rates services:
12
+ - [CurrencyLayer API](https://currencylayer.com/)
13
+ - [BNA](https://www.bna.com.ar/)
14
+ - FixedRates (for testing use)
15
+ * Pluggable stores for persisting the exchange rates
16
+
7
17
  ## Installation
8
18
 
9
19
  ```ruby
@@ -29,10 +39,12 @@ Please refer to `examples/single_currency.rb` for some configuration options.
29
39
 
30
40
  ## Multi-Currency Support
31
41
 
32
- To handle multiple currencies you need to configure an `Exchange` in order to fetch the rates. For example, with [CurrencyLayer](https://currencylayer.com/):
42
+ To handle multiple currencies you need to configure an `Exchange` in order to fetch the rates. For example, with CurrencyLayer:
33
43
 
34
44
  ```ruby
35
45
  # This can be placed in a Rails initializer
46
+ require 'danconia/exchanges/currency_layer'
47
+
36
48
  Danconia.configure do |config|
37
49
  config.default_exchange = Danconia::Exchanges::CurrencyLayer.new(access_key: '...')
38
50
  end
@@ -56,6 +68,8 @@ By default, rates are stored in memory, but you can supply a store in the exchan
56
68
  Given a `products` table with a decimal column `price` and a string column `price_currency` (optional), then you can use the `money` class method to automatically convert it to Money:
57
69
 
58
70
  ```ruby
71
+ require 'danconia/integrations/active_record'
72
+
59
73
  class Product < ActiveRecord::Base
60
74
  money :price
61
75
  end
@@ -17,11 +17,15 @@ Gem::Specification.new do |gem|
17
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
- gem.add_dependency "activerecord", '>= 3.0.0'
20
+
21
+ gem.add_dependency 'activesupport', '>= 4.0'
22
+ gem.add_development_dependency "activerecord", '>= 4.0'
21
23
  gem.add_development_dependency "sqlite3"
22
24
  gem.add_development_dependency "rake"
23
25
  gem.add_development_dependency "guard"
24
26
  gem.add_development_dependency "guard-rspec"
25
27
  gem.add_development_dependency "rspec"
26
28
  gem.add_development_dependency "webmock"
29
+ gem.add_development_dependency "nokogiri"
30
+ gem.add_development_dependency 'rubocop', '~> 0.80.0'
27
31
  end
@@ -0,0 +1,35 @@
1
+ # Example using BNA (Banco Nación de Argentina) and storing rates daily in ActiveRecord.
2
+
3
+ require 'danconia/integrations/active_record'
4
+ require 'danconia/exchanges/bna'
5
+
6
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
7
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
8
+
9
+ ActiveRecord::Schema.define do
10
+ # You can use this in a Rails migration
11
+ create_table :exchange_rates do |t|
12
+ t.date :date
13
+ t.string :pair, limit: 6
14
+ t.string :rate_type
15
+ t.decimal :rate, precision: 12, scale: 6
16
+ t.index [:date, :pair, :rate_type], unique: true
17
+ end
18
+ end
19
+
20
+ Danconia.configure do |config|
21
+ config.default_exchange = Danconia::Exchanges::BNA.new(
22
+ store: Danconia::Stores::ActiveRecord.new(unique_keys: %i[date pair rate_type], date_field: :date)
23
+ )
24
+ end
25
+
26
+ # Periodically call this method to keep the rates up to date
27
+ puts 'Updating rates...'
28
+ Danconia.config.default_exchange.update_rates!
29
+
30
+ # Uses the latest rate
31
+ puts Money(1, 'USD').exchange_to('ARS', rate_type: 'billetes').inspect
32
+ puts Money(1, 'USD').exchange_to('ARS', rate_type: 'divisas').inspect
33
+
34
+ # Raises Danconia::Errors::ExchangeRateNotFound as there is no rate for that date
35
+ # Money(1, 'USD').exchange_to('ARS', rate_type: 'billetes', date: Date.new(2000))
@@ -1,4 +1,9 @@
1
+ # Example using CurrencyLayer API and storing only the latests rates in ActiveRecord.
1
2
  # Remember to supply your CurrencyLayer key in the ACCESS_KEY environment variable to run this example
3
+
4
+ require 'danconia/integrations/active_record'
5
+ require 'danconia/exchanges/currency_layer'
6
+
2
7
  ActiveRecord::Base.logger = Logger.new(STDOUT)
3
8
  ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
4
9
 
@@ -18,8 +23,8 @@ Danconia.configure do |config|
18
23
  )
19
24
  end
20
25
 
21
- # Periodically call this method to keep rates up to date
22
- puts 'Updating dates with CurrencyLayer API...'
26
+ # Periodically call this method to keep the rates up to date
27
+ puts 'Updating rates...'
23
28
  Danconia.config.default_exchange.update_rates!
24
29
 
25
30
  puts Money(1, 'USD').exchange_to('EUR').inspect # => 0.854896 EUR
@@ -1,6 +1,7 @@
1
+ # Example showing how to use a single currency.
2
+
1
3
  # USD is the default currency if no configuration is provided
2
- puts (Money(10.25) / 2).inspect # => 5.125 USD
3
- puts (Money(10.25) / 2).to_s # => $5.13
4
+ puts Money(10.25).inspect # => 5.125 USD
4
5
 
5
6
  # Lets switch to other currency
6
7
  Danconia.configure do |config|
@@ -1,11 +1,13 @@
1
+ require 'date'
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/object/blank'
5
+
1
6
  require 'danconia/version'
2
7
  require 'danconia/config'
3
8
  require 'danconia/currency'
4
9
  require 'danconia/money'
5
10
  require 'danconia/kernel'
6
- require 'danconia/integrations/active_record'
7
- require 'danconia/exchanges/exchange'
11
+ require 'danconia/exchange'
8
12
  require 'danconia/exchanges/fixed_rates'
9
- require 'danconia/exchanges/currency_layer'
10
13
  require 'danconia/stores/in_memory'
11
- require 'danconia/stores/active_record'
@@ -1,6 +1,6 @@
1
1
  module Danconia
2
- class Currency < Struct.new(:code, :symbol, :description, keyword_init: true)
3
- def self.find code, exchange
2
+ Currency = Struct.new(:code, :symbol, :description, keyword_init: true) do
3
+ def self.find code
4
4
  return code if code.is_a? Currency
5
5
  new Danconia.config.available_currencies.find { |c| c[:code] == code } || {code: code, symbol: '$'}
6
6
  end
@@ -0,0 +1,47 @@
1
+ require 'danconia/pair'
2
+
3
+ module Danconia
4
+ class Exchange
5
+ def rate from, to, **opts
6
+ return 1.0 if from == to
7
+
8
+ pair = Pair.new(from, to)
9
+ rates = direct_and_inverted_rates(opts)
10
+ rates[pair] or indirect_rate(pair, rates) or raise Errors::ExchangeRateNotFound.new(from, to)
11
+ end
12
+
13
+ # Override this method in subclasses. Should return a map of pairs to rates.
14
+ # See `FixedRates` for an example implementation.
15
+ def rates **_opts
16
+ raise NotImplementedError
17
+ end
18
+
19
+ private
20
+
21
+ # Returns the original rates plus the inverted ones, to simplify rate finding logic.
22
+ # Also wraps the pair strings into Pair objects.
23
+ def direct_and_inverted_rates opts
24
+ rates(opts).each_with_object({}) do |(pair_str, rate), rs|
25
+ pair = Pair.parse(pair_str)
26
+ rs[pair] = rate
27
+ rs[pair.invert] ||= 1.0 / rate
28
+ end
29
+ end
30
+
31
+ def indirect_rate ind_pair, rates
32
+ if (from_pair = rates.keys.detect { |pair| pair.from == ind_pair.from }) &&
33
+ (to_pair = rates.keys.detect { |pair| pair.to == ind_pair.to })
34
+ rates[from_pair] * rates[to_pair]
35
+ end
36
+ end
37
+
38
+ def array_of_rates_to_hash array
39
+ pairs = array.map { |er| er[:pair] }
40
+ if pairs.size != pairs.uniq.size
41
+ raise ArgumentError, "Exchange returned duplicate pairs. Maybe you forgot a filter?\n#{array}"
42
+ end
43
+
44
+ Hash[array.map { |er| er.values_at(:pair, :rate) }]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,61 @@
1
+ require 'danconia/errors/api_error'
2
+ require 'net/http'
3
+ require 'nokogiri'
4
+
5
+ module Danconia
6
+ module Exchanges
7
+ # The BNA does not provide an API to pull the rates, so this implementation scrapes the home HTML directly.
8
+ # Returns rates of both types, "Billetes" and "Divisas", and only the "tipo de cambio vendedor" ones.
9
+ # See `examples/bna.rb` for a complete usage example.
10
+ class BNA < Exchange
11
+ attr_reader :store
12
+
13
+ def initialize store: Stores::InMemory.new
14
+ @store = store
15
+ end
16
+
17
+ def fetch_rates
18
+ response = Net::HTTP.get URI 'https://www.bna.com.ar/Personas'
19
+ scrape_rates(response, 'billetes') + scrape_rates(response, 'divisas')
20
+ end
21
+
22
+ def update_rates!
23
+ @store.save_rates fetch_rates
24
+ end
25
+
26
+ def rates rate_type:, date: nil
27
+ array_of_rates_to_hash @store.rates(rate_type: rate_type, date: date)
28
+ end
29
+
30
+ private
31
+
32
+ def scrape_rates response, type
33
+ doc = Nokogiri::XML(response).css("##{type}")
34
+
35
+ if doc.css('thead th:last-child').text != 'Venta'
36
+ raise Errors::APIError, "Could not scrape '#{type}' rates. Maybe the format changed?"
37
+ end
38
+
39
+ doc.css('tbody tr').map do |row|
40
+ pair = parse_pair(row.css('td:first-child').text) or next
41
+ rate = parse_rate(row.css('td:last-child').text, pair)
42
+ date = Date.parse(doc.css('.fechaCot').text)
43
+ {pair: pair, rate: rate, date: date, rate_type: type}
44
+ end.compact.presence or raise Errors::APIError, "Could not scrape '#{type}' rates. Maybe the format changed?"
45
+ end
46
+
47
+ def parse_pair cur_name
48
+ case cur_name
49
+ when 'Dolar U.S.A' then 'USDARS'
50
+ when 'Euro' then 'EURARS'
51
+ when 'Real *' then 'BRLARS'
52
+ end
53
+ end
54
+
55
+ def parse_rate str, pair
56
+ val = Float(str.tr(',', '.'))
57
+ pair == 'BRLARS' ? val / 100.0 : val
58
+ end
59
+ end
60
+ end
61
+ end