currency-rate 0.1.1

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +11 -0
  5. data/Gemfile.lock +91 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +52 -0
  8. data/Rakefile +24 -0
  9. data/VERSION +1 -0
  10. data/currency-rate.gemspec +95 -0
  11. data/lib/adapter.rb +42 -0
  12. data/lib/btc_adapter.rb +17 -0
  13. data/lib/btc_adapters/average_rate_adapter.rb +50 -0
  14. data/lib/btc_adapters/bitpay_adapter.rb +18 -0
  15. data/lib/btc_adapters/bitstamp_adapter.rb +14 -0
  16. data/lib/btc_adapters/btce_adapter.rb +14 -0
  17. data/lib/btc_adapters/coinbase_adapter.rb +13 -0
  18. data/lib/btc_adapters/kraken_adapter.rb +14 -0
  19. data/lib/btc_adapters/localbitcoins_adapter.rb +13 -0
  20. data/lib/btc_adapters/okcoin_adapter.rb +14 -0
  21. data/lib/core_ext/classify.rb +7 -0
  22. data/lib/core_ext/deep_get.rb +11 -0
  23. data/lib/currency_rate.rb +49 -0
  24. data/lib/fiat_adapter.rb +30 -0
  25. data/lib/fiat_adapters/fixer_adapter.rb +11 -0
  26. data/lib/fiat_adapters/yahoo_adapter.rb +22 -0
  27. data/lib/storage.rb +17 -0
  28. data/spec/currency_rate_spec.rb +28 -0
  29. data/spec/lib/btc_adapter_spec.rb +55 -0
  30. data/spec/lib/btc_adapters/average_rate_adapter_spec.rb +51 -0
  31. data/spec/lib/btc_adapters/bitpay_adapter_spec.rb +35 -0
  32. data/spec/lib/btc_adapters/bitstamp_adapter_spec.rb +35 -0
  33. data/spec/lib/btc_adapters/btce_adapter_spec.rb +35 -0
  34. data/spec/lib/btc_adapters/coinbase_adapter_spec.rb +35 -0
  35. data/spec/lib/btc_adapters/kraken_adapter_spec.rb +35 -0
  36. data/spec/lib/btc_adapters/localbitcoins_adapter_spec.rb +35 -0
  37. data/spec/lib/btc_adapters/okcoin_adapter_spec.rb +35 -0
  38. data/spec/lib/fiat_adapters/fixer_adapter_spec.rb +22 -0
  39. data/spec/lib/fiat_adapters/yahoo_adapter_spec.rb +22 -0
  40. data/spec/lib/storage_spec.rb +25 -0
  41. data/spec/spec_helper.rb +13 -0
  42. metadata +169 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f65d7412d5bdf082fb39bc670010cf88aaf83ace
