Skip to content

Commit

Permalink
Update 1.1.0 (#16)
Browse files Browse the repository at this point in the history
Version 1.1.0:

- Refactoring
- Extracted RussianCentralBankFetcher
- Introduced FetchError
- Switched to use XML feed
- HTTParty
- Integration specs
- CircleCI 2.0
  • Loading branch information
rmustafin authored Mar 29, 2019
1 parent 563586d commit 87955f9
Show file tree
Hide file tree
Showing 16 changed files with 501 additions and 249 deletions.
44 changes: 44 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: 2
jobs:
build:
docker:
- image: circleci/ruby:2.4.2
environment:
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
BUNDLE_PATH: vendor/bundle
steps:
- checkout

# Which version of bundler?
- run:
name: Which bundler?
command: bundle -v

# Restore bundle cache
- restore_cache:
keys:
- russian_central_bank-{{ checksum "russian_central_bank.gemspec" }}
- russian_central_bank

- run:
name: Bundle Install
command: bundle check || bundle install

# Store bundle cache
- save_cache:
key: russian_central_bank-{{ checksum "russian_central_bank.gemspec" }}
paths:
- vendor/bundle

# Run rspec
- run:
name: Run rspec
command: |
gem install rspec && \
bundle exec rspec --format progress \
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
# Save test results for timing analysis
- store_test_results:
path: test_results
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
Expand Down
8 changes: 8 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Metrics/BlockLength:
Enabled: true
Exclude:
- spec/**/*

Metrics/LineLength:
Enabled: true
Max: 120
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,46 @@ Or install it yourself as:

$ gem install russian_central_bank

NOTE: use 0.x version of `russian_central_bank` for > 6.0 `money` versions
NOTE: use 0.x version of `russian_central_bank` for `money` versions < 6.0

##Dependencies
## Dependencies

* [savon](http://savonrb.com/)
* [httparty](https://github.com/jnunemaker/httparty)
* [money](https://github.com/RubyMoney/money)

## Usage

Regular usage
### Regular usage

require 'russian_central_bank'

Money.locale_backend = :currency
bank = Money::Bank::RussianCentralBank.new

Money.default_bank = bank

# Load today's rates
bank.update_rates

# Exchange 100 USD to RUB
Money.new(1000, "USD").exchange_to('RUB')
# Exchange 1000 USD to RUB
Money.new(1000_00, "USD").exchange_to('RUB').format # => 64.592,50 ₽

Specific date rates
# Use indirect exchange rates, USD -> RUB -> EUR
Money.new(1000_00, "USD").exchange_to('EUR').format # => €888,26

### Specific date rates

# Specify rates date
bank.update_rates(Date.today - 3000)
Money.new(1000, "USD").exchange_to('RUB') # makes you feel better
bank.update_rates(Date.new(2010, 12, 31))
Money.new(1000_00, "USD").exchange_to('RUB').format # => 30.476,90 ₽

# Check last rates update
bank.rates_updated_at

# Check on which date rates were updated
bank.rates_updated_on

Autoupdate
### Autoupdate

# Use ttl attribute to enable rates autoupdate
bank.ttl = 1.day
Expand All @@ -64,14 +68,14 @@ Autoupdate

### Safe rates fetch

There are some cases, when the `cbr.ru` returns HTTP 302.
There are some cases, when the `cbr.ru` doesn't return HTTP 200.
To avoid issues in production, you use fallback:

```ruby
bank = Money::Bank::RussianCentralBank.new
begin
bank.update_rates
rescue Money::Bank::RussianCentralBank::FetchError => e
rescue Money::Bank::RussianCentralBankFetcher::FetchError => e
Rails.logger.info "CBR failed: #{e.response}"

## fallback
Expand Down
85 changes: 85 additions & 0 deletions lib/money/bank/russian_central_bank.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require 'money'

class Money
module Bank
class RussianCentralBank < Money::Bank::VariableExchange
attr_reader :rates_updated_at, :rates_updated_on, :rates_expired_at, :ttl

def flush_rates
@store = Money::RatesStore::Memory.new
end

def update_rates(date = Date.today)
store.transaction do
update_parsed_rates(exchange_rates(date))
@rates_updated_at = Time.now
@rates_updated_on = date
update_expired_at
store.send(:index)
end
end

def add_rate(from, to, rate)
super(from, to, rate)
super(to, from, 1.0 / rate)
end

def get_rate(from, to)
update_rates if rates_expired?
super || indirect_rate(from, to)
end

def ttl=(value)
@ttl = value
update_expired_at
@ttl
end

def rates_expired?
rates_expired_at && rates_expired_at <= Time.now
end

private

def fetcher
@fetcher ||= RussianCentralBankFetcher.new
end

def exchange_rates(date)
fetcher.perform(date)
end

def update_expired_at
@rates_expired_at = if ttl
@rates_updated_at ? @rates_updated_at + ttl : Time.now
else
nil
end
end

def indirect_rate(from, to)
get_rate('RUB', to) / get_rate('RUB', from)
end

def local_currencies
@local_currencies ||= Money::Currency.table.map { |currency| currency.last[:iso_code] }
end

def update_parsed_rates(rates)
add_rate('RUB', 'RUB', 1)
rates.each do |rate|
begin
if local_currencies.include?(rate[:code])
add_rate(
'RUB',
rate[:code],
1 / (rate[:value] / rate[:nominal])
)
end
rescue Money::Currency::UnknownCurrency
end
end
end
end
end
end
49 changes: 49 additions & 0 deletions lib/money/bank/russian_central_bank_fetcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'httparty'

class Money
module Bank
class RussianCentralBankFetcher
DAILY_RATES_URL = 'http://www.cbr.ru/scripts/XML_daily.asp'.freeze

class FetchError < StandardError
attr_reader :response

def initialize(message, response = nil)
super(message)
@response = response
end
end

def perform(date = Date.today)
response = HTTParty.get(rates_url(date))
unless response.success?
raise_fetch_error("cbr.ru respond with #{response.code}", response)
end
extract_rates(response.parsed_response)
rescue HTTParty::Error => e
raise_fetch_error(e.message)
end

private

def raise_fetch_error(message, response = nil)
raise FetchError.new(message, response)
end

def rates_url(date)
"#{DAILY_RATES_URL}?date_req=#{date.strftime('%d/%m/%Y')}"
end

def extract_rates(parsed_response)
rates_arr = parsed_response['ValCurs']['Valute']
rates_arr.map do |rate|
{
code: rate['CharCode'],
nominal: rate['Nominal'].to_i,
value: rate['Value'].tr(',', '.').to_f
}
end
end
end
end
end
95 changes: 2 additions & 93 deletions lib/russian_central_bank.rb
Original file line number Diff line number Diff line change
@@ -1,93 +1,2 @@
require 'money'
require 'savon'

class Money
module Bank
class RussianCentralBank < Money::Bank::VariableExchange
class FetchError < StandardError
attr_reader :response

def initialize(message, response=nil)
super(message)
@response = response
end
end

CBR_SERVICE_URL = 'http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx?WSDL'

attr_reader :rates_updated_at, :rates_updated_on, :ttl, :rates_expired_at

def flush_rates
@store = Money::RatesStore::Memory.new
end

def update_rates(date = Date.today)
store.transaction do
update_parsed_rates(exchange_rates(date))
@rates_updated_at = Time.now
@rates_updated_on = date
update_expired_at
store.send(:index)
end
end

def add_rate(from, to, rate)
super(from, to, rate)
super(to, from, 1.0 / rate)
end

def get_rate(from, to)
update_rates if rates_expired?
super || indirect_rate(from, to)
end

def ttl=(value)
@ttl = value
update_expired_at
@ttl
end

def rates_expired?
rates_expired_at && rates_expired_at <= Time.now
end

private

def update_expired_at
@rates_expired_at = if ttl
@rates_updated_at ? @rates_updated_at + ttl : Time.now
else
nil
end
end

def indirect_rate(from, to)
from_base_rate = get_rate('RUB', from)
to_base_rate = get_rate('RUB', to)
to_base_rate / from_base_rate
end

def exchange_rates(date = Date.today)
client = Savon::Client.new wsdl: CBR_SERVICE_URL, log: false, log_level: :error,
follow_redirects: true
response = client.call(:get_curs_on_date, message: { 'On_date' => date.strftime('%Y-%m-%dT%H:%M:%S') })
response.body[:get_curs_on_date_response][:get_curs_on_date_result][:diffgram][:valute_data][:valute_curs_on_date]
rescue Wasabi::Resolver::HTTPError => e
raise FetchError.new(e.message, e.response)
end

def update_parsed_rates(rates)
local_currencies = Money::Currency.table.map { |currency| currency.last[:iso_code] }
add_rate('RUB', 'RUB', 1)
rates.each do |rate|
begin
if local_currencies.include? rate[:vch_code]
add_rate('RUB', rate[:vch_code], 1/ (rate[:vcurs].to_f / rate[:vnom].to_i))
end
rescue Money::Currency::UnknownCurrency
end
end
end
end
end
end
require 'money/bank/russian_central_bank_fetcher'
require 'money/bank/russian_central_bank'
11 changes: 7 additions & 4 deletions russian_central_bank.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)

Gem::Specification.new do |spec|
spec.name = 'russian_central_bank'
spec.version = '1.0.1'
spec.version = '1.1.0'
spec.authors = ['Ramil Mustafin']
spec.email = ['[email protected]']
spec.description = 'RussianCentralBank extends Money::Bank::VariableExchange and gives you access to the Central Bank of Russia currency exchange rates.'
Expand All @@ -17,10 +17,13 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']

spec.add_development_dependency 'bundler', '~>1.3'
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rspec', '~>3'
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'webmock', '~>3'

spec.add_dependency 'money', '~> 6.7.0'
spec.add_dependency 'savon', '~>2.0'
spec.add_dependency 'httparty', '~>0'
spec.add_dependency 'money', '~> 6'
end
Loading

0 comments on commit 87955f9

Please sign in to comment.