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.
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)