omni_exchange 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniExchange
4
+ class Provider
5
+ # @providers is a hash of registered providers that OmniExchange can request exchange rates data from
6
+ @providers = {}
7
+ @currency_data = nil
8
+
9
+ # This method registers providers by adding a provider's name as a key and a provider class as a value to
10
+ # @providers. This happens automatically on load at the top of the lib/omni_exchange.rb file when each
11
+ # file in the lib/omni_exchange/providers folder becomes required.
12
+ #
13
+ # @param provider_name [Symbol] the name of the exchange rate API data provider. ie. :xe, :open_exchange_rates
14
+ # @param provider_module [Class] the class of the provider. ie. OmniExchange::Xe, OmniExchange::OpenExchangeRates
15
+ def self.register_provider(provider_name, provider_module)
16
+ @providers[provider_name] = provider_module
17
+ end
18
+
19
+ # This method is called in the .exchange_currency method. It returns the provider that is requested if the provider
20
+ # is registered in the @providers hash. Once loaded, class methods (such as .get_exchange_rate) can be called on
21
+ # the provider. However, if the provider is unregistered, a LoadError is raised.
22
+ #
23
+ # @param provider_name [Symbol] the name of the exchange rate API provider that is to be loaded. ie. :xe
24
+ # @return [Class] the provider is returned if it has been registered properly in the @providers hash. Otherwise,
25
+ # a LoadError exception is raised
26
+ def self.load_provider(provider_name)
27
+ provider = @providers[provider_name]
28
+ return provider if provider
29
+
30
+ raise(LoadError.new, "#{provider_name} did not load properly as a provider")
31
+ end
32
+
33
+ # a method that gives access to the @providers hash and the providers registered in it
34
+ def self.all
35
+ @providers
36
+ end
37
+
38
+ # Each provider class should inherit from Provider and have a .get_exchange_rates method. If a provider class
39
+ # doesn't have a .get_exchange_rates method, the method below will be called and an error will be raised.
40
+ def self.get_exchange_rate
41
+ raise 'method not implemented...'
42
+ end
43
+
44
+ # Some currencies, such as the US dollar, have subunits (ie. cents). Therefore, to make sure that currencies are
45
+ # exchanged accurately, a currency's subunit needs to be taken into account, and that's what this method does.
46
+ # Subunit data of currencies is stored as @currency_data after being read from the currency_data.json file.
47
+ #
48
+ # @param base_currency [String] the ISO Currency Code of the currency that you're exchanging from. A check is done
49
+ # on this currency to see if it has subunits (such as the US dollar having cents). ie. "USD", "JPY"
50
+ # @return [Float] the amount an exchange rate should be multiplied by to account for subunits
51
+ def self.get_currency_unit(base_currency)
52
+ return 1.0 / @currency_data[base_currency.downcase]['subunit_to_unit'] if @currency_data
53
+
54
+ file = File.read(File.join(File.dirname(__FILE__), './currency_data.json'))
55
+ @currency_data = JSON.parse(file)
56
+
57
+ 1.0 / @currency_data[base_currency.downcase]['subunit_to_unit']
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omni_exchange'
4
+
5
+ module OmniExchange
6
+ class OpenExchangeRates < Provider
7
+ ENDPOINT_URL = 'https://openexchangerates.org/api/latest.json'
8
+
9
+ # This method returns the exchange rate, the rate at which the smallest unit of one currency (the base currency)
10
+ # will be exchanged for another currency (the target currency), from Open Exchange Rate's API.
11
+ # This method is called in the OmniExchange.exchange_currency method.
12
+ #
13
+ # @param base_currency: [String] the ISO Currency Code of the currency that you're exchanging from. ie. "USD", "JPY"
14
+ # @param target_currency: [String] the ISO Currency Code of the currency that you're exchanging to. ie. "EUR", "KRW"
15
+ # @ return [BigDecimal] an exchange rate is returned as a BigDecimal for precise calculation since this exchange
16
+ # rate will be used to calculate an convert an exchange of currencies. However, an exception will be raised
17
+ # if there is a timeout while connecting to xe.com or a timeout while reading Open Exchange Rate's API.
18
+ def self.get_exchange_rate(base_currency:, target_currency:)
19
+ config = OmniExchange.configuration.provider_config[:open_exchange_rates]
20
+ app_id = config[:app_id]
21
+
22
+ api = Faraday.new(ENDPOINT_URL) do |conn|
23
+ conn.response :json, parser_options: { symbolize_names: true }
24
+ end
25
+
26
+ begin
27
+ response = api.get do |req|
28
+ req.url "?app_id=#{app_id}&base=#{base_currency}"
29
+ req.options.timeout = config[:read_timeout] || OmniExchange::Configuration::DEFAULT_READ_TIMEOUT
30
+ req.options.open_timeout = config[:connect_timeout] || OmniExchange::Configuration::DEFAULT_CONNECTION_TIMEOUT
31
+ end
32
+ rescue Faraday::Error, Faraday::ConnectionFailed => e
33
+ raise e.class, 'Open Exchange Rates has timed out.'
34
+ end
35
+ exchange_rate = response.body[:rates][target_currency.to_sym].to_d
36
+
37
+ currency_unit = get_currency_unit(base_currency)
38
+
39
+ exchange_rate * currency_unit
40
+ end
41
+
42
+ # when this file is required at the top of lib/omni_exchange.rb, this method call is run and allows
43
+ # OmniExchange::OpenExchangeRates to be registered in @providers.
44
+ OmniExchange::Provider.register_provider(:open_exchange_rates, self)
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omni_exchange'
4
+
5
+ module OmniExchange
6
+ class Xe < Provider
7
+ ENDPOINT_URL = 'https://xecdapi.xe.com/'
8
+
9
+ # This method returns the exchange rate, the rate at which the smallest unit of one currency (the base currency)
10
+ # will be exchanged for another currency (the target currency), from xe.com's API.
11
+ # This method is called in the OmniExchange.exchange_currency method.
12
+ #
13
+ # @param base_currency: [String] the ISO Currency Code of the currency that you're exchanging from. ie. "USD", "JPY"
14
+ # @param target_currency: [String] the ISO Currency Code of the currency that you're exchanging to. ie. "EUR", "KRW"
15
+ # @ return [BigDecimal] an exchange rate is returned as a BigDecimal for precise calculation since this exchange
16
+ # rate will be used to calculate an convert an exchange of currencies. However, an exception will be raised
17
+ # if there is a timeout while connecting to xe.com or a timeout while reading xe.com's API.
18
+ def self.get_exchange_rate(base_currency:, target_currency:)
19
+ config = OmniExchange.configuration.provider_config[:xe]
20
+ api_id = config[:api_id]
21
+ api_key = config[:api_key]
22
+ currency_unit = get_currency_unit(base_currency)
23
+
24
+ api = Faraday.new(ENDPOINT_URL) do |conn|
25
+ conn.request :authorization, :basic, api_id, api_key
26
+ conn.response :json, parser_options: { symbolize_names: true }
27
+ end
28
+
29
+ begin
30
+ response = api.get do |req|
31
+ req.url "v1/convert_from.json/?from=#{base_currency}&to=#{target_currency}&amount=#{currency_unit}"
32
+ req.options.timeout = config[:read_timeout] || OmniExchange::Configuration::DEFAULT_READ_TIMEOUT
33
+ req.options.open_timeout = config[:connect_timeout] || OmniExchange::Configuration::DEFAULT_CONNECTION_TIMEOUT
34
+ end
35
+ rescue Faraday::Error, Faraday::ConnectionFailed => e
36
+ raise e.class, 'xe.com has timed out.'
37
+ end
38
+
39
+ response.body[:to][0][:mid].to_d
40
+ end
41
+
42
+ # when this file is required at the top of lib/omni_exchange.rb, this method call is run and allows
43
+ # OmniExchange::Xe to be registered in @providers.
44
+ OmniExchange::Provider.register_provider(:xe, self)
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniExchange
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # requires all of the provider files in the providers folder
4
+ # which then allows the providers to be registered
5
+ Dir['omni_exchange/providers/*.rb'].each do |file|
6
+ require_relative file
7
+ end
8
+ require 'omni_exchange/version'
9
+ require 'omni_exchange/configuration'
10
+ require 'faraday'
11
+ require 'json'
12
+ require 'bigdecimal/util'
13
+
14
+ # rubocop:disable Lint/Syntax
15
+ module OmniExchange
16
+ class Error < StandardError; end
17
+
18
+ # the configuration instance variable is set to the module scope
19
+ class << self
20
+ attr_accessor :configuration
21
+ end
22
+
23
+ def self.configuration
24
+ @configuration ||= OmniExchange::Configuration.new
25
+ end
26
+
27
+ # This method allows you to call OmniExchange.configure with a block that creates a new
28
+ # instance of OmniExchange::Configuration
29
+ def self.configure
30
+ yield(configuration)
31
+ end
32
+
33
+ module_function
34
+
35
+ # returns the amount of money in one country's currency when exchanged from an amount of money of another country's
36
+ # currency using exchange rates data from API providers
37
+ #
38
+ # @param amount: [Integer, #to_d] the amount to exchange (in cents, if applicable to the currency). ie. 1, 10, 100
39
+ # @param base_currency: [String] the ISO Currency Code of the currency that you're exchanging from. ie. "USD", "JPY"
40
+ # @param target_currency: [String] the ISO Currency Code of the currency that you're exchanging to. ie. "EUR", "KRW"
41
+ # @param providers: [Array] an array of symbols of the providers that will be used to get exchange rates API
42
+ # data. The symbols must be found in the @providers hash in the Provider class (lib/omni_exchange/provider.rb).
43
+ # ie. xe:, :open_exchange_rates
44
+ # @return [BigDecimal] the amount of the base currency exchanged to the target currency using an exchange rate
45
+ # provided by one of the data providers in the providers hash. The final amount is returned as a BigDecimal
46
+ # for precise calculation. If all of the providers in the providers hash fail to retrieve data,
47
+ # an exception is raised.
48
+ def exchange_currency(amount:, base_currency:, target_currency:, providers:)
49
+ error_messages = []
50
+
51
+ # Make sure all providers passed exist. If not, a LoadError is raise and not rescued
52
+ provider_classes = providers.map { |p| OmniExchange::Provider.load_provider(p) }
53
+
54
+ # Gracefully hit each provider and fail-over to the next one
55
+ provider_classes.each do |klass|
56
+ rate = klass.get_exchange_rate(base_currency: base_currency, target_currency: target_currency)
57
+
58
+ return rate * amount.to_d
59
+ rescue Faraday::Error, Faraday::ConnectionFailed => e
60
+ error_messages << e.inspect
61
+ end
62
+
63
+ raise "Failed to load #{base_currency}->#{target_currency}:\n#{exception_messages.join("\n")}"
64
+ end
65
+ end
66
+ # rubocop:enable Lint/Syntax
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/omni_exchange/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'omni_exchange'
7
+ spec.version = OmniExchange::VERSION
8
+ spec.authors = ['Yun Chung']
9
+ spec.email = ['yunseok_chung@degica.com']
10
+
11
+ spec.summary = 'Write a short summary, because RubyGems requires one.'
12
+ spec.description = 'Write a longer description or delete this line.'
13
+ spec.homepage = 'https://degica.com'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
16
+
17
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://degica.com'
21
+ spec.metadata['changelog_uri'] = 'https://degica.com'
22
+
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.add_dependency 'faraday'
31
+
32
+ spec.add_development_dependency 'dotenv'
33
+ spec.add_development_dependency 'pry'
34
+ spec.add_development_dependency 'rake', '~> 10.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.0'
36
+ spec.add_development_dependency 'rubocop', '~> 0.80'
37
+ spec.add_development_dependency 'vcr'
38
+ end
metadata ADDED
@@ -0,0 +1,167 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omni_exchange
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yun Chung
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
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'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.80'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.80'
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Write a longer description or delete this line.
112
+ email:
113
+ - yunseok_chung@degica.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".DS_Store"
119
+ - ".github/workflows/main.yml"
120
+ - ".gitignore"
121
+ - ".rspec"
122
+ - ".rubocop.yml"
123
+ - ".rubocop_todo.yml"
124
+ - CODE_OF_CONDUCT.md
125
+ - Gemfile
126
+ - Gemfile.lock
127
+ - LICENSE.txt
128
+ - README.md
129
+ - Rakefile
130
+ - bin/console
131
+ - bin/setup
132
+ - lib/omni_exchange.rb
133
+ - lib/omni_exchange/configuration.rb
134
+ - lib/omni_exchange/currency_data.json
135
+ - lib/omni_exchange/provider.rb
136
+ - lib/omni_exchange/providers/open_exchange_rates.rb
137
+ - lib/omni_exchange/providers/xe.rb
138
+ - lib/omni_exchange/version.rb
139
+ - omni_exchange.gemspec
140
+ homepage: https://degica.com
141
+ licenses:
142
+ - MIT
143
+ metadata:
144
+ allowed_push_host: https://rubygems.org
145
+ homepage_uri: https://degica.com
146
+ source_code_uri: https://degica.com
147
+ changelog_uri: https://degica.com
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: 2.3.0
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubygems_version: 3.1.6
164
+ signing_key:
165
+ specification_version: 4
166
+ summary: Write a short summary, because RubyGems requires one.
167
+ test_files: []