fx_exchange_rate 1.0.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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rdoc_options +32 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +17 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +5 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +100 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +106 -0
  13. data/Rakefile +8 -0
  14. data/bin/console +12 -0
  15. data/bin/setup +8 -0
  16. data/fixtures/vcr_cassettes/ExchangeRate/_retrieve/fetches_the_feed_and_stores_the_values_locally.yml +112 -0
  17. data/fixtures/vcr_cassettes/ExchangeRate/_retrieve/overwrites_existing_values.yml +112 -0
  18. data/fixtures/vcr_cassettes/ExchangeRate/_retrieve/raises_ExchangeRate_RetrievalFailed_when_the_the_feed_cannot_be_retrieved.yml +66 -0
  19. data/fixtures/vcr_cassettes/ExchangeRate_RateSources_ECBRateRetriever/_save_/raises_when_the_feed_content_is_invalid.yml +64 -0
  20. data/fixtures/vcr_cassettes/ExchangeRate_RateSources_ECBRateRetriever/_save_/retrieves_the_feed.yml +112 -0
  21. data/fx_exchange_rate.gemspec +39 -0
  22. data/lib/exchange_rate.rb +31 -0
  23. data/lib/exchange_rate/configuration.rb +35 -0
  24. data/lib/exchange_rate/currency_converter.rb +78 -0
  25. data/lib/exchange_rate/currency_rate.rb +34 -0
  26. data/lib/exchange_rate/database_connection.rb +37 -0
  27. data/lib/exchange_rate/db/migrate/20180827184400_create_currency_rates.rb +18 -0
  28. data/lib/exchange_rate/initializers/sequel.rb +12 -0
  29. data/lib/exchange_rate/missing_rate_error.rb +8 -0
  30. data/lib/exchange_rate/module_methods.rb +87 -0
  31. data/lib/exchange_rate/rate_sources.rb +9 -0
  32. data/lib/exchange_rate/rate_sources/e_c_b_rate_retriever.rb +119 -0
  33. data/lib/exchange_rate/retrieval_failed_error.rb +7 -0
  34. data/lib/exchange_rate/version.rb +7 -0
  35. metadata +247 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExchangeRate
