omni_exchange 0.1.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.
@@ -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: []