138 lines
4.9 KiB
Ruby
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
|