Rebrand, serve HTML

This commit is contained in:
Hakan Ensari 2018-04-16 16:19:15 +01:00
parent b30c23d0d8
commit 2c91173ecf
23 changed files with 411 additions and 78 deletions

View File

@ -1,5 +1,4 @@
APP_ENV=production
HTTPS_METHOD=noredirect
LETSENCRYPT_EMAIL=foo@bar.com
LETSENCRYPT_HOST=example.com,www.example.com
VIRTUAL_HOST=example.com,www.example.com
LETSENCRYPT_HOST=example.com
VIRTUAL_HOST=example.com

16
.eslintrc Normal file
View File

@ -0,0 +1,16 @@
{
"extends": ["airbnb-standard"],
"globals": {
"fetch": true,
"hljs": true
},
"rules": {
"no-new-func": [
"off"
],
"no-param-reassign": [
"error",
{ "props": false }
]
}
}

View File

@ -1 +1 @@
ruby 2.5.0
ruby 2.5.1

View File

@ -3,12 +3,12 @@ sudo: false
language: ruby
rvm:
- 2.5.0
- 2.5
addons:
postgresql: "9.6"
before_install:
- "psql -c 'create database fixer_test;' -U postgres"
- "psql -c 'create database frankfurter_test;' -U postgres"
bundler_args: "--without development"
env:

View File

@ -1,9 +1,9 @@
FROM ruby:2.5.0
FROM ruby:2.5.1
RUN mkdir /app
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install --jobs=8 --without development
RUN bundle install --jobs=8 --without development test
ADD . /app
CMD ["unicorn", "-c", "./config/unicorn.rb"]

View File

@ -2,7 +2,7 @@
source 'http://rubygems.org'
ruby '2.5.0'
ruby '2.5.1'
gem 'oj'
gem 'ox'
@ -14,7 +14,12 @@ gem 'sinatra'
gem 'unicorn'
group :development do
gem 'guard'
gem 'guard-livereload'
gem 'guard-minitest'
gem 'pry'
gem 'rack-livereload'
gem 'shotgun'
end
group :test do

View File

@ -7,19 +7,58 @@ GEM
coderay (1.1.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
et-orbi (1.0.9)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
et-orbi (1.1.2)
tzinfo
eventmachine (1.2.7)
ffi (1.9.25)
formatador (0.2.5)
fugit (1.1.1)
et-orbi (~> 1.1, >= 1.1.1)
raabro (~> 1.1)
guard (2.14.2)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
guard-livereload (2.5.2)
em-websocket (~> 0.5)
guard (~> 2.8)
guard-compat (~> 1.0)
multi_json (~> 1.8)
guard-minitest (2.4.6)
guard-compat (~> 1.2)
minitest (>= 3.0)
hashdiff (0.3.7)
http_parser.rb (0.6.0)
jaro_winkler (1.4.0)
kgio (2.11.2)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
lumberjack (1.0.13)
method_source (0.9.0)
minitest (5.11.3)
minitest-around (0.4.1)
minitest (~> 5.0)
multi_json (1.13.1)
mustermann (1.0.2)
oj (3.5.0)
ox (2.9.0)
nenv (0.3.0)
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
oj (3.6.2)
ox (2.9.2)
parallel (1.12.1)
parser (2.5.0.4)
parser (2.5.1.0)
ast (~> 2.4.0)
pg (1.0.0)
powerpack (0.1.1)
@ -27,16 +66,23 @@ GEM
coderay (~> 1.1.0)
method_source (~> 0.9.0)
public_suffix (3.0.2)
rack (2.0.4)
raabro (1.1.5)
rack (2.0.5)
rack-cors (1.0.2)
rack-protection (2.0.1)
rack-livereload (0.3.17)
rack
rack-test (0.8.3)
rack-protection (2.0.2)
rack
rack-test (1.0.0)
rack (>= 1.0, < 3)
rainbow (3.0.0)
raindrops (0.19.0)
rake (12.3.1)
rubocop (0.54.0)
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rubocop (0.57.0)
jaro_winkler (~> 1.4.0)
parallel (~> 1.10)
parser (>= 2.5)
powerpack (~> 0.1)
@ -44,28 +90,33 @@ GEM
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.9.0)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
ruby_dep (1.5.0)
rufus-scheduler (3.5.0)
fugit (~> 1.1, >= 1.1.1)
safe_yaml (1.0.4)
sequel (5.6.0)
sequel_pg (1.8.1)
sequel (5.9.0)
sequel_pg (1.8.2)
pg (>= 0.18.0)
sequel (>= 4.34.0)
sinatra (2.0.1)
shellany (0.0.1)
shotgun (0.9.2)
rack (>= 1.0)
sinatra (2.0.2)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.1)
rack-protection (= 2.0.2)
tilt (~> 2.0)
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.3.0)
unicode-display_width (1.4.0)
unicorn (5.4.0)
kgio (~> 2.6)
raindrops (~> 0.7)
vcr (4.0.0)
webmock (3.3.0)
webmock (3.4.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
@ -74,24 +125,29 @@ PLATFORMS
ruby
DEPENDENCIES
guard
guard-livereload
guard-minitest
minitest
minitest-around
oj
ox
pry
rack-cors
rack-livereload
rack-test
rake
rubocop
rufus-scheduler
sequel_pg
shotgun
sinatra
unicorn
vcr
webmock
RUBY VERSION
ruby 2.5.0p0
ruby 2.5.1p57
BUNDLED WITH
1.16.1

13
Guardfile Normal file
View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
guard 'livereload' do
extensions = %i[css js png gif jpg]
watch(%r{lib/web/public/.+\.(#{extensions * '|'})})
watch(%r{lib/web/views/.+\.erb$})
end
guard :minitest do
watch(%r{^spec/(.*)_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^spec/helper\.rb$}) { 'spec' }
end

View File

@ -1,8 +1,8 @@
# Fixer
# Frankfurter
[![Travis](https://travis-ci.org/hakanensari/fixer.svg)](https://travis-ci.org/hakanensari/fixer)
[![Travis](https://travis-ci.org/hakanensari/frankfurter.svg)](https://travis-ci.org/hakanensari/frankfurter)
Fixer is a free API for current and historical foreign exchange rates [published by the European Central Bank](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html).
Frankfurter is a free API for current and historical foreign exchange rates [published by the European Central Bank](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html).
Rates are updated around 4PM CET every working day.

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require './config/environment'
require 'api'
require 'web/server'
run Sinatra::Application

View File

@ -4,4 +4,4 @@ require 'pg'
require 'sequel'
Sequel.single_threaded = true
Sequel.connect(ENV['DATABASE_URL'] || "postgres://localhost/fixer_#{App.env}")
Sequel.connect(ENV['DATABASE_URL'] || "postgres://localhost/frankfurter_#{App.env}")

View File

@ -4,7 +4,7 @@ services:
restart: unless-stopped
web:
env_file: .env
image: hakanensari/fixer
image: hakanensari/frankfurter
logging:
options:
max-size: "50m"
@ -12,7 +12,7 @@ services:
restart: unless-stopped
scheduler:
env_file: .env
image: hakanensari/fixer
image: hakanensari/frankfurter
logging:
options:
max-size: "50m"

View File

@ -2,7 +2,7 @@
require 'currency'
class Quotation
class Quote
DEFAULT_BASE = 'EUR'
def initialize(amount: 1.0,
@ -15,7 +15,7 @@ class Quotation
@symbols = symbols
end
def quote
def to_h
{ base: @base, date: date, rates: calculate_rates }
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,29 @@
hljs.initHighlightingOnLoad();
// Make URLs clickable
document.querySelectorAll('.http').forEach((element) => {
element.onclick = () => {
if (element.innerHTML.indexOf('\n') > -1) {
const output = element.innerHTML.match(/^(.*)\n/)[1];
element.innerHTML = output;
} else {
const url = element.innerHTML.match(/>(\/[^<]*)/)[1];
fetch(url)
.then((resp) => {
const host = resp.url.match(/\/\/([^:/]+)/)[1];
element.insertAdjacentHTML('beforeend', `\nHost: ${host}\nContent-Type: ${resp.headers.get('Content-Type')}\nContent-Length: ${resp.headers.get('Content-Length')}\n\n`);
return resp.json();
})
.then((data) => {
element.insertAdjacentHTML('beforeend', `${JSON.stringify(data, undefined, 4)}`);
hljs.highlightBlock(element);
});
}
};
});
// Make the JavaScript examples clickable
document.querySelectorAll('.js').forEach((element) => {
const code = element.innerHTML.replace(/&gt;/g, '>');
element.onclick = () => new Function(code)();
});

View File

@ -0,0 +1,84 @@
body {
font-family: 'Open Sans', sans-serif;
}
.navbar .navbar-nav .nav-link {
font-size: .875rem;
}
.container {
max-width: 720px;
}
.section {
padding-top: 1em;
}
.hero .container {
text-align: center;
}
.hero .logo {
padding-bottom: 1em;
width: 120px;
}
.footer .container {
padding-bottom: 2em;
text-align: center;
}
.hljs {
background-color: #f8f9fa;
border: 2px solid #f8f9fa;
padding: 1em;
}
.hljs:hover {
border-bottom-color: #dee2e6;
cursor: pointer;
}
.hljs i {
color: #ced4da;
font-style: normal;
}
#carbonads {
display: none;
}
/*
@media screen and (max-width: 767px) {
#carbonads {
display: none;
}
}
@media screen and (min-width: 768px) {
#carbonads {
background-color: #f0f0f0;
position: absolute;
right: 24px;
top: 36px;
width: 150px;
}
.carbon-img,
.carbon-poweredby,
.carbon-text {
display: block;
margin: 10px;
}
.carbon-text,
.carbon-poweredby {
line-height: 1.3;
text-decoration: none;
}
.carbon-text {
font-size: 13px;
}
.carbon-poweredby {
font-size: 11px;
color: #999;
}
}
*/

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
require 'oj'
require 'sinatra'
require 'rack/cors'
require 'sinatra'
require 'query'
require 'quotation'
require 'quote'
use Rack::Cors do
allow do
@ -27,16 +27,17 @@ configure :test do
end
helpers do
def quotation
@quotation ||= begin
def quote
@quote ||= begin
query = Query.new(params)
Quotation.new(query.to_h)
Quote.new(query.to_h)
end
end
def jsonp(data)
def json(data)
json = Oj.dump(data, mode: :compat)
callback = params.delete('callback')
if callback
content_type :js
"#{callback}(#{json})"
@ -52,22 +53,27 @@ options '*' do
end
get '*' do
cache_control :public, :must_revalidate, max_age: 900
cache_control :public
pass
end
get '/' do
jsonp source: 'https://github.com/hakanensari/fixer'
erb :index
end
get '/latest' do
last_modified quotation.date
jsonp quotation.quote
get '/(?:latest|current)', mustermann_opts: { type: :regexp } do
last_modified quote.date
json quote.to_h
end
get '/(?<date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
last_modified quotation.date
jsonp quotation.quote
last_modified quote.date
json quote.to_h
end
get '/(?<start_date>\d{4}-\d{2}-\d{2})\.\.(?<end_date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
last_modified quote.end_date
json quote.to_h
end
not_found do

118
lib/web/views/index.erb Normal file
View File

@ -0,0 +1,118 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Frankfurter provides an open-source API for current and historical foreign exchange rates and currency conversion. The API tracks rates published daily by the European Central Bank.">
<title>Frankfurter</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:300,400">
<link rel="stylesheet" href="/stylesheets/application.css">
<link rel="shortcut icon" href="/images/frankfurter-icon.png">
</head>
<body>
<nav class="navbar navbar-expand">
<div class="container">
<ul class="navbar-nav mx-sm-auto">
<li class="nav-item">
<a class="nav-link" href="https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html" target="_blank">Data sets</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://status.frankfurter.app/" target="_blank">Status</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/hakanensari/frankfurter" target="_blank">Source</a>
</li>
</ul>
</div>
</nav>
<section class="hero section">
<div class="container">
<h1 class="display-4">Frankfurter</h1>
<p class="lead">
Foreign exchange rates and currency conversion API
</p>
<img alt="" class="logo" src="/images/frankfurter.png">
</div>
</section>
<section class="introduction section">
<div class="container">
<p>
Frankfurter is a lightweight API for querying current and historical forex rates published by the European Central Bank.
</p>
<p>
Rates update around 4PM CET on every working day. Historical data goes back to early 1999.
</p>
</div>
</section>
<section class="usage section">
<div class="container">
<h5>Usage</h5>
<p>
Get current foreign exchange rates.
</p>
<pre><code class="http hljs small">GET /current <i>HTTP/1.1</i></code></pre>
<p>
Get historical rates for any day since 1999.
</p>
<pre><code class="http hljs small">GET /1999-12-31 <i>HTTP/1.1</i></code></pre>
<p>
Rates are quoted against the Euro by default. Quote against a different
currency by setting the <strong>from</strong> parameter in your request.
</p>
<pre><code class="http hljs small">GET /current?from=USD <i>HTTP/1.1</i></code></pre>
<p>
Request specific exchange rates by setting the <strong>to</strong> parameter.
</p>
<pre><code class="http hljs small">GET /current?to=USD,GBP <i>HTTP/1.1</i></code></pre>
<p>
Convert a specific value.
</p>
<pre><code class="http hljs small">GET /current?amount=1000&from=GBP&to=EUR <i>HTTP/1.1</i></code></pre>
</div>
</section>
<section class="best-practices section">
<div class="container">
<h5>Best Practices</h5>
<p>
The primary use case of Frankfurter is client side. If you are converting currencies in the browser, you will appreciate the unencumbered access currently not possible with similar commercial services.
</p>
<pre><code class="js hljs small">// Fetch and display GBP/USD
fetch('/current?from=GBP&to=USD')
.then(resp => resp.json())
.then((data) => { alert(`GBP/USD = ${data.rates.USD}`); });</code></pre>
<p>
If you are working with rates on the server side, download them off the European Central Bank to avoid unnecessary indirection.
</p>
<p>
If you require more currencies or granular data, you are better off going with a commercial service.
</p>
<p>
Cache results whenever possible.
</p>
</div>
</section>
<section class="best-practices section">
<div class="container">
<h5>Notes</h5>
<p>
This project started out as <strong>Fixer</strong> in 2013. I renamed it to <strong>Frankfurter</strong> after selling the <a href="https://fixer.io" target="_blank">original domain</a> in early 2018.
<p>
The API is open source and comes with no warranty.
</p>
</div>
</section>
<section class="footer section">
<div class="container">
<iframe src="https://ghbtns.com/github-btn.html?user=hakanensari&repo=frankfurter&type=star&count=true" frameborder="0" scrolling="0" width="120px" height="20px"></iframe>
</div>
</section>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script src="/javascripts/application.js"></script>
<script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=fixerio" id="_carbonads_js"></script>
</body>
</html>

View File

View File

@ -2,7 +2,7 @@
require_relative 'helper'
require 'rack/test'
require 'api'
require 'web/server'
describe 'the API' do
include Rack::Test::Methods

View File

@ -1,35 +1,35 @@
# frozen_string_literal: true
require_relative 'helper'
require 'quotation'
require 'quote'
describe Quotation do
describe Quote do
describe 'by default' do
it 'quotes against the Euro' do
quotation = Quotation.new
rates = quotation.quote[:rates]
quote = Quote.new
rates = quote.to_h[:rates]
rates.keys.wont_include 'EUR'
end
end
describe 'when given a base' do
it 'quotes against that base' do
quotation = Quotation.new(base: 'USD')
rates = quotation.quote[:rates]
quote = Quote.new(base: 'USD')
rates = quote.to_h[:rates]
rates.keys.wont_include 'USD'
end
it 'sorts rates' do
quotation = Quotation.new(base: 'USD')
rates = quotation.quote[:rates]
quote = Quote.new(base: 'USD')
rates = quote.to_h[:rates]
rates.keys.must_equal rates.keys.sort
end
end
describe 'when given symbols' do
it 'quotes rates only for given symbols' do
quotation = Quotation.new(symbols: ['USD'])
rates = quotation.quote[:rates]
quote = Quote.new(symbols: ['USD'])
rates = quote.to_h[:rates]
rates.keys.must_include 'USD'
rates.keys.wont_include 'GBP'
end
@ -37,16 +37,16 @@ describe Quotation do
describe 'when given an amount' do
it 'quotes for that amount' do
quotation = Quotation.new(amount: 100)
rates = quotation.quote[:rates]
quote = Quote.new(amount: 100)
rates = quote.to_h[:rates]
rates['USD'].must_be :>, 10
end
end
describe 'when given an amount and symbols' do
it 'quotes for that amount' do
quotation = Quotation.new(amount: 100, symbols: ['USD'])
rates = quotation.quote[:rates]
quote = Quote.new(amount: 100, symbols: ['USD'])
rates = quote.to_h[:rates]
rates['USD'].must_be :>, 10
end
end

View File

@ -1,40 +1,40 @@
# frozen_string_literal: true
require_relative 'helper'
require_relative '../helper'
require 'rack/test'
require 'api'
require 'web/server'
describe 'the API' do
describe 'the server' do
include Rack::Test::Methods
let(:app) { Sinatra::Application }
let(:json) { Oj.load(last_response.body) }
let(:headers) { last_response.headers }
it 'describes itself' do
it 'has a homepage' do
get '/'
last_response.must_be :ok?
end
it 'returns latest quotes' do
get '/latest'
it 'returns current quotes' do
get '/current'
last_response.must_be :ok?
end
it 'sets base currency' do
get '/latest'
get '/current'
res = Oj.load(last_response.body)
get '/latest?base=USD'
get '/current?from=USD'
json.wont_equal res
end
it 'sets base amount' do
get '/latest?amount=10'
get '/current?amount=10'
json['rates']['USD'].must_be :>, 10
end
it 'filters symbols' do
get '/latest?symbols=USD'
get '/current?to=USD'
json['rates'].keys.must_equal %w[USD]
end
@ -50,21 +50,21 @@ describe 'the API' do
end
it 'returns a cache control header' do
%w[/ /latest /2012-11-20].each do |path|
%w[/ /current /2012-11-20].each do |path|
get path
headers['Cache-Control'].wont_be_nil
end
end
it 'returns a last modified header' do
%w[/latest /2012-11-20].each do |path|
%w[/current /2012-11-20].each do |path|
get path
headers['Last-Modified'].wont_be_nil
end
end
it 'allows cross-origin requests' do
%w[/ /latest /2012-11-20].each do |path|
%w[/ /current /2012-11-20].each do |path|
header 'Origin', '*'
get path
assert headers.key?('Access-Control-Allow-Methods')
@ -72,7 +72,7 @@ describe 'the API' do
end
it 'responds to preflight requests' do
%w[/ /latest /2012-11-20].each do |path|
%w[/ /current /2012-11-20].each do |path|
header 'Origin', '*'
header 'Access-Control-Request-Method', 'GET'
header 'Access-Control-Request-Headers', 'Content-Type'
@ -82,7 +82,14 @@ describe 'the API' do
end
it 'converts an amount' do
get '/latest?from=GBP&to=USD&amount=100'
get '/current?from=GBP&to=USD&amount=100'
json['rates']['USD'].must_be :>, 100
end
it 'returns rates for a given period' do
get '/2010-01-01..2010-12-31'
json['start_date'].must_equal '2010-01-01'
json['end_date'].must_equal '2010-12-31'
json['rates'].wont_be empty
end
end