OpenAPI-Server für Delphi: TsgcWSAPIServer_OpenAPI

· Komponenten

Die Enterprise-Edition von sgcWebSockets enthält eine neue Komponente, TsgcWSAPIServer_OpenAPI, die eine OpenAPI-3-Beschreibung in einen laufenden REST-Server innerhalb Ihrer Delphi-Anwendung verwandelt. Legen Sie sie auf ein Formular, verbinden Sie sie mit einem HTTP-Server, übergeben Sie ihr eine Spezifikation — und die Routen, die Anfragevalidierung, die Fehlerantworten und die Live-Swagger-UI-Dokumentation werden für Sie 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 Sie direkt in ein neues Projekt einfügen können.

Was die Komponente leistet

TsgcWSAPIServer_OpenAPI ist ein schlanker API-Server, der sich über den vorhandenen API-Server-Erweiterungspunkt in TsgcWebSocketHTTPServer einklinkt. Sie hängen ihn an einen Server an, laden 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: Ändern Sie einen Pfad, einen Parameter oder einen Antwortcode im JSON, starten Sie neu, und der Server übernimmt den neuen Vertrag, ohne dass Delphi-Code neu kompiliert werden muss.

Spezifikationsbasiert: eine bestehende OpenAPI-3-Datei laden

Wenn Sie bereits ein OpenAPI-3-Dokument haben (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 Sie neben jeder Operation im YAML/JSON eingetragen haben. Sie schreiben einen Zweig pro Operation, lesen die Eingaben aus dem Kontext und geben 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 application/problem+json-Body im Stil von RFC 7807 erzeugt, sodass Clients stets eine einheitliche Fehlerform erhalten.

Codebasiert: die Spezifikation aus Delphi-Attributen generieren

Der zweite Modus ist der umgekehrte Ablauf: Sie deklarieren Ihre API als Delphi-Klasse mit Attributen, lassen den Scanner das OpenAPI-Dokument zur Laufzeit ausgeben und führen 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 Ihrem 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 drei persistente Unterobjekte gegliedert, damit Sie alle im Objektinspektor sehen können:

Neben OnRequest erlauben vier weitere Ereignisse, sich in die Pipeline einzuklinken: OnBeforeRequest (setzen Sie Accept := False, um abzukürzen, nützlich für Ratenbegrenzung oder Protokollierung), OnAfterRequest (Nachverarbeitung, sobald Sie eine Antwort erzeugt haben), OnAuthenticate (setzen Sie Authenticated := True, nachdem Sie ein Token oder eine Session geprüft haben) und OnException (Auffangereignis, mit dem Sie den HTTP-Status ändern können, bevor das Framework den Fehler-Body schreibt).

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. Öffnen Sie die URL in einem Browser, und Sie erhalten das standardmäßige Try-it-out-Erlebnis, gespeist aus Ihrem 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 Ihre 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. Laden Sie den aktuellen Build von der sgcWebSockets-Downloadseite herunter.

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