exchange 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.gitignore +1 -0
  2. data/.rspec +1 -1
  3. data/Gemfile.lock +3 -3
  4. data/README.rdoc +115 -47
  5. data/benchmark/benchmark.rb +49 -0
  6. data/changelog.rdoc +8 -1
  7. data/lib/exchange.rb +4 -4
  8. data/lib/exchange/base.rb +1 -1
  9. data/lib/exchange/cache.rb +2 -0
  10. data/lib/exchange/cache/base.rb +20 -6
  11. data/lib/exchange/cache/configuration.rb +24 -0
  12. data/lib/exchange/cache/file.rb +24 -9
  13. data/lib/exchange/cache/memcached.rb +3 -3
  14. data/lib/exchange/cache/memory.rb +89 -0
  15. data/lib/exchange/cache/rails.rb +1 -1
  16. data/lib/exchange/cache/redis.rb +4 -4
  17. data/lib/exchange/configurable.rb +53 -0
  18. data/lib/exchange/configuration.rb +32 -26
  19. data/lib/exchange/core_extensions.rb +3 -0
  20. data/lib/exchange/core_extensions/cachify.rb +25 -0
  21. data/lib/exchange/core_extensions/float/error_safe.rb +25 -0
  22. data/lib/{core_extensions → exchange/core_extensions/numeric}/conversability.rb +12 -12
  23. data/lib/exchange/external_api.rb +2 -1
  24. data/lib/exchange/external_api/base.rb +34 -9
  25. data/lib/exchange/external_api/call.rb +6 -8
  26. data/lib/exchange/external_api/configuration.rb +25 -0
  27. data/lib/exchange/external_api/ecb.rb +16 -25
  28. data/lib/exchange/external_api/json.rb +11 -1
  29. data/lib/exchange/external_api/open_exchange_rates.rb +65 -0
  30. data/lib/exchange/external_api/xavier_media.rb +7 -7
  31. data/lib/exchange/iso_4217.rb +32 -5
  32. data/lib/exchange/{currency.rb → money.rb} +112 -110
  33. data/lib/exchange/typecasting.rb +94 -0
  34. data/spec/exchange/cache/base_spec.rb +2 -2
  35. data/spec/exchange/cache/configuration_spec.rb +56 -0
  36. data/spec/exchange/cache/file_spec.rb +10 -8
  37. data/spec/exchange/cache/memcached_spec.rb +9 -18
  38. data/spec/exchange/cache/memory_spec.rb +122 -0
  39. data/spec/exchange/cache/no_cache_spec.rb +5 -15
  40. data/spec/exchange/cache/rails_spec.rb +2 -6
  41. data/spec/exchange/cache/redis_spec.rb +8 -18
  42. data/spec/exchange/configuration_spec.rb +31 -7
  43. data/spec/exchange/core_extensions/array/cachify_spec.rb +12 -0
  44. data/spec/exchange/core_extensions/float/error_safe_spec.rb +49 -0
  45. data/spec/exchange/core_extensions/hash/cachify_spec.rb +12 -0
  46. data/spec/exchange/core_extensions/numeric/cachify_spec.rb +26 -0
  47. data/spec/{core_extensions → exchange/core_extensions/numeric}/conversability_spec.rb +22 -22
  48. data/spec/exchange/core_extensions/string/cachify_spec.rb +59 -0
  49. data/spec/exchange/core_extensions/symbol/cachify_spec.rb +12 -0
  50. data/spec/exchange/external_api/base_spec.rb +10 -7
  51. data/spec/exchange/external_api/call_spec.rb +3 -0
  52. data/spec/exchange/external_api/configuration_spec.rb +52 -0
  53. data/spec/exchange/external_api/ecb_spec.rb +8 -5
  54. data/spec/exchange/external_api/open_exchange_rates_spec.rb +70 -0
  55. data/spec/exchange/external_api/xavier_media_spec.rb +8 -5
  56. data/spec/exchange/iso_4217_spec.rb +208 -20
  57. data/spec/exchange/{currency_spec.rb → money_spec.rb} +102 -82
  58. data/spec/exchange/typecasting_spec.rb +86 -0
  59. metadata +117 -71
  60. data/exchange-0.7.5.gem +0 -0
  61. data/exchange-0.7.6.gem +0 -0
  62. data/lib/exchange/external_api/currency_bot.rb +0 -61
  63. 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
@@ -21,20 +21,18 @@ module Exchange
21
21
  #
22
22
  def cached api, opts={}, &block
23
23
  today = Time.now
24
- dir = Exchange.configuration.cache.path
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
- FileUtils.mkdir_p(dir) unless Dir.respond_to?(:exists?) && Dir.exists?(dir)
33
- keep_files = [key(api, :daily), key(api, :monthly)]
34
- Dir.entries(dir).each do |e|
35
- ::File.delete(::File.join(dir, e)) unless keep_files.include?(e) || e.match(/\A\./)
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::CurrencyBot, :monthly) #=> "Exchange_ExternalAPI_CurrencyBot_monthly_2012_1"
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("#{Exchange.configuration.cache.host}:#{Exchange.configuration.cache.port}")
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.to_s.gsub(/["\s+]/, '') : JSON.load(stored) if stored && !stored.empty?
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.to_json, Exchange.configuration.cache.expire == :daily ? 86400 : 3600
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
@@ -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 => Exchange.configuration.cache.expire == :daily ? 86400 : 3600, &block
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
@@ -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 => Exchange.configuration.cache.host, :port => Exchange.configuration.cache.port)
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.gsub(/["\s+]/, '') : JSON.load(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.to_json
42
- client.expire key(api, opts), Exchange.configuration.cache.expire == :daily ? 86400 : 3600
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, parent_module
53
+ def install_getter key
54
54
  define_method key do
55
55
  config_part = @config[key]
56
- config_part = OpenStruct.new(config_part) unless config_part.is_a?(OpenStruct)
57
- config_part.subclass = parent_module.const_get camelize(config_part.subclass) unless config_part.subclass.is_a?(Class)
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::Memcached,
75
- :host => 'localhost',
76
- :port => 11211,
77
- :expire => :daily
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 => :currency_bot, :retries => 2})
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 [OpenStruct] an openstruct with the complete api configuration
168
+ # @return [Exchange::ExternalAPI::Configuration] an api configuration
151
169
  #
152
- install_getter :api, ExternalAPI
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 [OpenStruct] an openstruct with the complete cache configuration
174
+ # @return [Exchange::Cache::Configuration] a cache configuration
157
175
  #
158
- install_getter :cache, 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,3 @@
1
+ require 'exchange/core_extensions/numeric/conversability'
2
+ require 'exchange/core_extensions/float/error_safe'
3
+ require 'exchange/core_extensions/cachify'
@@ -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)