omni_exchange 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|