fxer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bee3714d8bd267547541f35c4c39439b3dd712133d7fa2486db961c0ce796a9a
4
+ data.tar.gz: be700568d0b2677254d7b58695e1de9587e10d6c8c72f14705f7503618be5e14
5
+ SHA512:
6
+ metadata.gz: 4ce92080d2e7db647f7d3f684daafc69a94ea14fd3c179e679b70eb3fa9de70ac3ba79536645d4f098e1612155f44a1bbba61003e3cae96901caf23ffc87d9f9
7
+ data.tar.gz: 27f16af88934df6524be839438f6a0fbba1f73456472093079c8537b68307148f90d02e9c63e8fbb8b07a56adb719226ae03086b8342343b346114681cad9f45
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+ .DS_store
14
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.15.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in fxer.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Sam Nissen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,115 @@
1
+ # Fxer
2
+
3
+ Fxer is an exchange rate calculator, using the European Central Bank's
4
+ rates covering the last 90 days.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'fxer'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ ```bash
17
+ bundle
18
+ ```
19
+
20
+ Or install it yourself as, replacing the version numbers:
21
+
22
+ ```bash
23
+ gem build fxer.gemspec
24
+ gem install fxer-1.2.3.gem
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Simplest exchange
30
+
31
+ Fxer includes a quick and simple way of obtaining an exchange rate via a
32
+ separate namespace:
33
+
34
+ ```ruby
35
+ ExchangeRate.at(Date.today, 'EUR', 'AUD')
36
+ # => 1.4732
37
+ ```
38
+
39
+ ### Configurable exchange
40
+
41
+ For situations where you need more control, the Fxer namespace provides
42
+ configuration:
43
+
44
+ ```ruby
45
+ exchanger = Fxer::Exchange.new.configure do |config|
46
+ config.permissive = true
47
+ config.source = :ecb
48
+ config.store = "/my/path/"
49
+ end
50
+
51
+ exchanger.convert_at_date(Date.today, 'GBP', 'USD')
52
+ # => 1.309507859949982
53
+ ```
54
+
55
+ #### `config.permissive`
56
+
57
+ Fxer by default uses the most recently available data at or before
58
+ the date indicated. Setting permissive to false changes that, in effect a
59
+ strict-mode, and an error will be raised if a date
60
+ doesn't have corresponding data.
61
+
62
+ #### `config.source`
63
+
64
+ Fxer is designed to accommodate code for additional sources
65
+ of exchange rate data. Source can only be `:ecb` as of now.
66
+
67
+ #### `config.store`
68
+
69
+ The configuration of store allows you to indicate where you have
70
+ locally stored your exchange data file, so that Fxer does not need
71
+ to download that data to determine the rate.
72
+
73
+ ### Executable exchange
74
+
75
+ fxer also provides an executable for getting rates in Bash:
76
+
77
+ ```bash
78
+ FXER_RATE_DATA_PATH="/my/path" fxer "2017-07-18" NOK HKD
79
+ # => 0.9689571804652662
80
+ ```
81
+
82
+ where the environment variable for local file hosting is optional.
83
+
84
+ ### Data retrieval (also executable)
85
+
86
+ And fxer will download new ECB data for you, in either Ruby or Bash:
87
+
88
+ ```ruby
89
+ ENV['FXER_RATE_DATA_DIRECTORY'] = "/my/path/"
90
+ Fxer::Fetcher::Ecb.download
91
+ ```
92
+
93
+ ```bash
94
+ FXER_RATE_DATA_DIRECTORY="/my/path/" fxer-fetcher ecb
95
+ ```
96
+
97
+ ## Development
98
+
99
+ After checking out the repo, run `bin/setup` to install dependencies.
100
+ Then, run `bundle exec rspec spec` to run the tests.
101
+ You can also run `bin/console` for an interactive prompt that
102
+ will allow you to experiment.
103
+
104
+ Until this is pushed to RubyGems and GitHub, there is no defined development
105
+ process.
106
+
107
+ ## Contributing
108
+
109
+ Bug reports and pull requests will be welcome once fxer is live
110
+ at https://github.com/samnissen/fxer.
111
+
112
+ ## License
113
+
114
+ The gem is available as open source under the terms of the
115
+ [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rake/release"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fxer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1 @@
1
+ :ecb_fx_rate_url: "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"
@@ -0,0 +1,5 @@
1
+ :exchange:
2
+ :default_source_key: :ecb
3
+ :valid_source_keys:
4
+ - :ecb
5
+ :default_permission: true
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fxer'
4
+
5
+ #
6
+ # fxer is a simple command line tool to exchange data
7
+ # using the default exchange rate data source and an
8
+ # optional local store. See more documentation in Fxer::Exchange,
9
+ # and in the README.
10
+ #
11
+
12
+ path = ENV['FXER_RATE_DATA_PATH']
13
+ opts = { :store => path }
14
+
15
+ exchanger = Fxer::Exchange.new(opts)
16
+ puts exchanger.convert_at_date(ARGV[0], ARGV[1], ARGV[2])
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fxer'
4
+
5
+ #
6
+ # fxer-fetcher is a commandline pre-fetcher for exchange rate data.
7
+ # with only one data source, this is intentionally kept simple.
8
+ # fxer-fetcher downloads the most recent available data from the ECB.
9
+ #
10
+
11
+ DEFAULT_FETCHER_DOWNLOAD_CLASS_NAME = "ecb"
12
+
13
+ case "#{ARGV[0] || DEFAULT_FETCHER_DOWNLOAD_CLASS_NAME}".to_sym
14
+ when :ecb
15
+ puts "\n\n\tFetching ECB data..."
16
+ Fxer::Fetcher::Ecb.download
17
+ puts "\tSuccess!\n\n"
18
+ else
19
+ raise Fxer::ExchangeRate::INVALID_SOURCE_MESSAGE
20
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "fxer/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fxer"
8
+ spec.version = Fxer::VERSION
9
+ spec.authors = ["Sam Nissen"]
10
+ spec.email = ["scnissen@gmail.com"]
11
+
12
+ spec.summary = "Convert currency based on external sources' rates"
13
+ spec.homepage = "https://github.com/samnissen/fxer"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "activesupport", "~> 5.2.3"
24
+
25
+ spec.add_development_dependency "bundler", "~> 2.1.4"
26
+ spec.add_development_dependency "rake", "~> 13.0.1"
27
+ spec.add_development_dependency "rake-release", "~> 1.2"
28
+ spec.add_development_dependency "rspec", "~> 3.9"
29
+ spec.add_development_dependency "nokogiri", "~> 1.10"
30
+ end
@@ -0,0 +1,24 @@
1
+ require "fxer"
2
+
3
+ #
4
+ # ExchangeRate is a convenience module with one method to pass through
5
+ # data to Fxer::Exchange.
6
+ #
7
+ # Names like "ExchangeRate", "Exchange", etc. are the names of existing gems.
8
+ # Also, while Fxer::Exchange has options and state, ExchangeRate is
9
+ # simple and stateless. See that class for options and other details.
10
+ #
11
+ module ExchangeRate
12
+
13
+ #
14
+ # at requires three arguments, a date, and two strings representing
15
+ # currencies. More details about the requirements are detailed in
16
+ # Exchange.
17
+ #
18
+ # Example:
19
+ # ExchangeRate.at(Date.today,'GBP','USD')
20
+ #
21
+ def self.at(date, base, counter)
22
+ Fxer::Exchange.new.convert_at_date(date, base, counter)
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ require "exchange_rate"
2
+ require "fxer/exchange"
3
+ require "fxer/fetcher"
4
+ require "fxer/version"
5
+
6
+ module Fxer
7
+ FXER_BASE_PATH = Pathname(__dir__).parent
8
+ FXER_CONFIGURATION_PATH = File.join(FXER_BASE_PATH, "config")
9
+ end
@@ -0,0 +1,110 @@
1
+ require "fxer/exchange/data"
2
+ require "fxer/exchange/data_source"
3
+ require "fxer/exchange/exchange_configuration"
4
+
5
+ require "yaml"
6
+ require "bigdecimal"
7
+
8
+ module Fxer
9
+ class Exchange
10
+ attr_writer :configuration
11
+
12
+ #
13
+ # initialize takes a hash of optional values:
14
+ # 1. :source - a symbol representing the source of the exchange data
15
+ # 2. :store - a string representing a directory where
16
+ # exchange data is stored, so that new data is not
17
+ # downloaded upon each exchange calculation.
18
+ # 3. :permissive - a boolean that, set to true, allows the exchange
19
+ # rate to be pulled from the nearest possible date. Set to false,
20
+ # an error will arise if no data exists for the date.
21
+ #
22
+ # Alternatively these same values can be provided through
23
+ # a configuraiton block, like so:
24
+ # exchanger = Fxer::Exchange.new.configure do |config|
25
+ # config.source = :ecb
26
+ # config.store = "/path/to/data"
27
+ # end
28
+ #
29
+ # Alternatively these same values can be provided directly
30
+ # exchanger = Fxer::Exchange.new
31
+ # exchanger.configuration.source = :ecb
32
+ # exchanger.configuration.store = "/path/to/data"
33
+ #
34
+ # And these values can be reset to their defaults,
35
+ # defined in config/fxer.yml.
36
+ #
37
+ # Guidance on setting up much of the configuration comes from:
38
+ # http://brandonhilkert.com/blog/ruby-gem-configuration-patterns/
39
+ #
40
+ def initialize(opts = {})
41
+ configure { |config|
42
+ config.source = opts[:source] if opts[:source]
43
+ config.store = opts[:store] if opts[:store]
44
+ config.permissive = opts[:permissive] if opts[:permissive]
45
+ }
46
+ end
47
+
48
+ def configuration
49
+ @configuration ||= ExchangeConfiguration.new
50
+ end
51
+ def reset
52
+ @configuration = ExchangeConfiguration.new
53
+ end
54
+ def configure
55
+ yield(configuration)
56
+ self
57
+ end
58
+
59
+ #
60
+ # convert_at_date takes these arguments:
61
+ # 1. date - A Date object for the day's rates to access.
62
+ # The closest available day will be accessed if the
63
+ # chosen day is not available, or date is otherwise invalid
64
+ # 2. base - Representing a country's currency,
65
+ # formatted per source requirements
66
+ # 3. counter - Same format as `base`
67
+ #
68
+ # and returns a float, the result of diving two floats
69
+ # representing the currency's relative value at the date specified.
70
+ #
71
+ # Example:
72
+ # opts = {
73
+ # :store => "/my/path/",
74
+ # :source => :ecb,
75
+ # :permissive => true
76
+ # }
77
+ # Fxer::Exchange.new(opts).convert_at_date(Date.today, "HKD", "NZD")
78
+ # => 1.12345
79
+ #
80
+ def convert_at_date(date, base, counter)
81
+ data_source = Fxer::Exchange::DataSource.new(options)
82
+ counter_rate, base_rate = data_source.exchange(date, base, counter, source)
83
+
84
+ Float(BigDecimal(counter_rate.rate.to_s) / BigDecimal(base_rate.rate.to_s))
85
+ end
86
+
87
+ private
88
+
89
+ #
90
+ # DataSource and its subclasses don't require the same level of
91
+ # convenient configuration -- so they get a Hash of options
92
+ # (instead of a ExchangeConfiguration object, for instance).
93
+ # options returns such a Hash.
94
+ #
95
+ def options
96
+ {
97
+ :store => @configuration.store,
98
+ :permissive => @configuration.permissive
99
+ }
100
+ end
101
+
102
+ #
103
+ # source is a convenience method for the configuration object's source,
104
+ # and it returns a Symbol.
105
+ #
106
+ def source
107
+ @configuration.source
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,84 @@
1
+ require "fxer/exchange/data/date"
2
+
3
+ module Fxer
4
+ class Exchange
5
+
6
+ DATE_ERROR_MESSAGE = "Unable to find your date:"
7
+
8
+ class Data
9
+ attr_accessor :dates, :transaction_date, :to, :from
10
+
11
+ #
12
+ # initalize instantiates dates as an array to contain its Date objects.
13
+ #
14
+ def initialize(opts = {})
15
+ @options = opts
16
+ @dates = []
17
+ end
18
+
19
+ #
20
+ # rates sifts the available data for the nearest date, then
21
+ # sifts through the currencies to find the base and counter rates.
22
+ #
23
+ # Note @transaction_date, @to, @from must be set before `rates`
24
+ # is called. Convenient methods exist for this purpose. For instance:
25
+ # rate_set = Fxer::Exchange::DataSource::Ecb.new(options).rate_sets
26
+ # rates = rate_set.at("2017-07-21").from("USD").to("GBP").rates
27
+ #
28
+ # rates returns an array of two Currency objects,
29
+ # [counter, base].
30
+ #
31
+ def rates
32
+ date = nearest_date
33
+
34
+ [
35
+ date.currencies.find{ |d| d.key == @to },
36
+ date.currencies.find{ |d| d.key == @from }
37
+ ]
38
+ end
39
+
40
+ #
41
+ # at, from and to are convenience methods to set the transaction date,
42
+ # base currency, and counter currency, respectively. They return self
43
+ # so that they can be chained. arguments are defined in Fxer::Exchange
44
+ #
45
+ def at(d)
46
+ @transaction_date = d
47
+ self
48
+ end
49
+ def from(f)
50
+ @from = f
51
+ self
52
+ end
53
+ def to(t)
54
+ @to = t
55
+ self
56
+ end
57
+
58
+ #
59
+ # transaction_date takes one argument, defined in Fxer::Exchange
60
+ # and normalizes it to a Date object
61
+ #
62
+ def transaction_date=(raw_date)
63
+ @transaction_date = Date.normalize_date(d)
64
+ end
65
+
66
+ private
67
+
68
+ #
69
+ # nearest_date chooses the date object matching the transaction date.
70
+ #
71
+ # If no date is matched, the next date can be chosen if the
72
+ # permissive flag evaluates to true. Otherwise it raises an error.
73
+ #
74
+ def nearest_date
75
+ d = @dates.find { |d| "#{d.date}" == "#{@transaction_date}" }
76
+ return d if d
77
+
78
+ raise "#{DATE_ERROR_MESSAGE} #{@transaction_date}" unless @options[:permissive]
79
+
80
+ dates.sort_by{ |d| d.date }.reverse.first
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,56 @@
1
+ require "fxer/exchange/data/date/currency"
2
+
3
+ module Fxer
4
+ class Exchange
5
+ class Data
6
+
7
+ #
8
+ # Fxer::Exchange::Data::Date must be namespaced or
9
+ # it will overwrite the existing Date class, resulting in warnings:
10
+ # "warning: toplevel constant Date referenced by X", and odd behavior.
11
+ # http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
12
+ #
13
+ class Fxer::Exchange::Data::Date
14
+ attr_accessor :currencies, :date
15
+
16
+ #
17
+ # In creating a date, create a shell to store this Date's currencies.
18
+ #
19
+ def initialize
20
+ @currencies = []
21
+ end
22
+
23
+ #
24
+ # Setting the date allows string or Date object. For more details,
25
+ # see the documentation for normalize_date.
26
+ #
27
+ def date=(raw_date)
28
+ @date = Date.normalize_date(raw_date)
29
+ end
30
+
31
+ #
32
+ # self.normalize_date takes one argument,
33
+ # 1. raw_date, either a string represnting a date, or a Date,
34
+ # where the former will be converted into the latter
35
+ # and returns a Date object if it is a convertable string.
36
+ # Note this is used elsewhere, hence the Eigenclass method.
37
+ #
38
+ # In normalize_date, casing the class of a ruby object is not done
39
+ # by `.class`ing the object because of how the === operator works:
40
+ # https://stackoverflow.com/a/3801609/1651458
41
+ #
42
+ # Also, `::Date` is required because of the local Date
43
+ # class in this Gem: https://stackoverflow.com/a/29351635/1651458
44
+ #
45
+ def self.normalize_date(raw_date)
46
+ case raw_date
47
+ when String
48
+ ::Date.strptime(raw_date) # A namespace that specifies Ruby's core class.
49
+ else
50
+ raw_date
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ module Fxer
2
+ class Exchange
3
+ class Data
4
+ class Date
5
+ class Currency
6
+ attr_accessor :key, :rate
7
+
8
+ #
9
+ # rate= takes one argument,
10
+ # 1. raw_rate, a string representing a float
11
+ # and converts it into a float, and assigns it to its rate attribute
12
+ #
13
+ def rate=(raw_rate)
14
+ @rate = "#{raw_rate}".to_f
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,48 @@
1
+ require "active_support/core_ext/hash"
2
+ require "date"
3
+ require "open-uri"
4
+ require "pathname"
5
+
6
+ require "fxer/exchange/data_source/ecb"
7
+
8
+ module Fxer
9
+ class Exchange
10
+ class DataSource
11
+
12
+ #
13
+ # initialize accepts one optional argument,
14
+ # 1. a hash of options which will be passed to whichever data source
15
+ # the user has indicated (i.e. Fxer::Exchange::DataSource::Ecb).
16
+ #
17
+ def initialize(opts = {})
18
+ @options = opts
19
+ end
20
+
21
+ #
22
+ # exchange takes 4 required arguments and returns the data required
23
+ # to determine an exchange value. Arguments are defined in Fxer::Exchange.
24
+ #
25
+ def exchange(date, base, counter, source)
26
+ fetch_rates(source).at(date).from(base).to(counter).rates
27
+ end
28
+
29
+ private
30
+
31
+ #
32
+ # fetch_rates takes one argument,
33
+ # 1. a symbol represnting a source's corresponding class name.
34
+ # Source symbols are defined in Fxer::Exchange.
35
+ #
36
+ # fetch_rates converts the source of data indicated by the user,
37
+ # i.e. :ecb, (which has been sanitized during configuration), and
38
+ # converts that to a subclass of DataSource, providing options
39
+ # and returning a set of rates, represented by an
40
+ # Fxer::Exchange::Data object.
41
+ #
42
+ def fetch_rates(source)
43
+ klass_name = "#{source}".downcase.capitalize
44
+ Object.const_get("Fxer::Exchange::DataSource::#{klass_name}").new(@options).rate_set
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,114 @@
1
+ module Fxer
2
+ class Exchange
3
+ class DataSource
4
+ class Ecb
5
+ attr_reader :rate_set
6
+
7
+ #
8
+ # initialize takes one optional hash.
9
+ # Arguments are defined in Fxer::Exchange.
10
+ #
11
+ # It assigns store to be used in the Ecb class. initialize also
12
+ # kicks off the request for data and normalization into our
13
+ # Data objects.
14
+ #
15
+ def initialize(opts = {})
16
+ @store = opts[:store]
17
+ @options = opts.except(:store)
18
+
19
+ fetch_rates
20
+ normalize_rates
21
+ end
22
+
23
+ private
24
+
25
+ #
26
+ # normalize_rates creates a Data object to store each Date, which
27
+ # itself stores n Currencies, all assigned to @rate_set
28
+ #
29
+ # In ECB data, the Euro is static, and so is omitted from the dataset.
30
+ # So, the Currency representing the Euro is manually added.
31
+ #
32
+ def normalize_rates
33
+ @rate_set = Fxer::Exchange::Data.new(@options)
34
+
35
+ @raw_data.each do |rate_sets|
36
+ date = Fxer::Exchange::Data::Date.new()
37
+ date.date = Date.strptime(rate_sets["time"], "%Y-%m-%d")
38
+
39
+ date.currencies = rate_sets["Cube"].map do |rate|
40
+ currency = Fxer::Exchange::Data::Date::Currency.new()
41
+ currency.key = rate["currency"]
42
+ currency.rate = rate["rate"]
43
+ currency
44
+ end
45
+ date.currencies << european_currency
46
+
47
+ @rate_set.dates << date
48
+ end
49
+ end
50
+
51
+ #
52
+ # european_currency creates the Euro currency object, and
53
+ # returns that static currency object.
54
+ #
55
+ # Since it is always the base of the ECB currency data,
56
+ # the Euro is always set to 1.
57
+ #
58
+ def european_currency
59
+ currency = Fxer::Exchange::Data::Date::Currency.new()
60
+ currency.key = "EUR"
61
+ currency.rate = 1
62
+ currency
63
+ end
64
+
65
+ #
66
+ # fetch_rates gathers the entire available data set. (For ECB,
67
+ # that's 90 days worth of exchange rate data.)
68
+ # First, fetch_rates tries to gather the data from file,
69
+ # then directly from the source URL when it cannot.
70
+ #
71
+ def fetch_rates
72
+ @raw_data = local_data
73
+ @raw_data ||= from_url
74
+ end
75
+
76
+ #
77
+ # from_url opens the ECB XML feed, converts to hash, and
78
+ # returns a Hash representing the relevant portion.
79
+ #
80
+ def from_url
81
+ hashify(open(config[:ecb_fx_rate_url]))
82
+ end
83
+
84
+ #
85
+ # local_data accesses the provided data file, converts to hash and
86
+ # returns a Hash representing the relevant portion, but
87
+ # only if one is indeed provided.
88
+ #
89
+ def local_data
90
+ return false unless @store && File.exists?(@store)
91
+
92
+ return hashify(File.open(@store))
93
+ end
94
+
95
+ #
96
+ # hashify converts uses activesupport to convert XML to hash,
97
+ # then accesses and returns a Hash representing the relevant portion.
98
+ #
99
+ def hashify(raw)
100
+ Hash.from_xml(raw)["Envelope"]["Cube"]["Cube"]
101
+ end
102
+
103
+ #
104
+ # config accesses the yaml file containing the
105
+ # default Ecb configuration.
106
+ #
107
+ def config
108
+ @config_path = File.join(FXER_CONFIGURATION_PATH, "ecb.yml")
109
+ @config ||= YAML.load_file(@config_path)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,70 @@
1
+ module Fxer
2
+ class Exchange
3
+
4
+ INVALID_SOURCE_MESSAGE = "Please provide one of these valid source keys:"
5
+
6
+ class ExchangeConfiguration
7
+ attr_accessor :source, :store, :permissive
8
+
9
+ #
10
+ # This configuration object allows for hash, block or object setting
11
+ # of configuration options.
12
+ #
13
+ # initialize sets the default source and protects the app from attempting
14
+ # to use invalid source keys.
15
+ #
16
+ def initialize
17
+ set_defaults
18
+ end
19
+
20
+ #
21
+ # In order to ensure that a key passed at any arbitrary point is valid
22
+ # the setter is overwritten to validate the key.
23
+ #
24
+ def source=(key)
25
+ @source = validate_source_key(key)
26
+ end
27
+
28
+ private
29
+
30
+ #
31
+ # set_defaults does what is says on the tin, setting source and store
32
+ # to their default values.
33
+ #
34
+ # Note that a nil value for the store path will bypass the functions
35
+ # that access locally stored data.
36
+ #
37
+ def set_defaults
38
+ @source = config[:default_source_key]
39
+ @store = config[:default_store_path]
40
+ @permissive = config[:default_permission]
41
+ end
42
+
43
+ #
44
+ # config accesses the yaml file containing the default configuration,
45
+ # and returns a Hash of its values.
46
+ #
47
+ def config
48
+ @config_path ||= File.join(FXER_CONFIGURATION_PATH, "fxer.yml")
49
+ @config ||= YAML.load_file(@config_path)[:exchange]
50
+ end
51
+
52
+
53
+ #
54
+ # validate_source_key takes an argument
55
+ # 1. key, a symbol representing
56
+ # and returns the key if it is within the array of valid keys
57
+ #
58
+ # This stops any request that specifies a key that that is not
59
+ # encoded in our application as a valid bank data source.
60
+ #
61
+ def validate_source_key(key)
62
+ unless config[:valid_source_keys].include?(key)
63
+ raise "#{INVALID_SOURCE_MESSAGE} #{config[:valid_source_keys]}."
64
+ end
65
+
66
+ key
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,79 @@
1
+ module Fxer
2
+ class Fetcher
3
+ class Ecb
4
+ class << self
5
+
6
+ #
7
+ # download fetches the most recent data from the ECB URL if
8
+ # today's data isn't already present in the user's chosen
9
+ # directory (otherwise it aborts).
10
+ #
11
+ # After downloading the ECB data, it checks if the data contains
12
+ # data not yet accounted for in the directory (otherwise it aborts).
13
+ #
14
+ # Then it saves that data to a new XML file in the user's
15
+ # chosen directory.
16
+ #
17
+ def download
18
+ set_data_parameters
19
+
20
+ return true if abort_if_current(Date.today.to_s)
21
+
22
+ fetch_data
23
+
24
+ return true if abort_if_current(@date)
25
+
26
+ save_data
27
+ end
28
+
29
+ private
30
+
31
+ #
32
+ # set_data_parameters fetches and assigns the ECB URL from config.
33
+ # And it assigns the user's chosen rate directory, falling back to
34
+ # the working directory.
35
+ #
36
+ def set_data_parameters
37
+ config_path = File.join(Fxer::FXER_CONFIGURATION_PATH, "ecb.yml")
38
+ @url = YAML.load_file(config_path)[:ecb_fx_rate_url]
39
+ @dir = ENV['FXER_RATE_DATA_DIRECTORY'] || Dir.pwd
40
+ end
41
+
42
+ #
43
+ # save_data to an XML file named after the @date
44
+ #
45
+ def save_data
46
+ @path = File.join(@dir, "#{@date.to_s}.xml")
47
+
48
+ puts "\tData found. Saving data for '#{@date.to_s}' to '#{@path}' ..."
49
+ open(@path, "wb") { |f| f.write(@data) }
50
+
51
+ puts "\tSuccess!\n\n"
52
+ end
53
+
54
+ #
55
+ # fetch_data from the URL, and fetch the file's
56
+ # most recent date from that data
57
+ #
58
+ def fetch_data
59
+ puts "\n\n\tGoing to fetch data from '#{@url}'"
60
+ @data = open(@url) { |io| io.read }
61
+ @date = Hash.from_xml(@data)["Envelope"]["Cube"]["Cube"].first["time"]
62
+ end
63
+
64
+ #
65
+ # abort_if_current takes one argument:
66
+ # 1. date - a string representing the date a file is named for,
67
+ # and returns a boolean of that file's existence.
68
+ #
69
+ def abort_if_current(date)
70
+ return false unless File.exist?(File.join(@dir, "#{date.to_s}.xml"))
71
+
72
+ puts "\n\n\tThe most recent data already exists in #{@dir}. Exiting ...\n\n"
73
+ true
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module Fxer
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fxer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sam Nissen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-05-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.4
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 13.0.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 13.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake-release
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
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.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: nokogiri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.10'
97
+ description:
98
+ email:
99
+ - scnissen@gmail.com
100
+ executables:
101
+ - fxer
102
+ - fxer-fetcher
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - ".travis.yml"
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - bin/console
114
+ - bin/setup
115
+ - config/ecb.yml
116
+ - config/fxer.yml
117
+ - exe/fxer
118
+ - exe/fxer-fetcher
119
+ - fxer.gemspec
120
+ - lib/exchange_rate.rb
121
+ - lib/fxer.rb
122
+ - lib/fxer/exchange.rb
123
+ - lib/fxer/exchange/data.rb
124
+ - lib/fxer/exchange/data/date.rb
125
+ - lib/fxer/exchange/data/date/currency.rb
126
+ - lib/fxer/exchange/data_source.rb
127
+ - lib/fxer/exchange/data_source/ecb.rb
128
+ - lib/fxer/exchange/exchange_configuration.rb
129
+ - lib/fxer/fetcher.rb
130
+ - lib/fxer/version.rb
131
+ homepage: https://github.com/samnissen/fxer
132
+ licenses:
133
+ - MIT
134
+ metadata: {}
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubygems_version: 3.1.2
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Convert currency based on external sources' rates
154
+ test_files: []