diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..df71430 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +Metrics/BlockLength: + Exclude: + - '*.gemspec' + - 'test/**/*.rb' + +Metrics/ClassLength: + Exclude: + - 'lib/money/bank/currencylayer_bank.rb' diff --git a/Gemfile b/Gemfile index da9a46e..fbabc83 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Rakefile b/Rakefile index 2dcb56b..5e7d75b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake/testtask' require 'rubocop/rake_task' require 'inch/rake' @@ -6,6 +8,7 @@ task default: [:test, :rubocop, 'doc:suggest'] Rake::TestTask.new do |t| t.pattern = 'test/**/*_test.rb' + t.warning = false end task spec: :test diff --git a/lib/money/bank/currencylayer_bank.rb b/lib/money/bank/currencylayer_bank.rb index 618b9dd..c6666e8 100644 --- a/lib/money/bank/currencylayer_bank.rb +++ b/lib/money/bank/currencylayer_bank.rb @@ -1,10 +1,26 @@ # encoding: UTF-8 +# frozen_string_literal: true + require 'open-uri' require 'money' require 'json' # Money gem class class Money + # Build in memory rates store + module RatesStore + # Memory class + class Memory + # Add method to reset the build in memory store + # @param [Hash] rt Optional initial exchange rate data. + # + # @return [Object] store. + def reset!(rt = {}) + transaction { @index = rt } + end + end + end + # https://github.com/RubyMoney/money#exchange-rate-stores module Bank # Invalid cache, file not found or cache empty @@ -14,14 +30,13 @@ class InvalidCache < StandardError; end class NoAccessKey < StandardError; end # CurrencylayerBank base class - # rubocop:disable Metrics/ClassLength class CurrencylayerBank < Money::Bank::VariableExchange # CurrencylayerBank url - CL_URL = 'http://apilayer.net/api/live'.freeze + CL_URL = 'http://apilayer.net/api/live' # CurrencylayerBank secure url - CL_SECURE_URL = CL_URL.gsub('http:', 'https:').freeze + CL_SECURE_URL = CL_URL.gsub('http:', 'https:') # Default base currency - CL_SOURCE = 'USD'.freeze + CL_SOURCE = 'USD' # Use https to fetch rates from CurrencylayerBank # CurrencylayerBank only allows http as connection @@ -49,6 +64,10 @@ class CurrencylayerBank < Money::Bank::VariableExchange # Parsed CurrencylayerBank result as Hash attr_reader :rates + # Get the timestamp of rates in memory + # @return [Time] time object or nil + attr_reader :rates_mem_timestamp + # Set the seconds after than the current rates are automatically expired # by default, they never expire. # @@ -90,6 +109,7 @@ def ttl_in_seconds # Update all rates from CurrencylayerBank JSON # @return [Array] array of exchange rates def update_rates(straight = false) + store.reset! exchange_rates(straight).each do |exchange_rate| currency = exchange_rate.first[3..-1] rate = exchange_rate.last @@ -97,11 +117,23 @@ def update_rates(straight = false) add_rate(source, currency, rate) add_rate(currency, source, 1.0 / rate) end + @rates_mem_timestamp = rates_timestamp + end + + # Override Money `add_rate` method for caching + # @param [String] from_currency Currency ISO code. ex. 'USD' + # @param [String] to_currency Currency ISO code. ex. 'CAD' + # @param [Numeric] rate Rate to use when exchanging currencies. + # + # @return [Numeric] rate. + def add_rate(from_currency, to_currency, rate) + super end # Override Money `get_rate` method for caching # @param [String] from_currency Currency ISO code. ex. 'USD' # @param [String] to_currency Currency ISO code. ex. 'CAD' + # @param [Hash] opts Options hash to set special parameters. # # @return [Numeric] rate. def get_rate(from_currency, to_currency, opts = {}) # rubocop:disable all @@ -135,12 +167,15 @@ def get_rate(from_currency, to_currency, opts = {}) # rubocop:disable all rate end - # Fetch new rates if cached rates are expired + # Fetch new rates if cached rates are expired or stale # @return [Boolean] true if rates are expired and updated from remote def expire_rates! if expired? update_rates(true) true + elsif stale? + update_rates + true else false end @@ -152,6 +187,14 @@ def expired? Time.now > rates_expiration end + # Check if rates are stale + # Stale is true if rates are updated straight by another thread. + # The actual thread has always old rates in memory store. + # @return [Boolean] true if rates are stale + def stale? + rates_timestamp != rates_mem_timestamp + end + # Source url of CurrencylayerBank # defined with access_key and secure_connection # @return [String] the remote API url diff --git a/money-currencylayer-bank.gemspec b/money-currencylayer-bank.gemspec index 2b6e488..9bd7db1 100644 --- a/money-currencylayer-bank.gemspec +++ b/money-currencylayer-bank.gemspec @@ -1,6 +1,8 @@ +# frozen_string_literal: true + Gem::Specification.new do |s| s.name = 'money-currencylayer-bank' - s.version = '0.5.3' + s.version = '0.5.4' s.date = Time.now.utc.strftime('%Y-%m-%d') s.homepage = "http://github.com/phlegx/#{s.name}" s.authors = ['Egon Zemmer'] @@ -9,7 +11,7 @@ Gem::Specification.new do |s| 'rates from currencylayer.com. Compatible with the money gem.' s.summary = 'A gem that calculates the exchange rate using published rates ' \ 'from currencylayer.com.' - s.extra_rdoc_files = %w(README.md) + s.extra_rdoc_files = %w[README.md] s.files = Dir['LICENSE', 'README.md', 'Gemfile', 'lib/**/*.rb', 'test/**/*'] s.license = 'MIT' @@ -19,12 +21,12 @@ Gem::Specification.new do |s| s.rubygems_version = '1.3.7' s.add_dependency 'money', '~> 6.7' s.add_dependency 'monetize', '~> 1.4' - s.add_dependency 'json', '~> 1.8' + s.add_dependency 'json', '>= 1.8' s.add_development_dependency 'minitest', '~> 5.8' s.add_development_dependency 'minitest-line', '~> 0.6' s.add_development_dependency 'rr', '~> 1.1' - s.add_development_dependency 'rake', '~>10.4' + s.add_development_dependency 'rake', '~>12.0' s.add_development_dependency 'timecop', '~>0.8.1' - s.add_development_dependency 'rubocop', '~>0.37.2' + s.add_development_dependency 'rubocop', '~>0.48.1' s.add_development_dependency 'inch', '~>0.7.1' end diff --git a/test/currencylayer_bank_test.rb b/test/currencylayer_bank_test.rb index 581ac8a..ac55089 100644 --- a/test/currencylayer_bank_test.rb +++ b/test/currencylayer_bank_test.rb @@ -1,4 +1,6 @@ # encoding: UTF-8 +# frozen_string_literal: true + require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) describe Money::Bank::CurrencylayerBank do @@ -208,9 +210,12 @@ delimiter: ',' } Money::Currency.register(wtf) - subject.add_rate('USD', 'WTF', 2) - subject.add_rate('WTF', 'USD', 2) - subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0 + Timecop.freeze(subject.rates_timestamp) do + subject.add_rate('USD', 'WTF', 2) + subject.add_rate('WTF', 'USD', 2) + subject.exchange_with(5000.to_money('WTF'), 'USD').cents + subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0 + end end end @@ -232,9 +237,10 @@ @old_usd_eur_rate = 0.655 # see test/live.json +54 @new_usd_eur_rate = 0.886584 - subject.add_rate('USD', 'EUR', @old_usd_eur_rate) subject.cache = temp_cache_path stub(subject).source_url { data_path } + subject.update_rates + subject.add_rate('USD', 'EUR', @old_usd_eur_rate) end after do diff --git a/test/test_helper.rb b/test/test_helper.rb index d7ac273..708a378 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,6 @@ # encoding: UTF-8 +# frozen_string_literal: true + require 'minitest/autorun' require 'rr' require 'money/bank/currencylayer_bank'