4
+ data.tar.gz: 7fc7e9af03abb1747d6f1fc01c6f880d64433a31
5
+ SHA512:
6
+ metadata.gz: 1844ba52a93f9cdf1d5c3d6b2ecc7b8516ef8678f32771499fdb3c3c52785b3c5ed2dd3d9b1215e38256308ad19a58bc4e85d05cd87eab7cb9e92ed9c4586ac9
7
+ data.tar.gz: bc1a32af222d5555c8b7d1fe6cac660df64a512dee62c6ce7fd6d8995b7abd29bea28169edfbbe5dc8a6c17b5b81399143e57100eb73f6d85ac699aaf31ec632
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'satoshi-unit'
4
+
5
+ group :development do
6
+ gem "bundler", "~> 1.0"
7
+ gem "jeweler", "~> 2.1.1", git: 'git@github.com:technicalpickles/jeweler.git'
8
+ gem "rspec"
9
+ gem 'webmock'
10
+ gem 'vcr'
11
+ end
@@ -0,0 +1,91 @@
1
+ GIT
2
+ remote: git@github.com:technicalpickles/jeweler.git
3
+ revision: ec91e05d06f2835ee30568fa42b5fa7d451444b6
4
+ specs:
5
+ jeweler (2.1.1)
6
+ builder
7
+ bundler (>= 1.0)
8
+ git (>= 1.2.5)
9
+ github_api
10
+ highline (>= 1.6.15)
11
+ nokogiri (>= 1.5.10)
12
+ rake
13
+ rdoc
14
+ semver
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ addressable (2.4.0)
20
+ builder (3.2.2)
21
+ crack (0.4.3)
22
+ safe_yaml (~> 1.0.0)
23
+ descendants_tracker (0.0.4)
24
+ thread_safe (~> 0.3, >= 0.3.1)
25
+ diff-lcs (1.2.5)
26
+ faraday (0.9.2)
27
+ multipart-post (>= 1.2, < 3)
28
+ git (1.3.0)
29
+ github_api (0.14.0)
30
+ addressable (~> 2.4.0)
31
+ descendants_tracker (~> 0.0.4)
32
+ faraday (~> 0.8, < 0.10)
33
+ hashie (>= 3.4)
34
+ oauth2
35
+ hashdiff (0.3.0)
36
+ hashie (3.4.4)
37
+ highline (1.7.8)
38
+ json (1.8.3)
39
+ jwt (1.5.1)
40
+ mini_portile2 (2.0.0)
41
+ multi_json (1.12.1)
42
+ multi_xml (0.5.5)
43
+ multipart-post (2.0.0)
44
+ nokogiri (1.6.7.2)
45
+ mini_portile2 (~> 2.0.0.rc2)
46
+ oauth2 (1.1.0)
47
+ faraday (>= 0.8, < 0.10)
48
+ jwt (~> 1.0, < 1.5.2)
49
+ multi_json (~> 1.3)
50
+ multi_xml (~> 0.5)
51
+ rack (>= 1.2, < 3)
52
+ rack (1.6.4)
53
+ rake (11.1.2)
54
+ rdoc (4.2.2)
55
+ json (~> 1.4)
56
+ rspec (3.4.0)
57
+ rspec-core (~> 3.4.0)
58
+ rspec-expectations (~> 3.4.0)
59
+ rspec-mocks (~> 3.4.0)
60
+ rspec-core (3.4.4)
61
+ rspec-support (~> 3.4.0)
62
+ rspec-expectations (3.4.0)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.4.0)
65
+ rspec-mocks (3.4.1)
66
+ diff-lcs (>= 1.2.0, < 2.0)
67
+ rspec-support (~> 3.4.0)
68
+ rspec-support (3.4.1)
69
+ safe_yaml (1.0.4)
70
+ satoshi-unit (0.1.8)
71
+ semver (1.0.1)
72
+ thread_safe (0.3.5)
73
+ vcr (3.0.3)
74
+ webmock (2.0.3)
75
+ addressable (>= 2.3.6)
76
+ crack (>= 0.3.2)
77
+ hashdiff
78
+
79
+ PLATFORMS
80
+ ruby
81
+
82
+ DEPENDENCIES
83
+ bundler (~> 1.0)
84
+ jeweler (~> 2.1.1)!
85
+ rspec
86
+ satoshi-unit
87
+ vcr
88
+ webmock
89
+
90
+ BUNDLED WITH
91
+ 1.12.0
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Roman Snitko
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,52 @@
1
+ currency-rate
2
+ =============
3
+
4
+ Converter for fiat and crypto currencies
5
+
6
+ Installation
7
+ ------------
8
+
9
+ gem install currency-rate
10
+
11
+ or in Gemfile
12
+
13
+ gem "currency-rate"
14
+
15
+ Usage
16
+ -----
17
+ Basically, all you'll need is to use the top level class to fetch any rates you want.
18
+ For example:
19
+
20
+ CurrencyRate.convert('Bitstamp', amount: 5, from: 'BTC', to: 'USD')
21
+ CurrencyRate.convert('Bitstamp', amount: 2750, from: 'USD', to: 'BTC')
22
+ CurrencyRate.convert('Bitstamp', amount: 1000, from: 'USD', to: 'EUR')
23
+
24
+ In the third case, because Bitstamp doesn't really support direct conversion from
25
+ USD to EUR, the 1000 will first be converted to BTC, then the BTC amount will be converted to EUR.
26
+
27
+ This introduced the concept of anchor currency. For all Btc adapters in this lib, it's set to BTC
28
+ by default. For all Fiat adapters it's set to USD by default. To specify anchor currency manually,
29
+ simply pass it as another argument, for example:
30
+
31
+ CurrencyRate.convert('Bitstamp', amount: 1000, from: 'USD', to: 'EUR', anchor_currency: 'BTC')
32
+
33
+ For a list of available adapters, please see
34
+ [lib/btc_adapters](https://github.com/snitko/currency-rate/tree/master/lib/btc_adapters)
35
+ and [lib/fiat_adapters](https://github.com/snitko/currency-rate/tree/master/lib/fiat_adapters).
36
+ To specify an adapter for `#convert` or `#get`, remove the last `Adapter` part from its name.
37
+
38
+ Caching and Storage
39
+ -------------------
40
+
41
+ By default, a simple in-memory caching mechanism with a timeout of 1800 seconds is set.
42
+ To change the default timeout and also implement your very own caching mechanism you can
43
+ try reloading `CurrencyRate::Storage` methods and writing your own functionality,
44
+ storing data in Redis, for example.
45
+
46
+ While I agree it'd be nicer to have the timeout set somewhere in `CurrencyRate` class,
47
+ for simplicity reasons I've avoided that. It also makes sense that the `Storage` class is responsible
48
+ for how and when to store/fetch data, not other classes.
49
+
50
+ Credits
51
+ -------
52
+ This gem was extracted from [straight gem](https://github.com/MyceliumGear/straight), thanks to all the people who added various exchange rate adapters and contributed code.
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "currency-rate"
18
+ gem.homepage = "http://github.com/snitko/currency-rate"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Converter for fiat and crypto currencies}
21
+ gem.description = %Q{Fetches exchange rates from various sources and does the conversion}
22
+ gem.email = "roman.snitko@gmail.com"
23
+ gem.authors = ["Roman Snitko"]
24
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,95 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: currency-rate 0.1.1 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "currency-rate"
9
+ s.version = "0.1.1"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Roman Snitko"]
14
+ s.date = "2016-06-18"
15
+ s.description = "Fetches exchange rates from various sources and does the conversion"
16
+ s.email = "roman.snitko@gmail.com"
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ "Gemfile",
25
+ "Gemfile.lock",
26
+ "LICENSE.txt",
27
+ "README.md",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "currency-rate.gemspec",
31
+ "lib/adapter.rb",
32
+ "lib/btc_adapter.rb",
33
+ "lib/btc_adapters/average_rate_adapter.rb",
34
+ "lib/btc_adapters/bitpay_adapter.rb",
35
+ "lib/btc_adapters/bitstamp_adapter.rb",
36
+ "lib/btc_adapters/btce_adapter.rb",
37
+ "lib/btc_adapters/coinbase_adapter.rb",
38
+ "lib/btc_adapters/kraken_adapter.rb",
39
+ "lib/btc_adapters/localbitcoins_adapter.rb",
40
+ "lib/btc_adapters/okcoin_adapter.rb",
41
+ "lib/core_ext/classify.rb",
42
+ "lib/core_ext/deep_get.rb",
43
+ "lib/currency_rate.rb",
44
+ "lib/fiat_adapter.rb",
45
+ "lib/fiat_adapters/fixer_adapter.rb",
46
+ "lib/fiat_adapters/yahoo_adapter.rb",
47
+ "lib/storage.rb",
48
+ "spec/currency_rate_spec.rb",
49
+ "spec/lib/btc_adapter_spec.rb",
50
+ "spec/lib/btc_adapters/average_rate_adapter_spec.rb",
51
+ "spec/lib/btc_adapters/bitpay_adapter_spec.rb",
52
+ "spec/lib/btc_adapters/bitstamp_adapter_spec.rb",
53
+ "spec/lib/btc_adapters/btce_adapter_spec.rb",
54
+ "spec/lib/btc_adapters/coinbase_adapter_spec.rb",
55
+ "spec/lib/btc_adapters/kraken_adapter_spec.rb",
56
+ "spec/lib/btc_adapters/localbitcoins_adapter_spec.rb",
57
+ "spec/lib/btc_adapters/okcoin_adapter_spec.rb",
58
+ "spec/lib/fiat_adapters/fixer_adapter_spec.rb",
59
+ "spec/lib/fiat_adapters/yahoo_adapter_spec.rb",
60
+ "spec/lib/storage_spec.rb",
61
+ "spec/spec_helper.rb"
62
+ ]
63
+ s.homepage = "http://github.com/snitko/currency-rate"
64
+ s.licenses = ["MIT"]
65
+ s.rubygems_version = "2.5.1"
66
+ s.summary = "Converter for fiat and crypto currencies"
67
+
68
+ if s.respond_to? :specification_version then
69
+ s.specification_version = 4
70
+
71
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
72
+ s.add_runtime_dependency(%q<satoshi-unit>, [">= 0"])
73
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
74
+ s.add_development_dependency(%q<jeweler>, ["~> 2.1.1"])
75
+ s.add_development_dependency(%q<rspec>, [">= 0"])
76
+ s.add_development_dependency(%q<webmock>, [">= 0"])
77
+ s.add_development_dependency(%q<vcr>, [">= 0"])
78
+ else
79
+ s.add_dependency(%q<satoshi-unit>, [">= 0"])
80
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
81
+ s.add_dependency(%q<jeweler>, ["~> 2.1.1"])
82
+ s.add_dependency(%q<rspec>, [">= 0"])
83
+ s.add_dependency(%q<webmock>, [">= 0"])
84
+ s.add_dependency(%q<vcr>, [">= 0"])
85
+ end
86
+ else
87
+ s.add_dependency(%q<satoshi-unit>, [">= 0"])
88
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
89
+ s.add_dependency(%q<jeweler>, ["~> 2.1.1"])
90
+ s.add_dependency(%q<rspec>, [">= 0"])
91
+ s.add_dependency(%q<webmock>, [">= 0"])
92
+ s.add_dependency(%q<vcr>, [">= 0"])
93
+ end
94
+ end
95
+
@@ -0,0 +1,42 @@
1
+ module CurrencyRate
2
+ class Adapter
3
+ include Singleton
4
+
5
+ class FetchingFailed < Exception; end
6
+ class CurrencyNotSupported < Exception; end
7
+
8
+ def initialize
9
+ @storage = Storage.new
10
+ end
11
+
12
+ def fetch_rates!
13
+ raise "FETCH_URL is not defined!" unless self.class::FETCH_URL
14
+ uri = URI.parse(self.class::FETCH_URL)
15
+ begin
16
+ @rates = JSON.parse(uri.read(read_timeout: 4))
17
+ @rates_updated_at = Time.now
18
+ rescue OpenURI::HTTPError => e
19
+ raise FetchingFailed
20
+ end
21
+ end
22
+
23
+ def rate_for(currency_code)
24
+ @storage.fetch(self.class.to_s) do
25
+ self.fetch_rates!
26
+ end
27
+ nil # this should be changed in descendant classes
28
+ end
29
+
30
+ # This method will get value we are interested in from hash and
31
+ # prevent failing with 'undefined method [] for Nil' if at some point hash doesn't have such key value pair
32
+ def get_rate_value_from_hash(rates_hash, *keys)
33
+ rates_hash.deep_get(*keys) || raise(CurrencyNotSupported)
34
+ end
35
+
36
+ # We dont want to have false positive rate, because nil.to_f is 0.0
37
+ # This method checks that rate value is not nil
38
+ def rate_to_f(rate)
39
+ rate ? rate.to_f : raise(CurrencyNotSupported)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module CurrencyRate
2
+
3
+ class BtcAdapter < Adapter
4
+
5
+ def convert_from_currency(amount_in_currency, btc_denomination: :satoshi, currency: 'USD')
6
+ btc_amount = amount_in_currency.to_f/rate_for(currency)
7
+ Satoshi.new(btc_amount, from_unit: :btc, to_unit: btc_denomination).to_unit
8
+ end
9
+
10
+ def convert_to_currency(amount, btc_denomination: :satoshi, currency: 'USD')
11
+ amount_in_btc = Satoshi.new(amount.to_f, from_unit: btc_denomination).to_btc
12
+ amount_in_btc*rate_for(currency)
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,50 @@
1
+ module CurrencyRate
2
+ class AverageRateAdapter < BtcAdapter
3
+
4
+ # Takes exchange rate adapters instances or classes as arguments
5
+ def self.instance(*adapters)
6
+ instance = super()
7
+ instance.instance_variable_set(:@adapters, adapters.map { |adapter| adapter.respond_to?(:instance) ? adapter.instance : adapter })
8
+ instance
9
+ end
10
+
11
+ def fetch_rates!
12
+ failed_fetches = 0
13
+ @adapters.each do |adapter|
14
+ begin
15
+ adapter.fetch_rates!
16
+ rescue FetchingFailed => e
17
+ failed_fetches += 1
18
+ raise e if failed_fetches == @adapters.size
19
+ end
20
+ end
21
+ end
22
+
23
+ def rate_for(currency_code)
24
+ rates = []
25
+ @adapters.each do |adapter|
26
+ begin
27
+ rates << adapter.rate_for(currency_code)
28
+ rescue CurrencyNotSupported
29
+ rates << nil
30
+ end
31
+ end
32
+
33
+ unless rates.select(&:nil?).size == @adapters.size
34
+ rates.compact!
35
+ rates.inject {|sum, rate| sum + rate} / rates.size
36
+ else
37
+ raise CurrencyNotSupported
38
+ end
39
+ end
40
+
41
+ def get_rate_value_from_hash(rates_hash, *keys)
42
+ raise "This method is not supposed to be used in #{self.class}."
43
+ end
44
+
45
+ def rate_to_f(rate)
46
+ raise "This method is not supposed to be used in #{self.class}."
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module CurrencyRate
2
+ class BitpayAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://bitpay.com/api/rates'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ @rates.each do |rt|
9
+ if rt['code'] == currency_code
10
+ rate = get_rate_value_from_hash(rt, 'rate')
11
+ return rate_to_f(rate)
12
+ end
13
+ end
14
+ raise CurrencyNotSupported
15
+ end
16
+
17
+ end
18
+ end