currencylayer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8609a897bb711b7bfb0771651f86ca1cf76b64f
4
+ data.tar.gz: 90e220b8604e77ee6b0fb8783eb09be7c61f9261
5
+ SHA512:
6
+ metadata.gz: ff7762a4543cdcbb5cfa02ad8baa0683061d07eaaaad1c6d29c5db914f155738b39f21f92ad4d3d58084b6a2ae4e9afb52ee00358716581d75f1bfa95f0a8ea0
7
+ data.tar.gz: 0dc6dbc5b707448222178ee70287989ace921b88360f05d54c3e20fd0039ac772e6cb993aed04093cebb363530e8b36e9c9b97b2587ecd4c90d18c7599de17af
@@ -0,0 +1,356 @@
1
+ require 'money'
2
+ require 'open-uri'
3
+
4
+ # Money class, see http://github.com/RubyMoney/money
5
+ class Money
6
+
7
+ # Provides classes that aid in the ability of exchange one currency with
8
+ # another.
9
+ module Bank
10
+
11
+ # Exception that will be thrown if jsonrates.com api returns error on api request.
12
+ class RequestError < StandardError ; end
13
+
14
+ # Exception that will be thrown if access_key was not specified.
15
+ class AccessError < StandardError
16
+ # Default message.
17
+ def message
18
+ "Blank access_key! You should get your api access_key on currencylayer.com and specify it"
19
+ end
20
+ end
21
+
22
+ # Money::Bank implementation that gives access to the current exchange rates using jsonrates.com api.
23
+ class Currencylayer < Money::Bank::VariableExchange
24
+
25
+ # Host of service jsonrates
26
+ SERVICE_HOST = "apilayer.net"
27
+
28
+ # Relative path of jsonrates api
29
+ SERVICE_PATH = "/api/live"
30
+
31
+ # @return [Hash] Stores the currently known rates.
32
+ attr_reader :rates
33
+
34
+ # accessor of access_key of jsonrates.com service
35
+ attr_accessor :access_key
36
+
37
+ class << self
38
+ # @return [Integer] Returns the Time To Live (TTL) in seconds.
39
+ attr_reader :ttl_in_seconds
40
+
41
+ # @return [Time] Returns the time when the rates expire.
42
+ attr_reader :rates_expiration
43
+
44
+ # @return [Boolean] Returns is Rates Careful mode set.
45
+ attr_reader :rates_careful
46
+
47
+ ##
48
+ # Set Rates Careful mode
49
+ #
50
+ # @param [Boolean] value - mode Careful, if set - don't reload cache if get some exception
51
+ def rates_careful= value
52
+ @rates_careful = !!value
53
+ end
54
+
55
+ ##
56
+ # Set the Time To Live (TTL) in seconds.
57
+ #
58
+ # @param [Integer] value - the seconds between an expiration and another.
59
+ def ttl_in_seconds= value
60
+ @ttl_in_seconds = value
61
+ refresh_rates_expiration! if ttl_in_seconds
62
+ end
63
+
64
+ ##
65
+ # Set the rates expiration TTL seconds from the current time.
66
+ #
67
+ # @return [Time] The next expiration.
68
+ def refresh_rates_expiration!
69
+ @rates_expiration = Time.now + ttl_in_seconds
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Clears all rates stored in @rates
75
+ #
76
+ # @return [Hash] The empty @rates Hash.
77
+ #
78
+ # @example
79
+ # bank = Money::Bank::Currencylayer.new #=> <Money::Bank::Currencylayer...>
80
+ # bank.get_rate(:USD, :EUR) #=> 0.776337241
81
+ # bank.flush_rates #=> {}
82
+ def flush_rates
83
+ @mutex.synchronize{
84
+ @rates = {}
85
+ }
86
+ end
87
+
88
+ ##
89
+ # Clears the specified rate stored in @rates.
90
+ #
91
+ # @param [String, Symbol, Currency] from Currency to convert from (used
92
+ # for key into @rates).
93
+ # @param [String, Symbol, Currency] to Currency to convert to (used for
94
+ # key into @rates).
95
+ #
96
+ # @return [Float] The flushed rate.
97
+ #
98
+ # @example
99
+ # bank = Money::Bank::Currencylayer.new #=> <Money::Bank::Currencylayer...>
100
+ # bank.get_rate(:USD, :EUR) #=> 0.776337241
101
+ # bank.flush_rate(:USD, :EUR) #=> 0.776337241
102
+ def flush_rate(from, to)
103
+ key = rate_key_for(from, to)
104
+ @mutex.synchronize{
105
+ @rates.delete(key)
106
+ }
107
+ end
108
+
109
+ ##
110
+ # Returns the requested rate.
111
+ #
112
+ # It uses +#get_rate_careful+ or +#get_rate_straight+ respect of @rates_careful value
113
+ #
114
+ # @param [String, Symbol, Currency] from Currency to convert from
115
+ # @param [String, Symbol, Currency] to Currency to convert to
116
+ #
117
+ # @return [Float] The requested rate.
118
+ #
119
+ # @example
120
+ # bank = Money::Bank::Currencylayer.new #=> <Money::Bank::Currencylayer...>
121
+ # bank.get_rate(:USD, :EUR) #=> 0.776337241
122
+ def get_rate(from, to)
123
+ if self.class.rates_careful
124
+ get_rate_careful(from, to)
125
+ else
126
+ get_rate_straight(from, to)
127
+ end
128
+ end
129
+
130
+ # Registers a conversion rate and returns it (uses +#set_rate+).
131
+ #
132
+ # @param [Currency, String, Symbol] from Currency to exchange from.
133
+ # @param [Currency, String, Symbol] to Currency to exchange to.
134
+ # @param [Numeric] rate Rate to use when exchanging currencies.
135
+ #
136
+ # @return [Numeric]
137
+ #
138
+ # @example
139
+ # bank = Money::Bank::Currencylayer.new #=> <Money::Bank::Currencylayer...>
140
+ # bank.add_rate("USD", "CAD", 1.24515) #=> 1.24515
141
+ # bank.add_rate("CAD", "USD", 0.803115) #=> 0.803115
142
+ def add_rate from, to, rate
143
+ set_rate from, to, rate
144
+ end
145
+
146
+ # Set the rate for the given currencies. Uses +Mutex+ to synchronize data
147
+ # access.
148
+ #
149
+ # @param [Currency, String, Symbol] from Currency to exchange from.
150
+ # @param [Currency, String, Symbol] to Currency to exchange to.
151
+ # @param [Numeric] rate Rate to use when exchanging currencies.
152
+ # @param [Hash] opts Options hash to set special parameters
153
+ # @option opts [Boolean] :without_mutex disables the usage of a mutex
154
+ #
155
+ # @return [Numeric]
156
+ #
157
+ # @example
158
+ # @bank = Money::Bank::Currencylayer.new #=> <Money::Bank::Currencylayer...>
159
+ # bank.set_rate("USD", "CAD", 1.24515) #=> 1.24515
160
+ # bank.set_rate("CAD", "USD", 0.803115) #=> 0.803115
161
+ def set_rate from, to, rate
162
+ if self.class.rates_careful
163
+ set_rate_with_time(from, to, rate)
164
+ else
165
+ super
166
+ end
167
+ end
168
+
169
+
170
+ # Return the rate hashkey for the given currencies.
171
+ #
172
+ # @param [Currency, String, Symbol] from The currency to exchange from.
173
+ # @param [Currency, String, Symbol] to The currency to exchange to.
174
+ #
175
+ # @return [String]
176
+ #
177
+ # @example
178
+ # rate_key_for("USD", "CAD") #=> "USD_TO_CAD"
179
+ # Money::Bank::Currencylayer.rates_careful = true
180
+ # rate_key_for("USD", "CAD") #=> "USD_TO_CAD_C"
181
+ def rate_key_for(from, to)
182
+ if self.class.rates_careful
183
+ "#{Currency.wrap(from).iso_code}_TO_#{Currency.wrap(to).iso_code}_C".upcase
184
+ else
185
+ super
186
+ end
187
+ end
188
+
189
+ ##
190
+ # Flushes all the rates if they are expired.
191
+ #
192
+ # @return [Boolean]
193
+ def expire_rates
194
+ if expired?
195
+ flush_rates
196
+ self.class.refresh_rates_expiration!
197
+ true
198
+ else
199
+ false
200
+ end
201
+ end
202
+
203
+ private
204
+
205
+ ##
206
+ # Returns whether the time expired.
207
+ #
208
+ # @return [Boolean]
209
+ def expired?
210
+ self.class.ttl_in_seconds && self.class.rates_expiration <= Time.now
211
+ end
212
+
213
+ ##
214
+ # Returns the requested rate.
215
+ #
216
+ # It not flushes all the rates and create rates with created_at time.
217
+ # Check expired for each rate respectively.
218
+ # If it can't get new rate by some reason it returns cached value.
219
+ #
220
+ # @param [String, Symbol, Currency] from Currency to convert from
221
+ # @param [String, Symbol, Currency] to Currency to convert to
222
+ #
223
+ # @return [Float] The requested rate.
224
+ def get_rate_careful(from, to)
225
+
226
+ rate_key = rate_key_for(from, to)
227
+ rate_cached = @rates[rate_key]
228
+
229
+ if rate_cached.nil? || expired_time?(rate_cached[:created_at])
230
+ set_rate_with_time(from, to, fetch_rate(from, to))
231
+ @rates[rate_key][:rate]
232
+ else
233
+ rate_cached[:rate]
234
+ end
235
+ rescue RequestError => e
236
+ if rate_cached.nil?
237
+ raise e
238
+ else
239
+ rate_cached[:rate]
240
+ end
241
+ end
242
+
243
+ ##
244
+ # Returns the requested rate.
245
+ #
246
+ # It also flushes all the rates when and if they are expired.
247
+ #
248
+ # @param [String, Symbol, Currency] from Currency to convert from
249
+ # @param [String, Symbol, Currency] to Currency to convert to
250
+ #
251
+ # @return [Float] The requested rate.
252
+ def get_rate_straight(from, to)
253
+ expire_rates
254
+
255
+ @mutex.synchronize{
256
+ @rates[rate_key_for(from, to)] ||= fetch_rate(from, to)
257
+ }
258
+ end
259
+
260
+ # Registers a conversion rate with created_at
261
+ # and returns it (uses +#set_rate_with_time+).
262
+ #
263
+ # @param [Currency, String, Symbol] from Currency to exchange from.
264
+ # @param [Currency, String, Symbol] to Currency to exchange to.
265
+ # @param [Numeric] rate Rate to use when exchanging currencies.
266
+ #
267
+ # @return [Numeric]
268
+ def add_rate_with_time(from, to, rate)
269
+ set_rate_with_time(from, to, rate)
270
+ end
271
+
272
+ # Set the rate and created_at time for the given currencies.
273
+ # Uses +Mutex+ to synchronize data access.
274
+ #
275
+ # @param [Currency, String, Symbol] from Currency to exchange from.
276
+ # @param [Currency, String, Symbol] to Currency to exchange to.
277
+ # @param [Numeric] rate Rate to use when exchanging currencies.
278
+ # @param [Hash] opts Options hash to set special parameters
279
+ # @option opts [Boolean] :without_mutex disables the usage of a mutex
280
+ #
281
+ # @return [Numeric]
282
+ def set_rate_with_time(from, to, rate)
283
+ rate_d = BigDecimal.new(rate.to_s)
284
+ @mutex.synchronize {
285
+ @rates[rate_key_for(from, to)] = {rate: rate_d, created_at: Time.now}
286
+ }
287
+ rate_d
288
+ end
289
+
290
+ ##
291
+ # Check if time is expired
292
+ #
293
+ # @param [Time] time Time to check
294
+ #
295
+ # @return [Boolean] Is the time expired.
296
+ def expired_time? time
297
+ time + self.class.ttl_in_seconds.to_i < Time.now
298
+ end
299
+
300
+ ##
301
+ # Queries for the requested rate and returns it.
302
+ #
303
+ # @param [String, Symbol, Currency] from Currency to convert from
304
+ # @param [String, Symbol, Currency] to Currency to convert to
305
+ #
306
+ # @return [BigDecimal] The requested rate.
307
+ def fetch_rate(from, to)
308
+ from, to = Currency.wrap(from), Currency.wrap(to)
309
+ uri = build_uri(from, to)
310
+ data = perform_request(uri)
311
+ extract_rate(data, from, to)
312
+ end
313
+
314
+ ##
315
+ # Performs request on uri or raise exception message with RequestError
316
+ #
317
+ # @param [String] uri Requested uri
318
+ #
319
+ # @return [String]
320
+ def perform_request(uri)
321
+ uri.read
322
+ rescue Exception => e
323
+ raise RequestError, e.message
324
+ end
325
+
326
+ ##
327
+ # Build a URI for the given arguments.
328
+ #
329
+ # @param [Currency] from The currency to convert from.
330
+ # @param [Currency] to The currency to convert to.
331
+ #
332
+ # @return [URI::HTTP]
333
+ def build_uri(from, to)
334
+ raise AccessError if access_key.nil? || access_key.empty?
335
+ uri = URI::HTTP.build(
336
+ :host => SERVICE_HOST,
337
+ :path => SERVICE_PATH,
338
+ :query => "access_key=#{access_key}&source=#{from}&currencies=#{to}"
339
+ )
340
+ end
341
+
342
+ ##
343
+ # Takes the response from jsonrates.com and extract the rate.
344
+ #
345
+ # @param [String] data HTTP-Response of api.
346
+ #
347
+ # @return [BigDecimal]
348
+ def extract_rate(data, from, to)
349
+ request_hash = JSON.parse(data)
350
+ success = request_hash['success']
351
+ raise RequestError, request_hash['error']['info'] unless success
352
+ BigDecimal.new(request_hash['quotes']["#{from}#{to}"].to_s)
353
+ end
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Currencylayer" do
4
+ before :each do
5
+ @bank = Money::Bank::Currencylayer.new
6
+ @bank.access_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
7
+ end
8
+
9
+ it "should accept a ttl_in_seconds option" do
10
+ Money::Bank::Currencylayer.ttl_in_seconds = 86400
11
+ expect(Money::Bank::Currencylayer.ttl_in_seconds).to eq(86400)
12
+ end
13
+
14
+ describe '.get_rate' do
15
+ it 'returns rate' do
16
+ uri = @bank.send(:build_uri, 'USD', 'EUR').to_s
17
+ stub_request(:get, uri).to_return( :status => 200,
18
+ :body => '{"success":true,"terms":"https:\/\/currencylayer.com\/terms","privacy":"https:\/\/currencylayer.com\/privacy","timestamp":1434443053,"source":"USD","quotes":{"USDEUR":0.887701}}')
19
+
20
+ @bank.flush_rates
21
+ rate = @bank.get_rate('USD', 'EUR')
22
+ expect(rate).to eq(BigDecimal.new("0.887701"))
23
+ end
24
+ context "in careful mode" do
25
+
26
+ it "don't flush rate if get some exception on request" do
27
+
28
+ Money::Bank::Currencylayer.rates_careful = true
29
+ Money::Bank::Currencylayer.ttl_in_seconds = 0
30
+
31
+ @bank.flush_rates
32
+ @bank.add_rate('USD', 'EUR', 1.011)
33
+
34
+
35
+ uri = @bank.send(:build_uri, 'USD', 'EUR').to_s
36
+
37
+ stub_request(:get, uri).to_return(:status => 200, :body => '{"success":false,"error":{"code":202,"info":"You have provided one or more invalid Currency Codes. [Required format: currencies=EUR,USD,GBP,...]"}}')
38
+ rate = @bank.get_rate('USD', 'EUR')
39
+ expect(rate).to eq(BigDecimal.new("1.011"))
40
+
41
+ Money::Bank::Currencylayer.rates_careful = false
42
+ end
43
+
44
+ end
45
+ end
46
+
47
+ describe ".refresh_rates_expiration!" do
48
+ it "set the .rates_expiration using the TTL and the current time" do
49
+ Money::Bank::Currencylayer.ttl_in_seconds = 86400
50
+ new_time = Time.now
51
+ Timecop.freeze(new_time)
52
+ Money::Bank::Currencylayer.refresh_rates_expiration!
53
+ expect(Money::Bank::Currencylayer.rates_expiration).to eq(new_time + 86400)
54
+ end
55
+ end
56
+
57
+ describe ".flush_rates" do
58
+ it "should empty @rates" do
59
+ @bank.add_rate("USD", "CAD", 1.24515)
60
+ @bank.flush_rates
61
+ expect(@bank.rates).to eq({})
62
+ end
63
+ end
64
+
65
+ describe 'careful mode' do
66
+ it 'returns cached value if exception raised' do
67
+ @bank.flush_rates
68
+ @bank.add_rate("USD", "CAD", 32.231)
69
+ expect(@bank.get_rate("USD", "CAD")).to eq (BigDecimal.new('32.231'))
70
+ end
71
+ end
72
+
73
+ describe ".flush_rate" do
74
+ it "should remove a specific rate from @rates" do
75
+ @bank.flush_rates
76
+ @bank.add_rate('USD', 'EUR', 1.4)
77
+ @bank.add_rate('USD', 'JPY', 0.3)
78
+ @bank.flush_rate('USD', 'EUR')
79
+ expect(@bank.rates).to include('USD_TO_JPY')
80
+ expect(@bank.rates).to_not include('USD_TO_EUR')
81
+ end
82
+ end
83
+
84
+ describe ".expire_rates" do
85
+ before do
86
+ Money::Bank::Currencylayer.ttl_in_seconds = 1000
87
+ end
88
+
89
+ context "when the ttl has expired" do
90
+ before do
91
+ new_time = Time.now + 1001
92
+ Timecop.freeze(new_time)
93
+ end
94
+
95
+ it "should flush all rates" do
96
+ expect(@bank).to receive(:flush_rates)
97
+ @bank.expire_rates
98
+ end
99
+
100
+ it "updates the next expiration time" do
101
+ exp_time = Time.now + 1000
102
+
103
+ @bank.expire_rates
104
+ expect(Money::Bank::Currencylayer.rates_expiration).to eq(exp_time)
105
+ end
106
+ end
107
+
108
+ context "when the ttl has not expired" do
109
+ it "not should flush all rates" do
110
+ expect(@bank).to_not receive(:flush_rates)
111
+ @bank.expire_rates
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,4 @@
1
+ require 'timecop'
2
+ require 'money'
3
+ require 'money/bank/currencylayer'
4
+ require 'webmock/rspec'
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: currencylayer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrey Skuratovsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: money
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 6.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: timecop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ description: This gem extends Money::Bank::VariableExchange with Money::Bank::Currencylayer
98
+ and gives you access to the current exchange rates on currencylayer.com.
99
+ email:
100
+ - skuratowsky@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - lib/money/bank/currencylayer.rb
106
+ - spec/currencylayer_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: http://github.com/askuratovsky/currencylayer
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.4.3
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Access to the currencylayer.com online exchange rates
132
+ test_files: []
133
+ has_rdoc: