exchange 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.rspec +1 -1
- data/Gemfile.lock +3 -3
- data/README.rdoc +115 -47
- data/benchmark/benchmark.rb +49 -0
- data/changelog.rdoc +8 -1
- data/lib/exchange.rb +4 -4
- data/lib/exchange/base.rb +1 -1
- data/lib/exchange/cache.rb +2 -0
- data/lib/exchange/cache/base.rb +20 -6
- data/lib/exchange/cache/configuration.rb +24 -0
- data/lib/exchange/cache/file.rb +24 -9
- data/lib/exchange/cache/memcached.rb +3 -3
- data/lib/exchange/cache/memory.rb +89 -0
- data/lib/exchange/cache/rails.rb +1 -1
- data/lib/exchange/cache/redis.rb +4 -4
- data/lib/exchange/configurable.rb +53 -0
- data/lib/exchange/configuration.rb +32 -26
- data/lib/exchange/core_extensions.rb +3 -0
- data/lib/exchange/core_extensions/cachify.rb +25 -0
- data/lib/exchange/core_extensions/float/error_safe.rb +25 -0
- data/lib/{core_extensions → exchange/core_extensions/numeric}/conversability.rb +12 -12
- data/lib/exchange/external_api.rb +2 -1
- data/lib/exchange/external_api/base.rb +34 -9
- data/lib/exchange/external_api/call.rb +6 -8
- data/lib/exchange/external_api/configuration.rb +25 -0
- data/lib/exchange/external_api/ecb.rb +16 -25
- data/lib/exchange/external_api/json.rb +11 -1
- data/lib/exchange/external_api/open_exchange_rates.rb +65 -0
- data/lib/exchange/external_api/xavier_media.rb +7 -7
- data/lib/exchange/iso_4217.rb +32 -5
- data/lib/exchange/{currency.rb → money.rb} +112 -110
- data/lib/exchange/typecasting.rb +94 -0
- data/spec/exchange/cache/base_spec.rb +2 -2
- data/spec/exchange/cache/configuration_spec.rb +56 -0
- data/spec/exchange/cache/file_spec.rb +10 -8
- data/spec/exchange/cache/memcached_spec.rb +9 -18
- data/spec/exchange/cache/memory_spec.rb +122 -0
- data/spec/exchange/cache/no_cache_spec.rb +5 -15
- data/spec/exchange/cache/rails_spec.rb +2 -6
- data/spec/exchange/cache/redis_spec.rb +8 -18
- data/spec/exchange/configuration_spec.rb +31 -7
- data/spec/exchange/core_extensions/array/cachify_spec.rb +12 -0
- data/spec/exchange/core_extensions/float/error_safe_spec.rb +49 -0
- data/spec/exchange/core_extensions/hash/cachify_spec.rb +12 -0
- data/spec/exchange/core_extensions/numeric/cachify_spec.rb +26 -0
- data/spec/{core_extensions → exchange/core_extensions/numeric}/conversability_spec.rb +22 -22
- data/spec/exchange/core_extensions/string/cachify_spec.rb +59 -0
- data/spec/exchange/core_extensions/symbol/cachify_spec.rb +12 -0
- data/spec/exchange/external_api/base_spec.rb +10 -7
- data/spec/exchange/external_api/call_spec.rb +3 -0
- data/spec/exchange/external_api/configuration_spec.rb +52 -0
- data/spec/exchange/external_api/ecb_spec.rb +8 -5
- data/spec/exchange/external_api/open_exchange_rates_spec.rb +70 -0
- data/spec/exchange/external_api/xavier_media_spec.rb +8 -5
- data/spec/exchange/iso_4217_spec.rb +208 -20
- data/spec/exchange/{currency_spec.rb → money_spec.rb} +102 -82
- data/spec/exchange/typecasting_spec.rb +86 -0
- metadata +117 -71
- data/exchange-0.7.5.gem +0 -0
- data/exchange-0.7.6.gem +0 -0
- data/lib/exchange/external_api/currency_bot.rb +0 -61
- data/spec/exchange/external_api/currency_bot_spec.rb +0 -63
@@ -0,0 +1,24 @@
|
|
1
|
+
module Exchange
|
2
|
+
module Cache
|
3
|
+
# @author Beat Richartz
|
4
|
+
# A Class that handles caching configuration options
|
5
|
+
#
|
6
|
+
# @version 0.9
|
7
|
+
# @since 0.9
|
8
|
+
#
|
9
|
+
class Configuration < Exchange::Configurable
|
10
|
+
attr_accessor :expire, :host, :port, :path
|
11
|
+
|
12
|
+
def_delegators :instance, :expire, :expire=, :host, :host=, :port, :port=, :path, :path=
|
13
|
+
|
14
|
+
def parent_module
|
15
|
+
Cache
|
16
|
+
end
|
17
|
+
|
18
|
+
def key
|
19
|
+
:cache
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/exchange/cache/file.rb
CHANGED
@@ -21,20 +21,18 @@ module Exchange
|
|
21
21
|
#
|
22
22
|
def cached api, opts={}, &block
|
23
23
|
today = Time.now
|
24
|
-
dir =
|
24
|
+
dir = config.path
|
25
25
|
path = ::File.join(dir, key(api, opts[:cache_period]))
|
26
26
|
|
27
27
|
if ::File.exists?(path)
|
28
|
-
result = ::File.read(path)
|
28
|
+
result = opts[:plain] ? ::File.read(path) : ::File.read(path).decachify
|
29
29
|
else
|
30
30
|
result = super
|
31
31
|
if result && !result.to_s.empty?
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
::File.open(path, 'w') {|f| f.write(result) }
|
32
|
+
make_sure_exists dir
|
33
|
+
clean! dir, api
|
34
|
+
|
35
|
+
::File.open(path, 'w') {|f| f.write(result.cachify) }
|
38
36
|
end
|
39
37
|
end
|
40
38
|
|
@@ -49,12 +47,29 @@ module Exchange
|
|
49
47
|
# @param [optional, Symbol] cache_period The time for which the data is valid
|
50
48
|
# @return [String] A string that can be used as cache key
|
51
49
|
# @example
|
52
|
-
# Exchange::Cache::Base.key(Exchange::ExternalAPI::
|
50
|
+
# Exchange::Cache::Base.key(Exchange::ExternalAPI::OpenExchangeRates, :monthly) #=> "Exchange_ExternalAPI_CurrencyBot_monthly_2012_1"
|
53
51
|
#
|
54
52
|
def key(api_class, cache_period=:daily)
|
55
53
|
time = Time.now
|
56
54
|
[api_class.to_s.gsub(/::/, '_'), cache_period, time.year, time.send(cache_period == :monthly ? :month : :yday)].join('_')
|
57
55
|
end
|
56
|
+
|
57
|
+
# Make sure the directory exists
|
58
|
+
# @param [String] dir the directory path
|
59
|
+
#
|
60
|
+
def make_sure_exists dir
|
61
|
+
FileUtils.mkdir_p(dir) unless Dir.respond_to?(:exists?) && Dir.exists?(dir)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Clean the files not needed anymore
|
65
|
+
# @param [String] dir the directory path
|
66
|
+
#
|
67
|
+
def clean! dir, api
|
68
|
+
keep_files = [key(api, :daily), key(api, :monthly)]
|
69
|
+
Dir.entries(dir).each do |e|
|
70
|
+
::File.delete(::File.join(dir, e)) unless keep_files.include?(e) || e.match(/\A\./)
|
71
|
+
end
|
72
|
+
end
|
58
73
|
|
59
74
|
end
|
60
75
|
end
|
@@ -22,7 +22,7 @@ module Exchange
|
|
22
22
|
#
|
23
23
|
def client
|
24
24
|
Exchange::GemLoader.new('dalli').try_load unless defined?(::Dalli)
|
25
|
-
@client ||= Dalli::Client.new("#{
|
25
|
+
@client ||= Dalli::Client.new("#{config.host}:#{config.port}")
|
26
26
|
end
|
27
27
|
|
28
28
|
# returns either cached data from the memcached client or calls the block and caches it in memcached.
|
@@ -35,12 +35,12 @@ module Exchange
|
|
35
35
|
#
|
36
36
|
def cached api, opts={}, &block
|
37
37
|
stored = client.get(key(api, opts))
|
38
|
-
result = opts[:plain] ? stored
|
38
|
+
result = opts[:plain] ? stored : stored.decachify if stored && !stored.to_s.empty?
|
39
39
|
|
40
40
|
unless result
|
41
41
|
result = super
|
42
42
|
if result && !result.to_s.empty?
|
43
|
-
client.set key(api, opts), result.
|
43
|
+
client.set key(api, opts), result.cachify, config.expire == :daily ? 86400 : 3600
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Exchange
|
2
|
+
module Cache
|
3
|
+
# @author Beat Richartz
|
4
|
+
# A class that uses instance variables on the cache singleton class to store values in memory
|
5
|
+
#
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
# @example Activate caching via memory by setting the cache in the configuration to :memory
|
9
|
+
# Exchange::Configuration.define do |c|
|
10
|
+
# c.cache = :memory
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class Memory < Base
|
14
|
+
|
15
|
+
# returns either cached data from an instance variable or calls the block and caches it in an instance variable.
|
16
|
+
# This method has to be the same in all the cache classes in order for the configuration binding to work
|
17
|
+
# @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
|
18
|
+
# @param [Hash] opts the options to cache with
|
19
|
+
# @option opts [Time] :at the historic time of the exchange rates to be cached
|
20
|
+
# @yield [] This method takes a mandatory block with an arity of 0 for caching
|
21
|
+
# @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
|
22
|
+
#
|
23
|
+
def cached api, opts={}, &block
|
24
|
+
ivar_name = instance_variable_name(api, opts)
|
25
|
+
|
26
|
+
result = instance_variable_get(ivar_name)
|
27
|
+
|
28
|
+
unless result && !result.to_s.empty?
|
29
|
+
result = super
|
30
|
+
|
31
|
+
if result && !result.to_s.empty?
|
32
|
+
instance_variable_set(ivar_name, result)
|
33
|
+
end
|
34
|
+
|
35
|
+
clean!
|
36
|
+
end
|
37
|
+
|
38
|
+
opts[:plain] ? result.cachify : result
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Generate an instance variable name for the memory cache to get and set
|
44
|
+
# @param [Exchange::ExternalAPI::Subclass] api The API to store the data for
|
45
|
+
# @param [Hash] opts The options for caching
|
46
|
+
# @return [String] A string that can be used as instance variable name
|
47
|
+
#
|
48
|
+
def instance_variable_name(api, opts)
|
49
|
+
conversion_time = helper.assure_time(opts[:at], :default => :now)
|
50
|
+
time = Time.now
|
51
|
+
expire_hourly = config.expire == :hourly || nil
|
52
|
+
|
53
|
+
[
|
54
|
+
'@' + api.to_s.downcase.gsub(/::/, '_'),
|
55
|
+
conversion_time.year.to_s,
|
56
|
+
conversion_time.yday.to_s,
|
57
|
+
expire_hourly && conversion_time.hour.to_s,
|
58
|
+
time.year.to_s,
|
59
|
+
time.yday.to_s,
|
60
|
+
expire_hourly && time.hour.to_s,
|
61
|
+
opts[:key_for] && opts[:key_for].join('_')
|
62
|
+
].compact.join('_')
|
63
|
+
end
|
64
|
+
|
65
|
+
# Clean the memory from expired exchange instance variables. This removes instance variables matching
|
66
|
+
# only the specific key pattern
|
67
|
+
# @return [Array] The still persisting instance variables
|
68
|
+
#
|
69
|
+
def clean!
|
70
|
+
time = Time.now
|
71
|
+
|
72
|
+
instance_variables.select do |i|
|
73
|
+
condition = false
|
74
|
+
match = i.to_s.match(/\A@exchange[^\d]+\d{4}_\d{1,3}_?\d{0,2}_(\d{4})_(\d{1,3})_?(\d{1,2})?/)
|
75
|
+
|
76
|
+
if match
|
77
|
+
condition = match[1].to_i != time.year || match[2].to_i != time.yday
|
78
|
+
condition = match[3].to_i != time.hour if match[3]
|
79
|
+
end
|
80
|
+
|
81
|
+
condition
|
82
|
+
end.each do |i|
|
83
|
+
remove_instance_variable i
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/exchange/cache/rails.rb
CHANGED
@@ -34,7 +34,7 @@ module Exchange
|
|
34
34
|
def cached api, opts={}, &block
|
35
35
|
raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
|
36
36
|
|
37
|
-
result = client.fetch key(api, opts), :expires_in =>
|
37
|
+
result = client.fetch key(api, opts), :expires_in => config.expire == :daily ? 86400 : 3600, &block
|
38
38
|
client.delete(key(api, opts)) unless result && !result.to_s.empty?
|
39
39
|
|
40
40
|
result
|
data/lib/exchange/cache/redis.rb
CHANGED
@@ -21,7 +21,7 @@ module Exchange
|
|
21
21
|
#
|
22
22
|
def client
|
23
23
|
Exchange::GemLoader.new('redis').try_load unless defined?(::Redis)
|
24
|
-
@client ||= ::Redis.new(:host =>
|
24
|
+
@client ||= ::Redis.new(:host => config.host, :port => config.port)
|
25
25
|
end
|
26
26
|
|
27
27
|
# returns either cached data from the redis client or calls the block and caches it in redis.
|
@@ -34,12 +34,12 @@ module Exchange
|
|
34
34
|
#
|
35
35
|
def cached api, opts={}, &block
|
36
36
|
if result = client.get(key(api, opts))
|
37
|
-
result = opts[:plain] ? result
|
37
|
+
result = opts[:plain] ? result : result.decachify
|
38
38
|
else
|
39
39
|
result = super
|
40
40
|
if result && !result.to_s.empty?
|
41
|
-
client.set key(api, opts), result.
|
42
|
-
client.expire key(api, opts),
|
41
|
+
client.set key(api, opts), result.cachify
|
42
|
+
client.expire key(api, opts), config.expire == :daily ? 86400 : 3600
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Exchange
|
5
|
+
|
6
|
+
class Configurable
|
7
|
+
include Singleton
|
8
|
+
extend SingleForwardable
|
9
|
+
|
10
|
+
attr_accessor :subclass
|
11
|
+
|
12
|
+
def_delegators :instance, :subclass, :subclass=, :set
|
13
|
+
|
14
|
+
def subclass_with_constantize
|
15
|
+
self.subclass = parent_module.const_get camelize(self.subclass_without_constantize) unless self.subclass_without_constantize.is_a?(Class)
|
16
|
+
subclass_without_constantize
|
17
|
+
end
|
18
|
+
alias_method :subclass_without_constantize, :subclass
|
19
|
+
alias_method :subclass, :subclass_with_constantize
|
20
|
+
|
21
|
+
def set hash
|
22
|
+
hash.each_pair do |k,v|
|
23
|
+
self.send(:"#{k}=", v)
|
24
|
+
end
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset
|
30
|
+
set Exchange::Configuration::DEFAULTS[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
[:key, :parent_module].each do |subclass_method|
|
34
|
+
define_method subclass_method do
|
35
|
+
raise StandardError.new("Subclass Responsibility")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Camelize a string or a symbol
|
42
|
+
# @param [String, Symbol] s The string to camelize
|
43
|
+
# @return [String] a camelized string
|
44
|
+
# @example Camelize an underscored symbol
|
45
|
+
# camelize(:some_thing) #=> "SomeThing"
|
46
|
+
#
|
47
|
+
def camelize s
|
48
|
+
s = s.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -50,11 +50,16 @@ module Exchange
|
|
50
50
|
# @private
|
51
51
|
# @macro [getter] install_getter
|
52
52
|
#
|
53
|
-
def install_getter key
|
53
|
+
def install_getter key
|
54
54
|
define_method key do
|
55
55
|
config_part = @config[key]
|
56
|
-
|
57
|
-
|
56
|
+
|
57
|
+
if key == :api && !config_part.is_a?(ExternalAPI::Configuration)
|
58
|
+
config_part = ExternalAPI::Configuration.set(config_part)
|
59
|
+
elsif key == :cache && !config_part.is_a?(Cache::Configuration)
|
60
|
+
config_part = Cache::Configuration.set(config_part)
|
61
|
+
end
|
62
|
+
|
58
63
|
@config[key] = config_part
|
59
64
|
end
|
60
65
|
end
|
@@ -68,13 +73,16 @@ module Exchange
|
|
68
73
|
DEFAULTS = {
|
69
74
|
:api => {
|
70
75
|
:subclass => ExternalAPI::XavierMedia,
|
71
|
-
:retries => 5
|
76
|
+
:retries => 5,
|
77
|
+
:protocol => :http,
|
78
|
+
:app_id => nil
|
72
79
|
},
|
73
80
|
:cache => {
|
74
|
-
:subclass => Cache::
|
75
|
-
:
|
76
|
-
:
|
77
|
-
:
|
81
|
+
:subclass => Cache::Memory,
|
82
|
+
:expire => :daily,
|
83
|
+
:path => nil,
|
84
|
+
:host => nil,
|
85
|
+
:port => nil
|
78
86
|
},
|
79
87
|
:allow_mixed_operations => true
|
80
88
|
}
|
@@ -85,7 +93,7 @@ module Exchange
|
|
85
93
|
# @param [Hash] configuration The configuration as a hash
|
86
94
|
# @param [Proc] block A block to yield the configuration with
|
87
95
|
# @example Define the configuration with a hash
|
88
|
-
# Exchange::Configuration.new(:allow_mixed_operations => false, :api => {:subclass => :
|
96
|
+
# Exchange::Configuration.new(:allow_mixed_operations => false, :api => {:subclass => :open_exchange_rates, :retries => 2})
|
89
97
|
# @example Define the configuration with a block
|
90
98
|
# Exchange::Configuration.new do |c|
|
91
99
|
# c.allow_mixed_operations = false
|
@@ -100,6 +108,16 @@ module Exchange
|
|
100
108
|
super()
|
101
109
|
end
|
102
110
|
|
111
|
+
# Allows to reset the configuration to the defaults
|
112
|
+
# @version 0.9
|
113
|
+
# @since 0.9
|
114
|
+
#
|
115
|
+
def reset
|
116
|
+
api.reset
|
117
|
+
cache.reset
|
118
|
+
self.allow_mixed_operations = DEFAULTS[:allow_mixed_operations]
|
119
|
+
end
|
120
|
+
|
103
121
|
# Getter for the mixed operations configuration. If set to true, operations with mixed currencies will not raise errors
|
104
122
|
# If set to false, mixed operations will raise errors
|
105
123
|
# @since 0.6
|
@@ -120,7 +138,7 @@ module Exchange
|
|
120
138
|
def allow_mixed_operations= data
|
121
139
|
@config[:allow_mixed_operations] = data
|
122
140
|
end
|
123
|
-
|
141
|
+
|
124
142
|
# Setter for the api configuration.
|
125
143
|
# @since 0.6
|
126
144
|
# @version 0.6
|
@@ -147,27 +165,15 @@ module Exchange
|
|
147
165
|
|
148
166
|
# Getter for the api configuration. Instantiates the configuration as an open struct, if called for the first time.
|
149
167
|
# Also camelizes and constantizes the api subclass, if used for the first time.
|
150
|
-
# @return [
|
168
|
+
# @return [Exchange::ExternalAPI::Configuration] an api configuration
|
151
169
|
#
|
152
|
-
install_getter :api
|
170
|
+
install_getter :api
|
153
171
|
|
154
172
|
# Getter for the cache configuration. Instantiates the configuration as an open struct, if called for the first time.
|
155
173
|
# Also camelizes and constantizes the cache subclass, if used for the first time.
|
156
|
-
# @return [
|
174
|
+
# @return [Exchange::Cache::Configuration] a cache configuration
|
157
175
|
#
|
158
|
-
install_getter :cache
|
159
|
-
|
160
|
-
private
|
161
|
-
|
162
|
-
# Camelize a string or a symbol
|
163
|
-
# @param [String, Symbol] s The string to camelize
|
164
|
-
# @return [String] a camelized string
|
165
|
-
# @example Camelize an underscored symbol
|
166
|
-
# camelize(:some_thing) #=> "SomeThing"
|
167
|
-
#
|
168
|
-
def camelize s
|
169
|
-
s = s.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
170
|
-
end
|
176
|
+
install_getter :cache
|
171
177
|
|
172
178
|
end
|
173
179
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Exchange
|
2
|
+
module Cachify
|
3
|
+
|
4
|
+
def cachify
|
5
|
+
Marshal.dump self
|
6
|
+
end
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
module Decachify
|
11
|
+
|
12
|
+
def decachify
|
13
|
+
Marshal.load self
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Numeric.send :include, Exchange::Cachify
|
20
|
+
String.send :include, Exchange::Cachify
|
21
|
+
Symbol.send :include, Exchange::Cachify
|
22
|
+
String.send :include, Exchange::Decachify
|
23
|
+
Hash.send :include, Exchange::Cachify
|
24
|
+
Array.send :include, Exchange::Cachify
|
25
|
+
NilClass.send :include, Exchange::Cachify
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Exchange
|
2
|
+
|
3
|
+
# Make Floating Points forget about their incapabilities when dealing with money
|
4
|
+
#
|
5
|
+
module ErrorSafe
|
6
|
+
|
7
|
+
def self.included base
|
8
|
+
%W(* / + -).each do |meth|
|
9
|
+
base.send(:define_method, :"#{meth}without_errors", lambda { |other|
|
10
|
+
if other.is_a?(Exchange::Money)
|
11
|
+
(BigDecimal.new(self.to_s).send(meth, other)).to_f
|
12
|
+
else
|
13
|
+
send(:"#{meth}with_errors", other)
|
14
|
+
end
|
15
|
+
})
|
16
|
+
base.send :alias_method, :"#{meth}with_errors", meth.to_sym
|
17
|
+
base.send :alias_method, meth.to_sym, :"#{meth}without_errors"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
Float.send(:include, Exchange::ErrorSafe)
|