İ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:
| URL | Sunan |
|---|---|
/openapi/openapi.json | spesifikasyon belgesi |
/openapi/docs | Swagger UI |
/openapi/status | getStatus işlemi |
/openapi/users/alice | getUser 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:
- OPTIONS ön kontrolü her zaman sunucu tarafından yanıtlanır, motorun sahip olduğu bir yol için bile. Sunucu onu, istek eklentiye sunulmadan önce karşılar.
- Motorun sahip olduğu bir yoldaki asıl yanıt motor tarafından damgalanır. Sunucu kendi başlıklarını yalnızca eklentiler isteği geri çevirdikten sonra ekler.
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:
- Yalnızca sunucu: ön kontrol başarılı olur, ardından gerçek yanıt hiçbir CORS başlığı taşımaz ve tarayıcı onu engeller.
- Yalnızca motor: motor, sahip olmadıkları da dahil sunucunun her yolu için ön kontrolü yanıtlar, bu sırada elle yazdığınız rotalar,
/healthve/metricshiçbir başlık olmadan yanıt verir.
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.
