money-json-rates 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85c0a51f24c0ea3fe650c940046814e18dbab2cd
4
- data.tar.gz: 196b94ee24154b084dd58a8c618ba87f76f2ae5d
3
+ metadata.gz: e55164db6a531154c35743cafd6001a7910d4d76
4
+ data.tar.gz: e4ea963b5edb18852a91cee6c7dbe0af01188ded
5
5
  SHA512:
6
- metadata.gz: 730c31992252f3951e9391ac19f253ca03cc62ffb1a805c1d49c693c85e1197b32941aad192390a6d649e6b323c09e401ee46ddb8e231ae1f81610511be6a9c1
7
- data.tar.gz: a6eba880d5d24153ff0febedb77ba6cf76695dc2489d4a9decf411c20841d94c202cccc5d6a31e7c3640efc2e7913586515e83e19aab1a6914f97dd10ec12561
6
+ metadata.gz: 5ed5fa81da1ece9693bd4def1665751c243fa4ff19266751a6e370d5a5e303e6d0ded18fc2bb6b8aa23d1abe43609c03ca3f5b92cdc466998aa6557590fe5a5d
7
+ data.tar.gz: 79342fdfb5555d07bfb309081756a95e6e5923d4595859e3b32853a9532317c843bd671187a9f992461bb8b9eba39fb9097f459e86f86c74adbb173d4f724b20
@@ -1,24 +1,38 @@
1
1
  require 'money'
2
2
  require 'open-uri'
3
3
 
4
+
5
+ # Money class, see http://github.com/RubyMoney/money
4
6
  class Money
7
+
8
+ # Provides classes that aid in the ability of exchange one currency with
9
+ # another.
5
10
  module Bank
6
11
 
12
+ # Exception that will be thrown if jsonrates.com api returns error on api request.
7
13
  class JsonRatesRequestError < StandardError ; end
8
14
 
15
+ # Exception that will be thrown if api_key was not specified.
9
16
  class NoApiKey < StandardError
17
+ # Default message.
10
18
  def message
11
19
  "Blank api_key! You should get your api_key on jsonrates.com and specify it like JsonRates.api_key = YOUR_API_KEY"
12
20
  end
13
21
  end
14
22
 
23
+ # Money::Bank implementation that gives access to the current exchange rates using jsonrates.com api.
15
24
  class JsonRates < Money::Bank::VariableExchange
16
25
 
26
+ # Host of service jsonrates
17
27
  SERVICE_HOST = "jsonrates.com"
28
+
29
+ # Relative path of jsonrates api
18
30
  SERVICE_PATH = "/get"
19
31
 
20
32
  # @return [Hash] Stores the currently known rates.
21
33
  attr_reader :rates
34
+
35
+ # accessor of api_key of jsonrates.com service
22
36
  attr_accessor :api_key
23
37
 
24
38
  class << self
@@ -28,11 +42,22 @@ class Money
28
42
  # @return [Time] Returns the time when the rates expire.
29
43
  attr_reader :rates_expiration
30
44
 
45
+ # @return [Boolean] Returns is Rates Careful mode set.
46
+ attr_reader :rates_careful
47
+
48
+ ##
49
+ # Set Rates Careful mode
50
+ #
51
+ # @param [Boolean] value - mode Careful, if set - don't reload cache if get some exception
52
+ def rates_careful= value
53
+ @rates_careful = !!value
54
+ end
55
+
31
56
  ##
32
57
  # Set the Time To Live (TTL) in seconds.
33
58
  #
34
- # @param [Integer] the seconds between an expiration and another.
35
- def ttl_in_seconds=(value)
59
+ # @param [Integer] value - the seconds between an expiration and another.
60
+ def ttl_in_seconds= value
36
61
  @ttl_in_seconds = value
37
62
  refresh_rates_expiration! if ttl_in_seconds
38
63
  end
@@ -52,9 +77,9 @@ class Money
52
77
  # @return [Hash] The empty @rates Hash.
53
78
  #
54
79
  # @example
55
- # @bank = JsonRates.new #=> <Money::Bank::JsonRates...>
56
- # @bank.get_rate(:USD, :EUR) #=> 0.776337241
57
- # @bank.flush_rates #=> {}
80
+ # bank = Money::Bank::JsonRates.new #=> <Money::Bank::JsonRates...>
81
+ # bank.get_rate(:USD, :EUR) #=> 0.776337241
82
+ # bank.flush_rates #=> {}
58
83
  def flush_rates
