ecb_exchange 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: db431a48a2ca87303b94eff3c64e3c76793be304afd71ec248405500fbb74ae1
4
+ data.tar.gz: 6d8bb5e8b8b093553b11d7ad88b518e46428ae454905c856bd607776bc15365e
5
+ SHA512:
6
+ metadata.gz: 642b299a76308f93952b3cae6b73eebdd1c4d48cbbd737c3dae11371033a1b33e0b309bc2a0a0368825eb2e03c0812c1f1e1dfcfdac8e28bf861671c3d9b46a2
7
+ data.tar.gz: d1cf8857e13641d307f1c01f1eedc0e55246873663d478b788d410b10def30d3dc7d6ac46f1d3a2129bc5236fb821283d5374a40549b7fb34fcdc9951daa1a1e
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .byebug_history
@@ -0,0 +1,9 @@
1
+ SimpleCov.start do
2
+ add_filter '/test/'
3
+ add_filter '/vendor/'
4
+ end
5
+
6
+ SimpleCov.at_exit do
7
+ SimpleCov.result.format!
8
+ `open ./coverage/index.html` if RUBY_PLATFORM =~ /darwin/
9
+ end
@@ -0,0 +1,29 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.1.10
6
+ - 2.2.9
7
+ - 2.3.6
8
+ - 2.4.3
9
+ - 2.5.1
10
+ - ruby-head
11
+
12
+ before_install:
13
+ - gem update --system
14
+ - gem install bundler
15
+
16
+ matrix:
17
+ allow_failures:
18
+ - rvm: ruby-head
19
+
20
+ env:
21
+ global:
22
+ - CC_TEST_REPORTER_ID=2a6d225e3f463601e4e8e1b7a2f2a7c6c6b58dac59ccf77d03e784e3cc19a866
23
+ - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi)
24
+ before_script:
25
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
26
+ - chmod +x ./cc-test-reporter
27
+ - ./cc-test-reporter before-build - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi)
28
+ after_script:
29
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
@@ -0,0 +1,17 @@
1
+ # ChangeLog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](KeepAChangelog) and this project
6
+ adheres to [Semantic Versioning](Semver).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2018-05-05
11
+ ### Added
12
+ - Initial version released.
13
+
14
+ [Unreleased]: https://github.com/matthutchinson/ecb_exchange/compare/v0.1.0...HEAD
15
+ [0.1.0]: https://github.com/matthutchinson/ecb_exchange/master@{1day}...v0.1.0
16
+ [KeepAChangelog]: http://keepachangelog.com/en/1.0.0/
17
+ [Semver]: http://semver.org/spec/v2.0.0.html
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at matt@hiddenloop.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
@@ -0,0 +1,37 @@
1
+ ### Contributing
2
+
3
+ Pull Requests are welcome! To get started:
4
+
5
+ [Fork](https://guides.github.com/activities/forking) then clone the repository:
6
+
7
+ git clone git@github.com:your-username/ecb_exchange.git
8
+
9
+ Create your feature branch:
10
+
11
+ git checkout -b my-new-feature
12
+
13
+ When you are happy with your change, run the full test suite:
14
+
15
+ bundle exec rake
16
+
17
+ With a passing test suite, commit your changes, push and submit a new [Pull
18
+ Request](https://github.com/matthutchinson/ecb_exchange/compare):
19
+
20
+ git commit -am 'Added some feature'
21
+ git push origin my-new-feature
22
+
23
+ At this point you'll be waiting for one of our maintainers to review it. We will
24
+ try to reply to new pull requests within a few days. We might suggest some
25
+ changes, improvements or alternatives. To increase the chance that your pull
26
+ request gets accepted:
27
+
28
+ * Explain what your are doing (and why) in your pull request description.
29
+ * If you are fixing an
30
+ [issue](https://github.com/matthutchinson/ecb_exchange/issues), link to
31
+ it in your description and [mention
32
+ it](https://help.github.com/articles/closing-issues-via-commit-messages) in
33
+ the commit message.
34
+ * Write a good [commit
35
+ message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
36
+ * Write tests.
37
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ecb_exchange.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
@@ -0,0 +1,170 @@
1
+ # ECB Exchange
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/ecb_exchange.svg?style=flat)](http://rubygems.org/gems/ecb_exchange)
4
+ [![Travis Build Status](https://travis-ci.org/matthutchinson/ecb_exchange.svg?branch=master)](https://travis-ci.org/matthutchinson/ecb_exchange)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/c67969dd7b921477bdcc/maintainability)](https://codeclimate.com/github/matthutchinson/ecb_exchange/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/c67969dd7b921477bdcc/test_coverage)](https://codeclimate.com/github/matthutchinson/ecb_exchange/test_coverage)
7
+ [![Gem Dependency Status](https://gemnasium.com/badges/github.com/matthutchinson/ecb_exchange.svg)](https://gemnasium.com/github.com/matthutchinson/ecb_exchange)
8
+
9
+ Currency conversion using the European Central Bank's foreign [exchange
10
+ rates](http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml). Rates
11
+ for the last 90 days are fetched and cached on demand. All calculations are
12
+ performed and returned as `BigDecimal`, usually a [good
13
+ idea](https://makandracards.com/makandra/1178-bigdecimal-arithmetic-in-ruby)
14
+ when dealing with money.
15
+
16
+ ## Requirements
17
+
18
+ * Ruby >= 2.1.0
19
+
20
+ ## Installation
21
+
22
+ Add this line to your Gemfile and run `bundle install`:
23
+
24
+ ```ruby
25
+ gem 'ecb_exchange'
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ To convert an amount from one currency to another use:
31
+
32
+ ```ruby
33
+ ECB::Exchange.convert(100, from: 'EUR', to: 'GBP')
34
+ => 0.88235e2
35
+ ```
36
+
37
+ The converted amount (using today's current rate) will be returned (as a
38
+ `BigDecimal`). In doing so the gem will have fetched and cached ECB rates for
39
+ the last 90 days.
40
+
41
+ You can ask the exchange to convert an amount on a specific date:
42
+
43
+ ```ruby
44
+ ECB::Exchange.convert(100, from: 'EUR', to: 'GBP', date: Date.parse('2017-01-11'))
45
+ => 0.87235e2
46
+ ```
47
+
48
+ To return only the exchange rate multiplier between two currencies use:
49
+
50
+ ```ruby
51
+ ECB::Exchange.rate(from: 'EUR', to: 'USD')
52
+ => 0.11969e1
53
+ # you can pass an optional `date` argument to this method too
54
+ ```
55
+
56
+ You can ask for an array of supported currency codes with:
57
+
58
+ ```ruby
59
+ ECB::Exchange.currencies
60
+ => ["USD", "JPY", "BGN", "CZK", "DKK", "GBP", "HUF" ... ]
61
+ ```
62
+
63
+ Finally, you can adjust the rates endpoint by setting the
64
+ `XMLFeed.endpoint` (e.g. in an initializer):
65
+
66
+ ```ruby
67
+ ECB::Exchange::XMLFeed.endpoint = "http://my-awesome-service.com/feed.xml"
68
+ ```
69
+
70
+ The XML feed must conform to the [ECB
71
+ rates](http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml)
72
+ structure.
73
+
74
+ ## Handling Errors
75
+
76
+ Not all dates, rates or currencies may be available, or the remote endpoint
77
+ could be unresponsive. You should consider handling the following errors:
78
+
79
+ * `ECB::Exchange::DateNotFoundError`
80
+ * `ECB::Exchange::CurrencyNotFoundError`
81
+ * `ECB::Exchange::ResponseError`
82
+ * `ECB::Exchange::ParseError`
83
+
84
+ Or rescue `ECB::Exchange::Error` to catch any of them.
85
+
86
+ ## Caching
87
+
88
+ By default rates will be cached to one of the following backend stores (with
89
+ this order of preference).
90
+
91
+ * Your own backend cache store (see below)
92
+ * The `Rails.cache`
93
+ * An `ECB::Exchange::MemoryCache` instance (a simple in memory cache store)
94
+
95
+ To configure your own backend store:
96
+
97
+ ```ruby
98
+ ECB::Exchange::Cache.backend = MyAwesomeCache.new
99
+ # your cache must implement public `read(key)` and `write(key, value)` methods
100
+ ```
101
+
102
+ All keys in the cache are name-spaced with a `ecb_exchange_rates_for_date-`
103
+ prefix.
104
+
105
+ ## Development
106
+
107
+ Check out this repo and run `bin/setup`, this will install gem dependencies and
108
+ generate docs. Use `bundle exec rake` to run tests and generate a coverage
109
+ report.
110
+
111
+ You can also run `bin/console` for an interactive prompt allowing you to
112
+ experiment with the code.
113
+
114
+ ## Tests
115
+
116
+ MiniTest is used for testing. Run the test suite with:
117
+
118
+ $ rake test
119
+
120
+ ## Docs
121
+
122
+ Generate docs for this gem with:
123
+
124
+ $ rake rdoc
125
+
126
+ ## Troubles?
127
+
128
+ If you think something is broken or missing, please raise a new
129
+ [issue](https://github.com/matthutchinson/ecb_exchange/issues). Please remember
130
+ to check it hasn't already been raised.
131
+
132
+ ## Contributing
133
+
134
+ Bug [reports](https://github.com/matthutchinson/ecb_exchange/issues) and [pull
135
+ requests](https://github.com/matthutchinson/ecb_exchange/pulls) are welcome on
136
+ GitHub. When submitting pull requests, remember to add tests covering any new
137
+ behaviour, and ensure all tests are passing on
138
+ [Travis](https://travis-ci.org/matthutchinson/ecb_exchange). Read the
139
+ [contributing
140
+ guidelines](https://github.com/matthutchinson/ecb_exchange/blob/master/CONTRIBUTING.md)
141
+ for more details.
142
+
143
+ This project is intended to be a safe, welcoming space for collaboration, and
144
+ contributors are expected to adhere to the [Contributor
145
+ Covenant](http://contributor-covenant.org) code of conduct. See
146
+ [here](https://github.com/matthutchinson/ecb_exchange/blob/master/CODE_OF_CONDUCT.md)
147
+ for more details.
148
+
149
+
150
+ ## Todo
151
+
152
+ * Better rdoc documentation
153
+ * A small Rails app to demo this gem, with a one-click heroku install
154
+ * Allow `Net::HTTP` to be swapped out for any another HTTP client
155
+
156
+ ## License
157
+
158
+ The code is available as open source under the terms of
159
+ [LGPL-3](https://opensource.org/licenses/LGPL-3.0).
160
+
161
+ ## Links
162
+
163
+ * [Travis CI](https://travis-ci.org/matthutchinson/ecb_exchange)
164
+ * [Maintainability](https://codeclimate.com/github/matthutchinson/ecb_exchange/maintainability)
165
+ * [Test Coverage](https://codeclimate.com/github/matthutchinson/ecb_exchange/test_coverage)
166
+ * [RDoc](http://rdoc.info/projects/matthutchinson/ecb_exchange)
167
+ * [Issues](http://github.com/matthutchinson/ecb_exchange/issues)
168
+ * [Report a bug](http://github.com/matthutchinson/ecb_exchange/issues/new)
169
+ * [Gem](http://rubygems.org/gems/ecb_exchange)
170
+ * [GitHub](https://github.com/matthutchinson/ecb_exchange)
@@ -0,0 +1,31 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "rdoc/task"
4
+
5
+ # generate docs
6
+ RDoc::Task.new do |rd|
7
+ rd.main = "README.md"
8
+ rd.title = "ECB Exchange"
9
+ rd.rdoc_dir = "doc"
10
+ rd.options << "--all"
11
+ rd.rdoc_files.include("README.md", "LICENSE", "lib/**/*.rb")
12
+ end
13
+
14
+ # run tests
15
+ Rake::TestTask.new(:test) do |t|
16
+ t.libs << "test"
17
+ t.libs << "lib"
18
+ t.test_files = FileList["test/**/*_test.rb"]
19
+ end
20
+
21
+ # run tests with code coverage (default)
22
+ namespace :test do
23
+ desc "Run all tests and features and generate a code coverage report"
24
+ task :coverage do
25
+ ENV['COVERAGE'] = 'true'
26
+ Rake::Task['test'].execute
27
+ end
28
+ end
29
+
30
+
31
+ task :default => ['test:coverage']
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ecb_exchange"
5
+ require "irb"
6
+
7
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ bundle exec rake rdoc
@@ -0,0 +1,56 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ecb/exchange'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ecb_exchange"
8
+ spec.version = ECB::Exchange::VERSION
9
+ spec.authors = ["Matthew Hutchinson"]
10
+ spec.email = ["matt@hiddenloop.com"]
11
+
12
+ spec.license = "MIT"
13
+ spec.summary = <<-EOF
14
+ Finds and converts exchange rates based on available ECB reference rates from
15
+ the last 90 days
16
+ EOF
17
+
18
+ spec.description = <<-EOF
19
+ Finds (and caches) recent ECB reference rates, and provides an ExchangeRate
20
+ convertor between currencies supported by the ECB reference rate feed
21
+ EOF
22
+
23
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
24
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
25
+ if spec.respond_to?(:metadata)
26
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
27
+ else
28
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
29
+ end
30
+
31
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ spec.test_files = `git ls-files -- {test}/*`.split("\n")
33
+ spec.bindir = "bin"
34
+ spec.require_paths = ["lib"]
35
+
36
+ # documentation
37
+ spec.has_rdoc = true
38
+ spec.extra_rdoc_files = ['README.md', 'LICENSE']
39
+ spec.rdoc_options << '--title' << 'ECB Exchange' << '--main' << 'README.md' << '-ri'
40
+
41
+ # non-gem dependecies
42
+ spec.required_ruby_version = ">= 2.1.0"
43
+
44
+ # dev gems
45
+ spec.add_development_dependency "bundler"
46
+ spec.add_development_dependency "rake"
47
+ spec.add_development_dependency "pry-byebug"
48
+
49
+ # docs
50
+ spec.add_development_dependency "rdoc"
51
+
52
+ # testing
53
+ spec.add_development_dependency "webmock"
54
+ spec.add_development_dependency "minitest"
55
+ spec.add_development_dependency "simplecov"
56
+ end
@@ -0,0 +1,26 @@
1
+ require 'date'
2
+ require 'bigdecimal/util'
3
+
4
+ module ECB
5
+ module Exchange
6
+ VERSION = "0.1.0".freeze
7
+
8
+ def self.convert(amount, from:, to:, date: Date.today)
9
+ amount.to_d * rate(from: from, to: to, date: date)
10
+ end
11
+
12
+ def self.rate(from:, to:, date: Date.today)
13
+ rates = XMLFeed.rates(date)
14
+
15
+ [from, to].each do |currency|
16
+ raise CurrencyNotFoundError.new(currency) unless rates[currency]
17
+ end
18
+
19
+ rates[to].to_d * 1.to_d / rates[from].to_d
20
+ end
21
+
22
+ def self.currencies
23
+ XMLFeed.rates(Date.today).keys
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ module ECB
2
+ module Exchange
3
+ class Cache
4
+ KEY_PREFIX = "ecb_exchange_rates_for_date".freeze
5
+
6
+ class << self
7
+ attr_accessor :backend
8
+ end
9
+
10
+ def self.write(key, value)
11
+ store.write(cache_key(key), value)
12
+ end
13
+
14
+ def self.read(key)
15
+ store.read(cache_key(key))
16
+ end
17
+
18
+ # use backend if set (must respond to read, write), otherwise Rails.cache
19
+ # will be used (if available) or we fall back to use an in-memory cache
20
+ def self.store
21
+ if backend
22
+ backend
23
+ elsif defined?(Rails) && Rails.cache
24
+ Rails.cache
25
+ else
26
+ MemoryCache.cache
27
+ end
28
+ end
29
+
30
+ private
31
+ def self.cache_key(key)
32
+ "#{KEY_PREFIX}-#{key}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ module ECB
2
+ module Exchange
3
+ class Error < StandardError; end
4
+
5
+ class DateNotFoundError < Error
6
+ def initialize(date)
7
+ super("#{date} is missing or unavailable")
8
+ end
9
+ end
10
+
11
+ class CurrencyNotFoundError < Error
12
+ def initialize(currency_code)
13
+ super("#{currency_code} is missing or unavailable")
14
+ end
15
+ end
16
+
17
+ class ResponseError < Error
18
+ def initialize(url, error_details)
19
+ super("fetching '#{url}' failed - #{error_details}")
20
+ end
21
+ end
22
+
23
+ class ParseError < Error
24
+ def initialize(url)
25
+ super("parsing XML from '#{url}' failed")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module ECB
2
+ module Exchange
3
+ class MemoryCache
4
+ def self.cache
5
+ @cache ||= new
6
+ end
7
+
8
+ def initialize
9
+ @store = {}
10
+ end
11
+
12
+ def read(key)
13
+ @store[key]
14
+ end
15
+
16
+ def write(key, value)
17
+ @store[key] = value
18
+ end
19
+
20
+ def clear
21
+ @store.clear
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,87 @@
1
+ require "rexml/document"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module ECB
6
+ module Exchange
7
+ class XMLFeed
8
+
9
+ NINETY_DAY_ENDPOINT = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml".freeze
10
+ @endpoint = URI(NINETY_DAY_ENDPOINT)
11
+
12
+ # allow a configurable endpoint
13
+ class << self
14
+ attr_reader :endpoint
15
+ end
16
+
17
+ def self.endpoint=(new_endpoint)
18
+ @endpoint = URI(new_endpoint)
19
+ end
20
+
21
+ def self.rates(date)
22
+ if date > Date.today
23
+ raise ArgumentError.new("invalid date, must be today or in the past")
24
+ end
25
+
26
+ # find rates in cache, or fetch (and cache)
27
+ date = date.to_s
28
+ rates = Cache.read(date) || fetch_and_cache[date]
29
+ rates ? rates : raise(DateNotFoundError.new(date))
30
+ end
31
+
32
+ private
33
+
34
+ def self.fetch_and_cache
35
+ daily_rates = {}
36
+ parse(get_xml) do |date, rates|
37
+ daily_rates[date] = rates
38
+ # dont overwrite existing cached rates
39
+ Cache.write(date, rates) unless Cache.read(date)
40
+ end
41
+ daily_rates
42
+ end
43
+
44
+ def self.get_xml
45
+ resp = Net::HTTP.new(endpoint.host, endpoint.port).get(endpoint.path)
46
+ if resp.code == "200"
47
+ resp.body
48
+ else
49
+ raise ResponseError.new(endpoint, "status: #{resp.code}")
50
+ end
51
+ rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
52
+ Errno::EHOSTUNREACH, EOFError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
53
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
54
+ URI::InvalidURIError => exception
55
+ # catch and re-raise generic error with message
56
+ raise ResponseError.new(endpoint, exception.to_s)
57
+ end
58
+
59
+ def self.parse(xml)
60
+ elements = rate_elements(xml)
61
+ raise ParseError.new(endpoint) unless elements
62
+
63
+ elements.each do |element|
64
+ # map currency rates into a hash with currency keys, rate values
65
+ # and always add the base EUR rate multiplier
66
+ rates = parse_element(element).merge('EUR' => 1.0)
67
+
68
+ # pass date and rates to block
69
+ yield element.attributes['time'], rates
70
+ end
71
+ rescue REXML::ParseException, ArgumentError
72
+ raise ParseError.new(endpoint)
73
+ end
74
+
75
+ def self.rate_elements(xml)
76
+ REXML::Document.new(xml, ignore_whitespace_nodes: :all).elements["//Cube"]
77
+ end
78
+
79
+ def self.parse_element(element)
80
+ element.children.map(&:attributes).inject({}) do |memo, currency_with_rate|
81
+ memo[currency_with_rate['currency']] = currency_with_rate['rate']
82
+ memo
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ require 'ecb/exchange'
2
+ require 'ecb/exchange/xml_feed'
3
+ require 'ecb/exchange/memory_cache'
4
+ require 'ecb/exchange/cache'
5
+ require 'ecb/exchange/errors'
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ecb_exchange
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Hutchinson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
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: rake
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-byebug
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: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
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: |2
112
+ Finds (and caches) recent ECB reference rates, and provides an ExchangeRate
113
+ convertor between currencies supported by the ECB reference rate feed
114
+ email:
115
+ - matt@hiddenloop.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files:
119
+ - README.md
120
+ - LICENSE
121
+ files:
122
+ - ".gitignore"
123
+ - ".simplecov"
124
+ - ".travis.yml"
125
+ - CHANGELOG.md
126
+ - CODE_OF_CONDUCT.md
127
+ - CONTRIBUTING.md
128
+ - Gemfile
129
+ - LICENSE
130
+ - README.md
131
+ - Rakefile
132
+ - bin/console
133
+ - bin/setup
134
+ - ecb_exchange.gemspec
135
+ - lib/ecb/exchange.rb
136
+ - lib/ecb/exchange/cache.rb
137
+ - lib/ecb/exchange/errors.rb
138
+ - lib/ecb/exchange/memory_cache.rb
139
+ - lib/ecb/exchange/xml_feed.rb
140
+ - lib/ecb_exchange.rb
141
+ homepage:
142
+ licenses:
143
+ - MIT
144
+ metadata:
145
+ allowed_push_host: https://rubygems.org
146
+ post_install_message:
147
+ rdoc_options:
148
+ - "--title"
149
+ - ECB Exchange
150
+ - "--main"
151
+ - README.md
152
+ - "-ri"
153
+ require_paths:
154
+ - lib
155
+ required_ruby_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: 2.1.0
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ requirements: []
166
+ rubyforge_project:
167
+ rubygems_version: 2.7.6
168
+ signing_key:
169
+ specification_version: 4
170
+ summary: Finds and converts exchange rates based on available ECB reference rates
171
+ from the last 90 days
172
+ test_files: []