OpenAPI-Server für Delphi: TsgcWSAPIServer_OpenAPI

· Komponenten
OpenAPI-Server für Delphi: TsgcWSAPIServer_OpenAPI

Die Enterprise-Edition von sgcWebSockets enthält eine neue Komponente, TsgcWSAPIServer_OpenAPI, die eine OpenAPI-3-Beschreibung in einen laufenden REST-Server innerhalb deiner Delphi-Anwendung verwandelt. Leg sie auf ein Formular, verbinde sie mit einem HTTP-Server, übergib ihr eine Spezifikation, und die Routen, die Anfragevalidierung, die Fehlerantworten und die Live-Swagger-UI-Dokumentation werden für dich verdrahtet. Dieser Beitrag erläutert, wie die Komponente funktioniert, die beiden Möglichkeiten, sie zu steuern (spezifikationsbasiert und codebasiert), die wichtigsten Konfigurationsschalter und ein vollständiges Delphi-Beispiel, das du direkt in ein neues Projekt einfügen kannst.

Was die Komponente leistet

TsgcWSAPIServer_OpenAPI ist ein schlanker API-Server, der sich über den vorhandenen API-Server-Erweiterungspunkt in TsgcWebSocketHTTPServer einklinkt. Du hängst ihn an einen Server an, lädst eine OpenAPI-3.0-Spezifikation, und er führt bei jeder eingehenden HTTP-Anfrage vier Dinge aus:

Das Ergebnis ist, dass die Spezifikation zur einzigen verbindlichen Quelle wird: Ändere einen Pfad, einen Parameter oder einen Antwortcode im JSON, starte neu, und der Server übernimmt den neuen Vertrag, ohne dass Delphi-Code neu kompiliert werden muss.

Spezifikationsbasiert: eine bestehende OpenAPI-3-Datei laden

Wenn du bereits ein OpenAPI-3-Dokument hast (zum Beispiel eine aus einem API-Designer exportierte petstore.json), umfasst die Einrichtung im Wesentlichen drei Zeilen: Komponente erstellen, Spezifikation laden, Server anhängen. Alles Weitere ist Konfiguration und der OnRequest-Handler, der die eigentlichen Antworten erzeugt.

uses
  sgcWebSocket, sgcWebSocket_Classes,
  sgcWebSocket_Server_API_OpenAPI,
  sgcHTTP_OpenAPI_Server;

var
  WSServer: TsgcWebSocketHTTPServer;
  FOpenAPI: TsgcWSAPIServer_OpenAPI;
begin
  WSServer := TsgcWebSocketHTTPServer.Create(nil);
  WSServer.Port := 8080;

  FOpenAPI := TsgcWSAPIServer_OpenAPI.Create(nil);
  FOpenAPI.OnRequest := OnOpenAPIRequest;
  FOpenAPI.OnBeforeRequest := OnOpenAPIBeforeRequest;
  FOpenAPI.OnValidationError := OnOpenAPIValidationError;

  // Configuration
  FOpenAPI.OpenAPIOptions.Endpoint.ServeSpec := True;
  FOpenAPI.OpenAPIOptions.Endpoint.ServeSwaggerUI := True;
  FOpenAPI.OpenAPIOptions.CORS.Enabled := True;
  FOpenAPI.OpenAPIOptions.Validation.ValidateRequest := True;
  FOpenAPI.OpenAPIOptions.Validation.ValidateRequired := True;
  FOpenAPI.OpenAPIOptions.Validation.ValidateRequestBody := True;

  // Load spec and attach to server
  FOpenAPI.LoadFromFile('petstore.json');
  FOpenAPI.Server := WSServer;

  WSServer.Active := True;
  // Swagger UI:   http://localhost:8080/docs
  // Raw spec:     http://localhost:8080/openapi.json
end;

Das OnRequest-Ereignis wird pro operationId ausgelöst, der Zeichenkette, die du neben jeder Operation im Dokument eingetragen hast. Du schreibst einen Zweig pro Operation, liest die Eingaben aus dem Kontext und gibst eine Antwort aus:

procedure TForm1.OnOpenAPIRequest(Sender: TObject;
  const aOperationId: string; const aContext: TsgcOpenAPIServerContext;
  var Handled: Boolean);
var
  vId: Int64;
  vLimit: Integer;
begin
  Handled := True;
  if aOperationId = 'listPets' then
  begin
    vLimit := aContext.QueryParamAsInteger('limit', 100);
    aContext.RespondJSON(200, BuildPetsJSON(vLimit));
  end
  else if aOperationId = 'getPetById' then
  begin
    vId := aContext.PathParamAsInteger('petId');
    if FindPet(vId) then
      aContext.RespondJSON(200, PetAsJSON(vId))
    else
      aContext.RespondError(404, 'Not Found',
                            Format('Pet %d not found', [vId]));
  end
  else
    Handled := False;
end;

Das Objekt TsgcOpenAPIServerContext ist das Arbeitstier jedes Handlers. Es stellt Pfad- und Query-Parameter über den Namen bereit (PathParamAsString, PathParamAsInteger, QueryParamAsString, QueryParamAsInteger, QueryParamAsBoolean), Header-Zugriff über HeaderValue, den Body als Zeichenkette (BodyAsString) oder als vorgeparstes JSON (BodyAsJSON), dazu zwei Antwort-Hilfsmethoden: RespondJSON(code, content) für eine normale Nutzlast und RespondError(code, title, detail), das einen RFC 7807-förmigen Body aus type, title, status und detail schreibt, sodass Clients stets eine einheitliche Fehlerform erhalten.

Codebasiert: die Spezifikation aus Delphi-Attributen generieren

Der zweite Modus ist der umgekehrte Ablauf: Du deklarierst deine API als Delphi-Klasse mit Attributen, lässt den Scanner das OpenAPI-Dokument zur Laufzeit ausgeben und führst dieses Dokument der gleichen Komponente wieder zu. Es gibt nichts, was von Hand in JSON geschrieben werden müsste.

uses
  sgcHTTP_OpenAPI_Server_CodeFirst;

type
  [sgcServiceContract('Task Manager API',
                      'A simple task manager', '1.0.0')]
  [sgcRoute('/api/v1')]
  TTaskService = class
  public
    [sgcHttpGet]
    [sgcRoute('/tasks')]
    [sgcSummary('List all tasks')]
    [sgcResponse(200, 'A list of tasks')]
    procedure ListTasks([sgcFromQuery] const status: string); virtual;

    [sgcHttpGet]
    [sgcRoute('/tasks/{taskId}')]
    [sgcSummary('Get a task by ID')]
    [sgcResponse(200, 'The requested task')]
    [sgcResponse(404, 'Task not found')]
    procedure GetTask([sgcFromPath][sgcRequired]
                      const taskId: Integer); virtual;

    [sgcHttpPost]
    [sgcRoute('/tasks')]
    [sgcSummary('Create a new task')]
    [sgcResponse(201, 'Task created')]
    procedure CreateTask([sgcFromBody] const body: string); virtual;
  end;

Die Klasse selbst benötigt keine echten Methodenrümpfe, der Scanner liest sie über RTTI aus, die eigentliche Logik liegt weiterhin in deinem OnRequest-Handler. Die Spezifikation zu erzeugen und den Server zu starten erfordert nur wenige Zeilen:

var
  oScanner: TsgcOpenAPICodeFirstScanner;
  vSpec: string;
begin
  oScanner := TsgcOpenAPICodeFirstScanner.Create;
  try
    vSpec := oScanner.GenerateSpec(TTaskService);
  finally
    oScanner.Free;
  end;

  FOpenAPI.LoadFromString(vSpec);
  FOpenAPI.Server := WSServer;
  WSServer.Active := True;
end;

Der Attributsatz deckt die häufigen Fälle ab: Routing (sgcHttpGet, sgcHttpPost, sgcHttpPut, sgcHttpDelete, sgcHttpPatch, sgcRoute), Parameterbindung (sgcFromPath, sgcFromQuery, sgcFromHeader, sgcFromBody), Validierung (sgcRequired, sgcMinLength, sgcMaxLength, sgcRange, sgcPattern) und Dokumentation (sgcSummary, sgcDescription, sgcTag, sgcResponse).

Konfiguration im Detail

OpenAPIOptions ist in fünf persistente Unterobjekte gegliedert, damit du sie alle im Objektinspektor sehen kannst. Drei davon enthalten die alltäglichen Einstellungen:

Neben OnRequest lassen sich weitere Ereignisse in die Pipeline einklinken: OnBeforeRequest (setze Accept := False, um mit einem 403 Forbidden abzukürzen, nützlich für Ratenbegrenzung oder Protokollierung), OnAfterRequest (Nachverarbeitung, sobald du eine Antwort erzeugt hast), OnAuthenticate (setze Authenticated zurück, um mit 401 Unauthorized zu antworten, nachdem du ein Token oder eine Session geprüft hast) und OnException (Auffangereignis, mit dem du den HTTP-Status ändern kannst, bevor das Framework den Fehler-Body schreibt). Die beiden verbleibenden Unterobjekte, Security und Mock, bringen OnValidateAPIKey, OnValidateBasic und OnValidateBearer für die Schemes, die die Spec deklariert, sowie eine synthetische Antwort für eine Operation, die noch niemand implementiert hat.

Swagger UI von Haus aus

Mit ServeSwaggerUI := True veröffentlicht der Server eine Swagger-UI-Seite unter BasePath + '/docs', die die Spezifikation von BasePath + '/openapi.json' lädt. Öffne die URL in einem Browser, und du bekommst das standardmäßige Try-it-out-Erlebnis, gespeist aus deinem eigenen laufenden Server, ohne separaten Dokumentations-Build, ohne statischen Export. Kombiniert mit CORS ist dies der schnellste Weg, ein Backend an ein Frontend-Team oder an einen Partner zu übergeben, der gegen deine API integriert.

Bezug

Die Komponente ist Teil der Enterprise-Edition von sgcWebSockets und auf der Palettenseite SGC OpenAPI registriert. Zwei vollständige Demos, eine spezifikationsbasiert mit einer Petstore-JSON, eine codebasiert mit einem Task-Manager-Dienst, werden in Demos\23.OpenAPI mitgeliefert. Lade den aktuellen Build von der sgcWebSockets-Downloadseite herunter.

Fragen, Rückmeldungen oder Hilfe bei der Einbindung in ein bestehendes Projekt? Kontakt aufnehmen, du bekommst eine Antwort von den Personen, die den Code geschrieben haben.