fxer 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,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: []