59
84
  @mutex.synchronize{
60
85
  @rates = {}
@@ -72,9 +97,9 @@ class Money
72
97
  # @return [Float] The flushed rate.
73
98
  #
74
99
  # @example
75
- # @bank = JsonRates.new #=> <Money::Bank::JsonRates...>
76
- # @bank.get_rate(:USD, :EUR) #=> 0.776337241
77
- # @bank.flush_rate(:USD, :EUR) #=> 0.776337241
100
+ # bank = Money::Bank::JsonRates.new #=> <Money::Bank::JsonRates...>
101
+ # bank.get_rate(:USD, :EUR) #=> 0.776337241
102
+ # bank.flush_rate(:USD, :EUR) #=> 0.776337241
78
103
  def flush_rate(from, to)
79
104
  key = rate_key_for(from, to)
80
105
  @mutex.synchronize{
@@ -85,7 +110,7 @@ class Money
85
110
  ##
86
111
  # Returns the requested rate.
87
112
  #
88
- # It also flushes all the rates when and if they are expired.
113
+ # It uses +#get_rate_careful+ or +#get_rate_straight+ respect of @rates_careful value
89
114
  #
90
115
  # @param [String, Symbol, Currency] from Currency to convert from
91
116
  # @param [String, Symbol, Currency] to Currency to convert to
@@ -93,14 +118,73 @@ class Money
93
118
  # @return [Float] The requested rate.
94
119
  #
95
120
  # @example
96
- # @bank = JsonRates.new #=> <Money::Bank::JsonRates...>
97
- # @bank.get_rate(:USD, :EUR) #=> 0.776337241
121
+ # bank = Money::Bank::JsonRates.new #=> <Money::Bank::JsonRates...>
122
+ # bank.get_rate(:USD, :EUR) #=> 0.776337241
98
123
  def get_rate(from, to)
99
- expire_rates
124
+ if self.class.rates_careful
125
+ get_rate_careful(from, to)
126
+ else
127
+ get_rate_straight(from, to)
128
+ end
129
+ end
100
130
 
101
- @mutex.synchronize{
102
- @rates[rate_key_for(from, to)] ||= fetch_rate(from, to)
103
- }
131
+ # Registers a conversion rate and returns it (uses +#set_rate+).
132
+ #
133
+ # @param [Currency, String, Symbol] from Currency to exchange from.
134
+ # @param [Currency, String, Symbol] to Currency to exchange to.
135
+ # @param [Numeric] rate Rate to use when exchanging currencies.
136
+ #
137
+ # @return [Numeric]
138
+ #
139
+ # @example
140
+ # bank = Money::Bank::JsonRates.new #=> <Money::Bank::JsonRates...>
141
+ # bank.add_rate("USD", "CAD", 1.24515) #=> 1.24515
142
+ # bank.add_rate("CAD", "USD", 0.803115) #=> 0.803115
143
+ def add_rate from, to, rate
144
+ set_rate from, to, rate
145
+ end
146
+
147
+ # Set the rate for the given currencies. Uses +Mutex+ to synchronize data
148
+ # access.
149
+ #
150
+ # @param [Currency, String, Symbol] from Currency to exchange from.
151
+ # @param [Currency, String, Symbol] to Currency to exchange to.
152
+ # @param [Numeric] rate Rate to use when exchanging currencies.
153
+ # @param [Hash] opts Options hash to set special parameters
154
+ # @option opts [Boolean] :without_mutex disables the usage of a mutex
155
+ #
156
+ # @return [Numeric]
157
+ #
158
+ # @example
159
+ # @bank = Money::Bank::JsonRates.new #=> <Money::Bank::JsonRates...>
160
+ # bank.set_rate("USD", "CAD", 1.24515) #=> 1.24515
161
+ # bank.set_rate("CAD", "USD", 0.803115) #=> 0.803115
162
+ def set_rate from, to, rate
163
+ if self.class.rates_careful
164
+ set_rate_with_time(from, to, rate)
165
+ else
166
+ super
167
+ end
168
+ end
169
+
170
+
171
+ # Return the rate hashkey for the given currencies.
172
+ #
173
+ # @param [Currency, String, Symbol] from The currency to exchange from.
174
+ # @param [Currency, String, Symbol] to The currency to exchange to.
175
+ #
176
+ # @return [String]
177
+ #
178
+ # @example
179
+ # rate_key_for("USD", "CAD") #=> "USD_TO_CAD"
180
+ # Money::Bank::JsonRates.rates_careful = true
181
+ # rate_key_for("USD", "CAD") #=> "USD_TO_CAD_C"
182
+ def rate_key_for(from, to)
183
+ if self.class.rates_careful
184
+ "#{Currency.wrap(from).iso_code}_TO_#{Currency.wrap(to).iso_code}_C".upcase
185
+ else
186
+ super
187
+ end
104
188
  end
105
189
 
106
190
  ##
@@ -108,7 +192,7 @@ class Money
108
192
  #
109
193
  # @return [Boolean]
110
194
  def expire_rates
111
- if self.class.ttl_in_seconds && self.class.rates_expiration <= Time.now
195
+ if expired?
112
196
  flush_rates
113
197
  self.class.refresh_rates_expiration!
114
198
  true
@@ -119,6 +203,101 @@ class Money
119
203
 
120
204
  private
121
205
 
206
+ ##
207
+ # Returns whether the time expired.
208
+ #
209
+ # @return [Boolean]
210
+ def expired?
211
+ self.class.ttl_in_seconds && self.class.rates_expiration <= Time.now
212
+ end
213
+
214
+ ##
215
+ # Returns the requested rate.
216
+ #
217
+ # It not flushes all the rates and create rates with created_at time.
218
+ # Check expired for each rate respectively.
219
+ # If it can't get new rate by some reason it returns cached value.
220
+ #
221
+ # @param [String, Symbol, Currency] from Currency to convert from
222
+ # @param [String, Symbol, Currency] to Currency to convert to
223
+ #
224
+ # @return [Float] The requested rate.
225
+ def get_rate_careful(from, to)
226
+
227
+ rate_key = rate_key_for(from, to)
228
+ rate_cached = @rates[rate_key]
229
+
230
+ if rate_cached.nil? || expired_time?(rate_cached[:created_at])
231
+ set_rate_with_time(from, to, fetch_rate(from, to))
232
+ @rates[rate_key][:rate]
233
+ else
234
+ rate_cached[:rate]
235
+ end
236
+ rescue JsonRatesRequestError => e
237
+ if rate_cached.nil?
238
+ raise e
239
+ else
240
+ rate_cached[:rate]
241
+ end
242
+ end
243
+
244
+ ##
245
+ # Returns the requested rate.
246
+ #
247
+ # It also flushes all the rates when and if they are expired.
248
+ #
249
+ # @param [String, Symbol, Currency] from Currency to convert from
250
+ # @param [String, Symbol, Currency] to Currency to convert to
251
+ #
252
+ # @return [Float] The requested rate.
253
+ def get_rate_straight(from, to)
254
+ expire_rates
255
+
256
+ @mutex.synchronize{
257
+ @rates[rate_key_for(from, to)] ||= fetch_rate(from, to)
258
+ }
259
+ end
260
+
261
+ # Registers a conversion rate with created_at
262
+ # and returns it (uses +#set_rate_with_time+).
263
+ #
264
+ # @param [Currency, String, Symbol] from Currency to exchange from.
265
+ # @param [Currency, String, Symbol] to Currency to exchange to.
266
+ # @param [Numeric] rate Rate to use when exchanging currencies.
267
+ #
268
+ # @return [Numeric]
269
+ def add_rate_with_time(from, to, rate)
270
+ set_rate_with_time(from, to, rate)
271
+ end
272
+
273
+ # Set the rate and created_at time for the given currencies.
274
+ # Uses +Mutex+ to synchronize data access.
275
+ #
276
+ # @param [Currency, String, Symbol] from Currency to exchange from.
277
+ # @param [Currency, String, Symbol] to Currency to exchange to.
278
+ # @param [Numeric] rate Rate to use when exchanging currencies.
279
+ # @param [Hash] opts Options hash to set special parameters
280
+ # @option opts [Boolean] :without_mutex disables the usage of a mutex
281
+ #
282
+ # @return [Numeric]
283
+ def set_rate_with_time(from, to, rate)
284
+ rate_d = BigDecimal.new(rate.to_s)
285
+ @mutex.synchronize {
286
+ @rates[rate_key_for(from, to)] = {rate: rate_d, created_at: Time.now}
287
+ }
288
+ rate_d
289
+ end
290
+
291
+ ##
292
+ # Check if time is expired
293
+ #
294
+ # @param [Time] time Time to check
295
+ #
296
+ # @return [Boolean] Is the time expired.
297
+ def expired_time? time
298
+ time + self.class.ttl_in_seconds.to_i < Time.now
299
+ end
300
+
122
301
  ##
123
302
  # Queries for the requested rate and returns it.
124
303
  #
@@ -127,11 +306,23 @@ class Money
127
306
  #
128
307
  # @return [BigDecimal] The requested rate.
129
308
  def fetch_rate(from, to)
130
- from, to = Currency.wrap(from), Currency.wrap(to)
131
- data = build_uri(from, to).read
309
+ uri = build_uri(from, to)
310
+ data = perform_request(uri)
132
311
  extract_rate(data)
133
312
  end
134
313
 
314
+ ##
315
+ # Performs request on uri or raise exception message with JsonRatesRequestError
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 JsonRatesRequestError, e.message
324
+ end
325
+
135
326
  ##
136
327
  # Build a URI for the given arguments.
137
328
  #
@@ -140,7 +331,8 @@ class Money
140
331
  #
141
332
  # @return [URI::HTTP]
142
333
  def build_uri(from, to)
143
- raise NoApiKey if api_key.nil? || api_key.blank?
334
+ from, to = Currency.wrap(from), Currency.wrap(to)
335
+ raise NoApiKey if api_key.nil? || api_key.empty?
144
336
  uri = URI::HTTP.build(
145
337
  :host => SERVICE_HOST,
146
338
  :path => SERVICE_PATH,
@@ -150,10 +342,14 @@ class Money
150
342
 
151
343
  ##
152
344
  # Takes the response from jsonrates.com and extract the rate.
345
+ #
346
+ # @param [String] data HTTP-Response of api.
347
+ #
153
348
  # @return [BigDecimal]
154
349
  def extract_rate(data)
155
350
  request_hash = JSON.parse(data)
156
- raise JsonRatesRequestError, request_hash['error'] if request_hash['error'].present?
351
+ error = request_hash['error']
352
+ raise JsonRatesRequestError, request_hash['error'] unless (error.nil? || error.empty?)
157
353
  BigDecimal.new(request_hash['rate'])
158
354
  end
159
355
  end
@@ -3,6 +3,7 @@ require 'spec_helper'
3
3
  describe "JsonRates" do
4
4
  before :each do
5
5
  @bank = Money::Bank::JsonRates.new
6
+ @bank.api_key = 'xx-xxx'
6
7
  end
7
8
 
8
9
  it "should accept a ttl_in_seconds option" do
@@ -10,6 +11,39 @@ describe "JsonRates" do
10
11
  expect(Money::Bank::JsonRates.ttl_in_seconds).to eq(86400)
11
12
  end
12
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 => '{"utctime":"2015-06-09T18:50:02+02:00","from":"USD","to":"EUR","rate":"0.88770100"}')
19
+
20
+ @bank.flush_rates
21
+ rate = @bank.get_rate('USD', 'EUR')
22
+ expect(rate).to eq(BigDecimal.new("0.88770100"))
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::JsonRates.rates_careful = true
29
+ Money::Bank::JsonRates.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 => '{"error":"currency XXXX does not exist"}')
38
+ rate = @bank.get_rate('USD', 'EUR')
39
+ expect(rate).to eq(BigDecimal.new("1.011"))
40
+
41
+ Money::Bank::JsonRates.rates_careful = false
42
+ end
43
+
44
+ end
45
+ end
46
+
13
47
  describe ".refresh_rates_expiration!" do
14
48
  it "set the .rates_expiration using the TTL and the current time" do
15
49
  Money::Bank::JsonRates.ttl_in_seconds = 86400
@@ -28,6 +62,14 @@ describe "JsonRates" do
28
62
  end
29
63
  end
30
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
+
31
73
  describe ".flush_rate" do
32
74
  it "should remove a specific rate from @rates" do
33
75
  @bank.add_rate('USD', 'EUR', 1.4)
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require 'timecop'
2
2
  require 'money'
3
- require 'money/bank/json_rates'
3
+ require 'money/bank/json_rates'
4
+ require 'webmock/rspec'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: money-json-rates
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Skuratovsky
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-05 00:00:00.000000000 Z
11
+ date: 2015-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: money
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 3.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,8 +80,7 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '10.0'
69
- description: JsonRates extends Money::Bank::Base and gives access to the current exchange
70
- rates using http://jsonrates.com/ api.
83
+ description: Ruby Money::Bank interface for jsonrates.com exchange data
71
84
  email:
72
85
  - skuratowsky@gmail.com
73
86
  executables: []
@@ -102,3 +115,4 @@ signing_key:
102
115
  specification_version: 4
103
116
  summary: Access the jsonrates.com for gem money
104
117
  test_files: []
118
+ has_rdoc: