Files
datapipeline/examples/demo_api.rb
T

138 lines
4.9 KiB
Ruby

#
# Kleine Demo-API für die REST-Beispiele, ohne Internet und ohne weitere Gems
#
# ruby examples/demo_api.rb # http://127.0.0.1:4567, Token: demo-token
#
# Endpunkte (alle mit Authorization: Bearer demo-token, sonst 401)
# GET /api/kunden?page=1&per_page=3 {"data": [...], "page": 1} die 2. Seite antwortet einmal mit 503
# GET /api/laender [{"code": "DE", "name": "Deutschland"}, ...]
# GET /api/bestellungen[?cursor=n] {"items": [...], "next": "/api/bestellungen?cursor=3"} oder "next": null
# POST /api/import ein Objekt oder ein Array; 422 ohne kundennr/name
# GET /api/import alles, was angenommen wurde
# PUT /api/archiv/<id> ein Objekt ablegen
# GET /api/archiv alles im Archiv
#
require 'json'
require 'socket'
require 'uri'
class DemoApi
TOKEN = 'demo-token'.freeze
KUNDEN = [
{ 'id' => 1, 'name' => 'Müller GmbH', 'land' => 'DE', 'umsatz' => '12500.50' },
{ 'id' => 2, 'name' => 'Huber & Partner', 'land' => 'AT', 'umsatz' => '8300' },
{ 'id' => 3, 'name' => 'Swiss Trade AG', 'land' => 'CH', 'umsatz' => '22000.75' },
{ 'id' => 4, 'name' => 'Rossi Srl', 'land' => 'IT', 'umsatz' => '4100.10' },
{ 'id' => 5, 'name' => 'Dupont SARL', 'land' => 'FR', 'umsatz' => 'n/a' },
{ 'id' => 6, 'name' => 'Nordic AB', 'land' => 'SE', 'umsatz' => '990' },
{ 'id' => 7, 'name' => 'Vaduz Treuhand', 'land' => 'LI', 'umsatz' => '15000' }
].freeze
LAENDER = [
{ 'code' => 'DE', 'name' => 'Deutschland' }, { 'code' => 'AT', 'name' => 'Österreich' },
{ 'code' => 'CH', 'name' => 'Schweiz' }, { 'code' => 'IT', 'name' => 'Italien' },
{ 'code' => 'FR', 'name' => 'Frankreich' }, { 'code' => 'LI', 'name' => 'Liechtenstein' }
].freeze
BESTELLUNGEN = (1..5).map { |i| { 'id' => i, 'kunde' => (i % 3) + 1, 'betrag' => i * 100.0 } }.freeze
attr_reader :imported, :archive
def self.start(port: 4567) = new(port).tap(&:start)
def initialize port
@server = TCPServer.new('127.0.0.1', port)
@imported = []
@archive = {}
@failed_once = false
end
def url = "http://127.0.0.1:#{@server.addr[1]}"
def start
@thread = Thread.new { loop { Thread.new(@server.accept) { |client| serve(client) } } }
self
end
def stop
@thread&.kill
@server.close
end
private
def serve client
method, target = client.gets.to_s.split
return unless method
headers = {}
while (line = client.gets) && line != "\r\n"
key, value = line.split(':', 2)
headers[key.downcase] = value.strip
end
body = headers['content-length'] ? client.read(headers['content-length'].to_i) : ''
path, query = target.split('?', 2)
params = URI.decode_www_form(query.to_s).to_h
status, payload = headers['authorization'] == "Bearer #{TOKEN}" ? route(method, path, params, body) : [401, { 'error' => 'token' }]
text = JSON.generate(payload)
client.write "HTTP/1.1 #{status} X\r\nContent-Type: application/json\r\nContent-Length: #{text.bytesize}\r\n" \
"Connection: close\r\n\r\n#{text}"
rescue StandardError => e
warn "DemoApi: #{e.class}: #{e.message}"
ensure
client.close
end
def route method, path, params, body
case [method, path]
in ['GET', '/api/kunden'] then kunden(params)
in ['GET', '/api/laender'] then [200, LAENDER]
in ['GET', '/api/bestellungen'] then bestellungen(params)
in ['POST', '/api/import'] then import(JSON.parse(body))
in ['GET', '/api/import'] then [200, @imported]
in ['PUT', %r{\A/api/archiv/\w+\z}] then archivieren(path.split('/').last, JSON.parse(body))
in ['GET', '/api/archiv'] then [200, @archive.values]
else [404, { 'error' => "#{method} #{path}" }]
end
end
def kunden params
page = params.fetch('page', '1').to_i
per_page = params.fetch('per_page', '3').to_i
if page == 2 && !@failed_once
@failed_once = true
return [503, { 'error' => 'kurz nicht verfügbar' }]
end
[200, { 'data' => KUNDEN.drop((page - 1) * per_page).first(per_page), 'page' => page }]
end
def bestellungen params
cursor = params.fetch('cursor', '0').to_i
items = BESTELLUNGEN.drop(cursor).first(2)
following = cursor + 2 < BESTELLUNGEN.size ? "/api/bestellungen?cursor=#{cursor + 2}" : nil
[200, { 'items' => items, 'next' => following }]
end
def archivieren id, data
@archive[id] = data
[200, { 'ok' => true }]
end
def import data
rows = data.is_a?(Array) ? data : [data]
invalid = rows.reject { |r| r['kundennr'] && r['name'] }
return [422, { 'error' => 'kundennr und name sind Pflicht', 'zeilen' => invalid }] if invalid.any?
@imported.concat(rows)
[201, { 'angenommen' => rows.size }]
end
end
if $PROGRAM_NAME == __FILE__
api = DemoApi.start(port: ENV.fetch('PORT', 4567).to_i)
puts "Demo-API auf #{api.url}, Token #{DemoApi::TOKEN} (Ctrl-C beendet)"
sleep
end