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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +113 -0
- data/.rubocop_todo.yml +47 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +71 -0
- data/LICENSE.txt +21 -0
- data/README.md +84 -0
- data/Rakefile +12 -0
- data/bin/console +21 -0
- data/bin/setup +8 -0
- data/lib/omni_exchange/configuration.rb +23 -0
- data/lib/omni_exchange/currency_data.json +2719 -0
- data/lib/omni_exchange/provider.rb +60 -0
- data/lib/omni_exchange/providers/open_exchange_rates.rb +46 -0
- data/lib/omni_exchange/providers/xe.rb +46 -0
- data/lib/omni_exchange/version.rb +5 -0
- data/lib/omni_exchange.rb +66 -0
- data/omni_exchange.gemspec +38 -0
- metadata +167 -0
@@ -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,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: []
|