4
+ ##
5
+ # Convert between currencies, using the local cache store
6
+ #
7
+ # Examples
8
+ #
9
+ # CurrencyConverter.new(Date.today, 'GBP', 'USD').convert!
10
+ # # => 1.291700027
11
+ #
12
+ class CurrencyConverter
13
+ ##
14
+ # Builds a CurrencyConverter. The cache is not accessed on initialization.
15
+ #
16
+ # date_of_rate - The date the rate should be used, for historical conversions.
17
+ # source_currency - The source currency code, e.g. 'GBP'.
18
+ # target_currency - The target currency code, e.g. 'USD'.
19
+ def initialize(date_of_rate, source_currency, target_currency)
20
+ @date_of_rate = date_of_rate
21
+ @source_currency = source_currency
22
+ @target_currency = target_currency
23
+ end
24
+
25
+ ##
26
+ # Convert between the source and target currencies.
27
+ # By default this will take 1 unit of the source currency.
28
+ #
29
+ # source_value - The number of units of source currency to convert.
30
+ #
31
+ # Examples
32
+ #
33
+ # converter = CurrencyConverter.new(Date.today, 'GBP', 'USD')
34
+ #
35
+ # converter.convert
36
+ # # => 1.291700027
37
+ #
38
+ # converter.convert(source_value: 2)
39
+ # # => 2.583400054
40
+ #
41
+ def convert!(source_value: 1)
42
+ (source_value / source_rate) * target_rate
43
+ end
44
+
45
+ ##
46
+ # The (cached) source ExchangeRate::CurrencyRate object.
47
+ #
48
+ # Returns ExchangeRate::CurrencyRate.
49
+ #
50
+ # Raises ExchangeRate::MissingRateError if a cached rate does not exist.
51
+ def source_rate
52
+ find_currency_rate(@source_currency, @date_of_rate).value_in_euro
53
+ end
54
+
55
+ ##
56
+ # The (cached) source ExchangeRate::CurrencyRate object.
57
+ #
58
+ # Returns ExchangeRate::CurrencyRate.
59
+ #
60
+ # Raises ExchangeRate::MissingRateError if a cached rate does not exist.
61
+ def target_rate
62
+ find_currency_rate(@target_currency, @date_of_rate).value_in_euro
63
+ end
64
+
65
+ private
66
+
67
+ ##
68
+ # Finds a currency rate
69
+ #
70
+ # Returns ExchangeRate::CurrencyRate.
71
+ #
72
+ # Raises ExchangeRate::MissingRateError if a cached rate does not exist.
73
+ def find_currency_rate(currency, date_of_rate)
74
+ CurrencyRate.first(currency: currency, date_of_rate: date_of_rate)
75
+ .tap { |rate| raise ExchangeRate::MissingRateError if rate.nil? }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+ require 'exchange_rate/database_connection'
5
+
6
+ module ExchangeRate
7
+ ##
8
+ # A currency/rate/date tuple. Represents the
9
+ # value of the currency on a specific date, in Euro
10
+ # Examples
11
+ #
12
+ # CurrencyRate.new(currency = 'GBP',
13
+ # value_in_euro = 0.001,
14
+ # date_of_rate = Date.parse('2019-03-30'))
15
+ #
16
+ class CurrencyRate < Sequel::Model(DatabaseConnection.connection[:currency_rates])
17
+ ##
18
+ # :attr_writer: currency
19
+ # The currency short-code String
20
+
21
+ ##
22
+ # :attr_writer: value_in_euro
23
+ # The value of one unit of the currency in Euro
24
+
25
+ ##
26
+ # :attr_writer: date_of_rate
27
+ # The effective date of this FX rate for the currency
28
+
29
+ def validate
30
+ super
31
+ validates_presence %i[currency value_in_euro date_of_rate]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+ require 'sequel/extensions/migration'
5
+ require 'yaml'
6
+
7
+ module ExchangeRate
8
+ ##
9
+ # Database connection management for ::ExchangeRate.
10
+ class DatabaseConnection
11
+ ##
12
+ # Establish a connection to the database, and ensure the schema is
13
+ # loaded. The connection is cached and kept open.
14
+ #
15
+ # Returns nothing
16
+ def self.connection
17
+ database_url = ExchangeRate.configuration.datastore_url
18
+ @connection ||= Sequel.connect(database_url).tap { |connection| apply_migrations(connection) }
19
+ end
20
+
21
+ ##
22
+ # Closes any open connections to the database and removes the cached connection.
23
+ def self.disconnect
24
+ @connection&.disconnect
25
+ @connection = nil
26
+ end
27
+
28
+ ##
29
+ # Apply the schema definitions to the database.
30
+ #
31
+ # Returns nothing
32
+ def self.apply_migrations(connection)
33
+ return if connection.nil?
34
+ Sequel::Migrator.run(connection, 'lib/exchange_rate/db/migrate/')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCurrencyRates < Sequel::Migration
4
+ def up
5
+ create_table(:currency_rates) do |_t|
6
+ primary_key :id
7
+ String :currency, null: false
8
+ BigDecimal :value_in_euro, null: false
9
+ Date :date_of_rate, null: false
10
+
11
+ index %i[currency date_of_rate], unique: true
12
+ end
13
+ end
14
+
15
+ def down
16
+ self << 'DROP TABLE currency_rates'
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+
5
+ # ActiveRecord-style migrations
6
+ Sequel.extension :migration
7
+
8
+ # Add helpers for when we're not sure if a record exists or not
9
+ Sequel::Model.plugin :update_or_create
10
+
11
+ # Adds ActiveRecord-style #validates_presence helpers
12
+ Sequel::Model.plugin :validation_helpers
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExchangeRate
4
+ ##
5
+ # Represents a cache-miss when requesting an FX
6
+ # rate.
7
+ class MissingRateError < StandardError; end
8
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Module Methods for ExchangeRate.
5
+ #
6
+ #--
7
+ # These methods are here because the configuration methods are needed in other classes which are
8
+ # required above the ExchangeRate module definition. It felt cleaner to move the module methods
9
+ # here than shift the module definition above the require statements.
10
+ #
11
+ # Note: this defines methods on the module rather than providing a mixin because the latter still
12
+ # required the mixin to be included in the module of any class which referenced it. That might be
13
+ # cleaner in the long run.
14
+ #++
15
+ module ExchangeRate
16
+ ##
17
+ # Retrieve the remote FX rate feed and cache locally.
18
+ #
19
+ # Returns nothing.
20
+ #
21
+ # Raises ExchangeRate::RetrievalFailedError if the retrieval fails or the cache cannot be updated.
22
+ def self.retrieve
23
+ configuration.rate_retriever.save!
24
+ nil
25
+ # We expect all rate retrievers to be nice and return ExchangeRate::RetrievalFailed
26
+ # but we'll catch everything here to safeguard against custom providers' errors
27
+ # bubbling up to the caller
28
+ rescue StandardError
29
+ raise ExchangeRate::RetrievalFailedError
30
+ end
31
+
32
+ ##
33
+ # Converts from from_currency to to_currency using the FX rate
34
+ # valid on the date_of_rate.
35
+ #
36
+ # date_of_rate - The Date to use for conversion
37
+ # from_currency - The source currency code String
38
+ # from_currency - The target currency code String
39
+ #
40
+ # Examples
41
+ #
42
+ # ExchangeRate.at(Date.today,'GBP','USD')
43
+ # # => 0.12883019198912e1
44
+ #
45
+ # Returns the value of 1 unit of from_currency in to_currency.
46
+ #
47
+ # Raises ExchangeRate::MissingRateError if the local cache cannot be accessed or the value has not been cached.
48
+ def self.at(date_of_rate, from_currency, to_currency)
49
+ ExchangeRate::CurrencyConverter.new(date_of_rate, from_currency, to_currency).convert!
50
+ rescue StandardError
51
+ raise ExchangeRate::MissingRateError
52
+ end
53
+
54
+ ##
55
+ # The configuration object used by this module.
56
+ #
57
+ # See ExchangeRate::Configuration.
58
+ #
59
+ # Returns a singleton ExchangeRate::Configuration global configuration object.
60
+ def self.configuration
61
+ @configuration ||= Configuration.new
62
+ end
63
+
64
+ ##
65
+ # Set the configuration object used by this module.
66
+ #
67
+ # See ExchangeRate::Configuration.
68
+ def self.configuration=(configuration)
69
+ @configuration = configuration
70
+ end
71
+
72
+ ##
73
+ # Configures the ExchangeRate module with the provided block.
74
+ #
75
+ # See ExchangeRate::Configuration.
76
+ #
77
+ # Examples
78
+ #
79
+ # ExchangeRate.configure do |configuration|
80
+ # configuration.rate_retriever = CustomRateRetriever.new
81
+ # end
82
+ #
83
+ # Returns nothing
84
+ def self.configure
85
+ yield(configuration)
86
+ end
87
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExchangeRate
4
+ ##
5
+ # Rate retrieval classes.
6
+ #
7
+ # See ExchangeRate::RateSources::ECBRateRetriever for an example implementation.
8
+ module RateSources; end
9
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/conversions.rb'
4
+ require 'net/http'
5
+ require 'sequel/exceptions'
6
+ require 'uri'
7
+
8
+ module ExchangeRate
9
+ module RateSources
10
+ ##
11
+ # Retrieves FX rates from the 90 day European Central Bank (ECB) feed.
12
+ class ECBRateRetriever
13
+ ##
14
+ # The ECB feed URL
15
+ DEFAULT_FEED_URL = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'
16
+
17
+ ##
18
+ # Builds a new ECBRateRetriever.
19
+ #
20
+ # - feed_url - The URL to request the ECB feed from.
21
+ def initialize(feed_url: DEFAULT_FEED_URL)
22
+ @feed_url = feed_url
23
+ end
24
+
25
+ ##
26
+ # Retrieve the feed, parse the contents, and update the local cache.
27
+ #
28
+ # Returns nothing
29
+ # Raises ExchangeRate::RetrievalFailedError if the feed could not be loaded
30
+ # Raises ExchangeRate::RetrievalFailedError if the cache could not be updated
31
+ #--
32
+ # TODO: This makes 1 database call per record to be saved. A bulk-insert would speed
33
+ # things up considerably.
34
+ #++
35
+ def save!
36
+ retrieve_feed
37
+ .yield_self { |feed_body| parse_feed(feed_body) }
38
+ .map { |rate_date_hash| rates_from(rate_date_hash, date_from(rate_date_hash)) }
39
+ .flatten
40
+ .map(&:save)
41
+ nil
42
+ rescue StandardError
43
+ raise ExchangeRate::RetrievalFailedError
44
+ end
45
+
46
+ private
47
+
48
+ ##
49
+ # Perform the HTTP request to grab the feed contents.
50
+ #
51
+ # Returns the response body as a String.
52
+ def retrieve_feed
53
+ uri = URI.parse(@feed_url)
54
+ Net::HTTP.get_response(uri).body
55
+ end
56
+
57
+ ##
58
+ # Parse the ECB feed XML into an array of feed ::Hash objects.
59
+ #
60
+ # feed_body - The XML ECB feed document as a String.
61
+ #
62
+ # Returns an array of currency/date/rate ::Hash objects, grouped by date (see #rate_array_from).
63
+ def parse_feed(feed_body)
64
+ Hash.from_xml(feed_body)
65
+ .yield_self { |feed_hash| rate_array_from(feed_hash) }
66
+ end
67
+
68
+ ##
69
+ # Extract the array of FX rates from the XML document.
70
+ #
71
+ # feed_hash - A ::Hash representation of the XML ECB feed document.
72
+ #
73
+ # Returns An array of date/rate/currency ::Hash objects, grouped by date.
74
+ def rate_array_from(feed_hash)
75
+ feed_hash['Envelope']['Cube']['Cube']
76
+ end
77
+
78
+ ##
79
+ # Extract the effective date of an FX rate.
80
+ #
81
+ # rate_date_hash - A single currency/date/rate ::Hash object.
82
+ #
83
+ # Returns the effective ::Date of the FX rate.
84
+ def date_from(rate_date_hash)
85
+ Date.parse(rate_date_hash['time'])
86
+ end
87
+
88
+ ##
89
+ # Builds an array of ExchangeRate::CurrencyRate objects.
90
+ #
91
+ # rate_date_array - An array of currency/rate ::Hash objects
92
+ # rate_effective_date - The effective ::Date for FX rates in the rate_date_hash.
93
+ def rates_from(rate_date_array, rate_effective_date)
94
+ rate_date_array['Cube'].map do |currency_rate_hash|
95
+ create_or_update_currency_rate(currency: currency_rate_hash['currency'],
96
+ value_in_euro: currency_rate_hash['rate'].to_f,
97
+ date_of_rate: rate_effective_date)
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Creates or updates the local cache of the currency/rate/date tuple
103
+ #
104
+ # currency - The currency short-code, e.g. 'GBP'.
105
+ # value_in_euro - The value of one unit (e.g. $1) in Euro.
106
+ # date_of_rate - The date this value applies to.
107
+ #
108
+ # Returns true if the record saved
109
+ # Raises ExchangeRate::RetrievalFailedError if the record could not be saved
110
+ def create_or_update_currency_rate(currency:, value_in_euro:, date_of_rate:)
111
+ rate = CurrencyRate.find_or_new(currency: currency, date_of_rate: date_of_rate)
112
+ rate.value_in_euro = value_in_euro
113
+ rate.tap(&:save)
114
+ rescue Sequel::ValidationFailed, Sequel::HookFailed
115
+ raise ExchangeRate::RetrievalFailedError
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExchangeRate
4
+ ##
5
+ # An error raised during rate feed retrieval
6
+ class RetrievalFailedError < StandardError; end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExchangeRate
4
+ ##
5
+ # The module version. Uses {Semantic Versioning}[https://www.semver.org/spec/v2.0.0.html]
6
+ VERSION = '1.0.0'
7
+ end
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fx_exchange_rate
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Brooks
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: database_cleaner
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.7.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.58.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.58.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.16.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.16.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 4.0.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 4.0.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 3.4.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 3.4.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.11.3
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.11.3
139
+ - !ruby/object:Gem::Dependency
140
+ name: activesupport
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 5.2.1
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 5.2.1
153
+ - !ruby/object:Gem::Dependency
154
+ name: sequel
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 5.11.0
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 5.11.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: sqlite3
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 1.3.13
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 1.3.13
181
+ description: ExchangeRate uses a cache for FX rates so you are not dependent on a
182
+ constant connection to your FX rate provider of choice, and allow custom FX rate
183
+ providers to be added with ease.
184
+ email:
185
+ - ryan@ryanbrooks.co.uk
186
+ executables: []
187
+ extensions: []
188
+ extra_rdoc_files: []
189
+ files:
190
+ - ".gitignore"
191
+ - ".rdoc_options"
192
+ - ".rspec"
193
+ - ".rubocop.yml"
194
+ - ".ruby-version"
195
+ - ".travis.yml"
196
+ - CODE_OF_CONDUCT.md
197
+ - Gemfile
198
+ - Gemfile.lock
199
+ - LICENSE.txt
200
+ - README.md
201
+ - Rakefile
202
+ - bin/console
203
+ - bin/setup
204
+ - fixtures/vcr_cassettes/ExchangeRate/_retrieve/fetches_the_feed_and_stores_the_values_locally.yml
205
+ - fixtures/vcr_cassettes/ExchangeRate/_retrieve/overwrites_existing_values.yml
206
+ - fixtures/vcr_cassettes/ExchangeRate/_retrieve/raises_ExchangeRate_RetrievalFailed_when_the_the_feed_cannot_be_retrieved.yml
207
+ - fixtures/vcr_cassettes/ExchangeRate_RateSources_ECBRateRetriever/_save_/raises_when_the_feed_content_is_invalid.yml
208
+ - fixtures/vcr_cassettes/ExchangeRate_RateSources_ECBRateRetriever/_save_/retrieves_the_feed.yml
209
+ - fx_exchange_rate.gemspec
210
+ - lib/exchange_rate.rb
211
+ - lib/exchange_rate/configuration.rb
212
+ - lib/exchange_rate/currency_converter.rb
213
+ - lib/exchange_rate/currency_rate.rb
214
+ - lib/exchange_rate/database_connection.rb
215
+ - lib/exchange_rate/db/migrate/20180827184400_create_currency_rates.rb
216
+ - lib/exchange_rate/initializers/sequel.rb
217
+ - lib/exchange_rate/missing_rate_error.rb
218
+ - lib/exchange_rate/module_methods.rb
219
+ - lib/exchange_rate/rate_sources.rb
220
+ - lib/exchange_rate/rate_sources/e_c_b_rate_retriever.rb
221
+ - lib/exchange_rate/retrieval_failed_error.rb
222
+ - lib/exchange_rate/version.rb
223
+ homepage:
224
+ licenses:
225
+ - MIT
226
+ metadata: {}
227
+ post_install_message:
228
+ rdoc_options: []
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ required_rubygems_version: !ruby/object:Gem::Requirement
237
+ requirements:
238
+ - - ">="
239
+ - !ruby/object:Gem::Version
240
+ version: '0'
241
+ requirements: []
242
+ rubyforge_project:
243
+ rubygems_version: 2.7.6
244
+ signing_key:
245
+ specification_version: 4
246
+ summary: Retrieve and cache historical foreign exchange rates
247
+ test_files: []