currencylayer 0.0.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.
@@ -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: