REST Sunucusu + OpenAPI: Delphi'de Sözleşme Öncelikli API'ler | eSeGeCe Blog

REST Sunucusu + OpenAPI: Delphi'de Sözleşme Öncelikli API'ler

· Bileşenler
OpenAPI entegrasyonlu sgcWebSockets REST sunucusu

İlk iki makale bir REST sunucusunu elle kurdu: ARequestInfo.Document değerini karşılaştırıyor, fiile göre dallanıyor ve parametreleri kendiniz ayrıştırıyorsunuz. Bu işe yarar ve bir avuç uç nokta için en kısa yoldur. Belli bir boyutun ötesinde ise API yerine bakımını yaptığınız şey yönlendirme tablosu haline gelir.

TsgcWSAPIServer_OpenAPI diğer yaklaşımı benimser. Bir OpenAPI 3 belgesi yazarsınız, eklentiyi sunucuya bağlarsınız ve spesifikasyon yönlendirici olur: yolları eşleştirir, yol parametrelerini çıkarır, isteği doğrular, bildirilen güvenlik şemalarını uygular ve hem belgeyi hem de bir Swagger UI sayfasını sunar. Kodunuza gerçekten size ait olan kısım kalır, işlem başına bir işleyici.

Bağlantı tek bir atamadır

Eklenti sgcWebSocket_Server_API_OpenAPI biriminde yer alır. Server özelliğini ayarlamak onu sunucuya kaydeder ve o andan itibaren her HTTP isteği, OnCommandGet çalışmadan önce eklentiye sunulur.

uses
  sgcHTTP_REST_Server, sgcHTTP_OpenAPI_Server,
  sgcWebSocket_Server_API_OpenAPI;

FOpenAPI := TsgcWSAPIServer_OpenAPI.Create(self);
FOpenAPI.OpenAPIOptions.Endpoint.BasePath := '/openapi';
FOpenAPI.OpenAPIOptions.Endpoint.ServeSpec := True;
FOpenAPI.OpenAPIOptions.Endpoint.ServeSwaggerUI := True;
FOpenAPI.OnRequest := OpenAPIRequest;
FOpenAPI.LoadFromFile('C:\api\petstore.json');
FOpenAPI.Server := FServer;

Active diye bir özellik yoktur. Anahtar Server özelliğidir: ona atama yapmak eklentiyi bağlar, nil olarak ayarlamak ise ayırır, üstelik her ikisi de sunucu çalışmaya devam ederken. Ayrıldığında, spesifikasyonun sahip olduğu yollar basitçe olağan işleyicinize düşer.

FOpenAPI.Server := nil;   // detach, server keeps running

Spesifikasyonu yüklemek ve bir tuzak

Bir belgeyi yüklemenin üç yolu var ve hepsi aynı şekilde davranmıyor:

FOpenAPI.LoadFromFile('C:\api\petstore.json');   // parses immediately
FOpenAPI.LoadFromString(CS_SPEC);                // parses immediately
FOpenAPI.OpenAPIOptions.Endpoint.SpecFile := 'C:\api\petstore.json';  // lazy

SpecFile tembel olarak, ne spesifikasyon uç noktası ne de Swagger UI sayfası olan ilk istekte yüklenir. Bu ikisi yükleme gerçekleşmeden önce yanıtlanır, dolayısıyla yalnızca SpecFile ayarlıysa, en ilk GET /openapi/openapi.json isteği boş bir gövde döndürür. Belgenin ilk istekten itibaren eksiksiz olmasını istediğinizde, ki bu neredeyse her zaman böyledir, LoadFromFile veya LoadFromString kullanın.

Spesifikasyonun size bedavaya verdikleri

İki işlemli asgari bir belge:

{
  "openapi": "3.0.3",
  "info": { "title": "demo", "version": "1.0.0" },
  "servers": [ { "url": "/openapi" } ],
  "paths": {
    "/status": {
      "get": { "operationId": "getStatus",
        "responses": { "200": { "description": "server status" } } }
    },
    "/users/{username}": {
      "get": { "operationId": "getUser",
        "parameters": [ { "name": "username", "in": "path",
          "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "the account" },
                       "404": { "description": "no such account" } } }
    }
  }
}

BasePath değeri /openapi olarak ayarlandığında, yalnızca o belge çalışan dört URL üretir:

URLSunan
/openapi/openapi.jsonspesifikasyon belgesi
/openapi/docsSwagger UI
/openapi/statusgetStatus işlemi
/openapi/users/alicegetUser işlemi

İşlemleri ele almak

Sevk, yola veya fiile göre değil operationId değerine göre yapılır. OnRequest tetiklendiğinde motor rotayı çoktan eşleştirmiş ve yol parametrelerini doldurmuş olur, dolayısıyla işleyici onları ada göre okur:

procedure TForm1.OpenAPIRequest(Sender: TObject;
  const aOperationId: string; const aContext: TsgcOpenAPIServerContext;
  var Handled: Boolean);
var
  vName: string;
  oInfo: TsgcUserInfo;
begin
  if SameText(aOperationId, 'getStatus') then
  begin
    aContext.RespondJSON(200, '{"status":"running"}');
    Handled := True;
  end
  else if SameText(aOperationId, 'getUser') then
  begin
    vName := aContext.PathParamAsString('username');
    if FUsers.FindUser(vName, oInfo) then
      aContext.RespondJSON(200, '{"username":"' + oInfo.Username + '"}')
    else
      aContext.RespondError(404, 'Not Found', 'no such account');
    Handled := True;
  end;
end;

Handled değerini False olarak bırakmak anlamlıdır: motor o zaman işlemi adıyla belirterek 501 Not Implemented yanıtı verir. Spesifikasyonda bildirilmiş ama henüz yazılmamış bir işlem, kafa karıştırıcı bir 404 yerine tam olarak bunu bildirir.

Bağlam nesnesi isteğin tamamını ve yanıt yardımcılarını taşır:

vPage := aContext.QueryParamAsInteger('page', 1);
vDebug := aContext.QueryParamAsBoolean('debug', False);
vAuth := aContext.HeaderValue('Authorization');
oJSON := aContext.BodyAsJSON;

aContext.RespondJSON(201, '{"created":true}');
aContext.RespondError(422, 'Unprocessable', 'quantity must be positive');

RespondError bir RFC 7807 problem belgesi üretir, böylece hata biçimleri siz onları biçimlendirmeden API genelinde tutarlı olur.

Şemadan istek doğrulama

Doğrulama varsayılan olarak kapalıdır. Ana bayrağı hiçbir kapsam ayarlamadan açmak, spesifikasyonun bildirdiği her şeyi doğrular:

FOpenAPI.OpenAPIOptions.Validation.ValidateRequest := True;

Ya da yalnızca kontrol edilmesini istediğiniz kısımlarla sınırlayın:

FOpenAPI.OpenAPIOptions.Validation.ValidateRequest := True;
FOpenAPI.OpenAPIOptions.Validation.ValidatePathParams := True;
FOpenAPI.OpenAPIOptions.Validation.ValidateQueryParams := True;
FOpenAPI.OpenAPIOptions.Validation.ValidateRequestBody := False;

Başarısız olan bir istek, işleyiciniz çalışmadan önce 400 ve her hatayı listeleyen bir problem belgesiyle yanıtlanır:

{"type":"about:blank","title":"Bad Request","status":400,
 "detail":"Request validation failed",
 "errors":["parameter 'limit' must be integer"]}

OnValidationError hataları incelemenize ve kararı geçersiz kılmanıza olanak tanır. Continue parametresi False olarak gelir, dolayısıyla onu True yapmak bilinçli bir eylemdir:

procedure TForm1.OpenAPIValidationError(Sender: TObject;
  const aOperationId: string; const aErrors: TStringList;
  const aContext: TsgcOpenAPIServerContext; var Continue: Boolean);
begin
  DoLog(aOperationId + ': ' + aErrors.Text);
  Continue := False;   // answer 400
end;

Spesifikasyonda bildirilen güvenlik

EnforceSecurity açıkken, belgenin securitySchemes tanımları gelen isteklere uygulanır: bir başlıkta, sorguda veya çerezde API anahtarları, HTTP Basic, taşıyıcı belirteçler, OAuth2 ve OpenID Connect.

FOpenAPI.OpenAPIOptions.Security.EnforceSecurity := True;
FOpenAPI.OpenAPIOptions.Security.JWTSecret := GetSecretFromEnvironment;
FOpenAPI.OpenAPIOptions.Security.ValidateExpiration := True;
FOpenAPI.OpenAPIOptions.Security.Issuer := 'https://auth.example.com';
FOpenAPI.OpenAPIOptions.Security.Audience := 'my-api';

Taşıyıcı belirteçler JWTSecret değerine karşı doğrulanır. Bir HMAC gizli anahtarı olduğu gibi kullanılır; -----BEGIN içeren bir değer PEM açık anahtarı olarak kabul edilir ve RSA ile ECDSA algoritmalarını etkinleştirir. JWTSecret değerini boş bırakırsanız belirteç yalnızca varlık açısından kontrol edilir; bu da onu OnValidateBearer içinde kendiniz doğrulamak istediğinizde doğru ayardır:

procedure TForm1.OpenAPIValidateBearer(Sender: TObject;
  const aToken: string; const aContext: TsgcOpenAPIServerContext;
  var Valid: Boolean);
begin
  Valid := MyTokenService.Verify(aToken);
end;

Başarısızlıklar 401 ile yanıtlanır, istek kimlik doğrulamasını geçip yalnızca kapsamda yetersiz kaldıysa 403 ile. Bunlara karşılık gelen OnValidateAPIKey ve OnValidateBasic olayları vardır.

Kod var olmadan önce sahte yanıtlar

İşleyicisi olmayan bir işlem, spesifikasyonun kendi örneklerinden ve şemalarından yanıtlanabilir; bu da uygulama hâlâ yazılırken bir ön yüz ekibinin üretken olmasını sağlar:

FOpenAPI.OpenAPIOptions.Mock.Enabled := True;
FOpenAPI.OpenAPIOptions.Mock.StatusCode := 200;

Uygulanmış işlemler işleyicinizden yanıt vermeye devam eder; yalnızca ele alınmayanlar sahte yanıta düşer.

Swagger UI, çevrimdışı dahil

Arayüz sayfası <BasePath>/docs üzerinde sunulur ve CSS ile JavaScript dosyalarını varsayılan olarak herkese açık bir CDN'den çeker. Ağdan yalıtılmış bir makinede bu işe yaramaz, bu yüzden onu swagger-ui.css ve swagger-ui-bundle.js dosyalarını barındıran yerel bir klasöre yönlendirin; sayfa bunları kendisi sunsun:

FOpenAPI.OpenAPIOptions.Endpoint.SwaggerUIAssetsPath := 'C:\www\swagger';

Bunun yerine CDN'den belirli bir sürümü sabitlemek için SwaggerUIBaseURL özelliğini ayarlayın. ServeSwaggerUI özelliğini kapatmak sayfayı tamamen kaldırır; bu, bir üretim dağıtımı için makul bir seçimdir.

CORS: her iki politikayı da aynı değerlerle yapılandırın

İnsanları en çok yanıltan kısım bu, bu yüzden net olmakta fayda var. Sunucu ve OpenAPI motorunun her biri kendi CORS politikasına sahiptir ve kaynaklar arası bir çağrının farklı yarılarını yanıtlarlar:

Bu ayrım aynı zamanda başlıkların neden asla iki kez yayılmadığının da nedenidir, ki bir tarayıcı Access-Control-Allow-Origin başlığını birden fazla kez taşıyan bir yanıtı reddeder. Ancak bu, asıl bozulmaya yol açanın ikisinden yalnızca birini etkinleştirmek olduğu anlamına gelir:

Her ikisini de aynı değerlerle etkinleştirin. Bir kaynağı onaylayan bir ön kontrolün ardından başka bir kaynağa izin veren bir yanıt gelmesi de aynı şekilde reddedilir:

FServer.CORSOptions.Enabled := True;
FServer.CORSOptions.AllowOrigins := 'https://app.example.com';
FServer.CORSOptions.AllowHeaders := 'Content-Type, Authorization';
FServer.CORSOptions.AllowMethods := 'GET, POST, PUT, DELETE, OPTIONS';

FOpenAPI.OpenAPIOptions.CORS.Enabled := FServer.CORSOptions.Enabled;
FOpenAPI.OpenAPIOptions.CORS.AllowOrigins := FServer.CORSOptions.AllowOrigins;
FOpenAPI.OpenAPIOptions.CORS.AllowHeaders := FServer.CORSOptions.AllowHeaders;
FOpenAPI.OpenAPIOptions.CORS.AllowMethods := FServer.CORSOptions.AllowMethods;

İki stili birlikte kullanmak

Eklenti sunucuyu ele geçirmez. Her istek önce ona sunulur ve yalnızca spesifikasyonunun bildirdiği yolları yanıtlar; geri kalan her şey eskisi gibi OnCommandGet olayına ulaşır. Böylece sözleşme öncelikli bir bölüm, elle yazılmış rotaların, DocumentRoot üzerinden sunulan statik içeriğin ve önceki makaledeki /health ile /metrics uç noktalarının yanında, hepsi tek bir portta yaşayabilir.

Eklenti kimlik doğrulama kapısından sonra çalıştığı için sunucunun kendi kimlik doğrulaması geçerliliğini korur ve çok kiracılılık işlem işleyicisi çalışmadan önce çözümlenir, dolayısıyla FServer.Tenant OnRequest içinde de geçerlidir.

procedure TForm1.OpenAPIRequest(Sender: TObject;
  const aOperationId: string; const aContext: TsgcOpenAPIServerContext;
  var Handled: Boolean);
begin
  DoLog(aOperationId + ' tenant=' + FServer.Tenant);
  ...
end;

Eksiksiz bir sunucu

FServer := TsgcHTTPRESTServer.Create(self);
FServer.Port := 5876;
FServer.OnCommandGet := ServerCommandGet;

FOpenAPI := TsgcWSAPIServer_OpenAPI.Create(self);
FOpenAPI.OpenAPIOptions.Endpoint.BasePath := '/openapi';
FOpenAPI.OpenAPIOptions.Validation.ValidateRequest := True;
FOpenAPI.OnRequest := OpenAPIRequest;
FOpenAPI.LoadFromFile('C:\api\petstore.json');
FOpenAPI.Server := FServer;

FServer.Active := True;

Kullanıcı deposu, kiracılık, metrikler ve OpenAPI eklentisinin hepsinin tek bir sunucuda bulunduğu tam çalışan bir örnek, Demos\20.HTTP_Protocol\15.REST_Server altında REST Server demosu olarak sunulmaktadır.

En son derlemeyi sgcWebSockets indirme sayfasından indirin.