exchange 0.8.0 → 0.9.0
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.
- 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)
|