delphimvcframework/sources/MVCFramework.Middleware.Swagger.pas

443 lines
14 KiB
ObjectPascal
Raw Normal View History

2019-10-09 23:14:56 +02:00
// ***************************************************************************
//
// Delphi MVC Framework
//
2021-08-15 18:39:55 +02:00
// Copyright (c) 2010-2021 Daniele Teti and the DMVCFramework Team
2019-10-09 23:14:56 +02:00
//
// https://github.com/danieleteti/delphimvcframework
//
// Collaborators on this file:
// Jo<4A>o Ant<6E>nio Duarte (https://github.com/joaoduarte19)
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// *************************************************************************** }
2019-07-27 20:23:48 +02:00
unit MVCFramework.Middleware.Swagger;
{$I dmvcframework.inc}
2019-07-27 20:23:48 +02:00
interface
uses
MVCFramework,
Swag.Doc,
MVCFramework.Swagger.Commons,
Swag.Doc.SecurityDefinition,
Swag.Common.Types,
System.JSON;
2019-07-27 20:23:48 +02:00
type
TMVCSwaggerMiddleware = class(TInterfacedObject, IMVCMiddleware)
private
fEngine: TMVCEngine;
fSwaggerInfo: TMVCSwaggerInfo;
fSwagDocURL: string;
fJWTDescription: string;
fEnableBasicAuthentication: Boolean;
fHost: string;
fBasePath: string;
2019-07-27 20:23:48 +02:00
procedure DocumentApiInfo(const ASwagDoc: TSwagDoc);
procedure DocumentApiSettings(AContext: TWebContext; ASwagDoc: TSwagDoc);
procedure DocumentApiAuthentication(const ASwagDoc: TSwagDoc);
2019-07-27 20:23:48 +02:00
procedure DocumentApi(ASwagDoc: TSwagDoc);
2020-01-03 20:49:53 +01:00
procedure SortApiPaths(ASwagDoc: TSwagDoc);
2019-10-09 23:14:56 +02:00
procedure InternalRender(AContent: string; AContext: TWebContext);
2019-07-27 20:23:48 +02:00
public
2019-10-09 23:14:56 +02:00
constructor Create(const AEngine: TMVCEngine; const ASwaggerInfo: TMVCSwaggerInfo;
2019-11-03 16:16:35 +01:00
const ASwaggerDocumentationURL: string = '/swagger.json'; const AJWTDescription: string = JWT_DEFAULT_DESCRIPTION;
const AEnableBasicAuthentication: Boolean = False; const AHost: string = ''; const ABasePath: string = '');
2019-07-27 20:23:48 +02:00
destructor Destroy; override;
procedure OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
procedure OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName: string;
const AActionName: string; var AHandled: Boolean);
procedure OnAfterControllerAction(AContext: TWebContext; const AActionName: string; const AHandled: Boolean);
procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
2019-07-27 20:23:48 +02:00
end;
implementation
uses
System.SysUtils,
MVCFramework.Commons,
System.Classes,
JsonDataObjects,
System.Rtti,
Swag.Doc.Path,
Swag.Doc.Path.Operation,
Swag.Doc.Path.Operation.Response,
MVCFramework.Middleware.JWT,
Swag.Doc.Path.Operation.RequestParameter,
Swag.Doc.SecurityDefinitionApiKey,
Swag.Doc.SecurityDefinitionBasic,
2020-01-03 20:49:53 +01:00
Swag.Doc.Definition,
System.Generics.Collections,
2020-01-03 20:49:53 +01:00
System.Generics.Defaults;
2019-07-27 20:23:48 +02:00
{ TMVCSwaggerMiddleware }
constructor TMVCSwaggerMiddleware.Create(const AEngine: TMVCEngine; const ASwaggerInfo: TMVCSwaggerInfo;
const ASwaggerDocumentationURL, AJWTDescription: string; const AEnableBasicAuthentication: Boolean;
const AHost, ABasePath: string);
2019-07-27 20:23:48 +02:00
begin
inherited Create;
fSwagDocURL := ASwaggerDocumentationURL;
fEngine := AEngine;
fSwaggerInfo := ASwaggerInfo;
fJWTDescription := AJWTDescription;
fEnableBasicAuthentication := AEnableBasicAuthentication;
fHost := AHost;
fBasePath := ABasePath;
2019-07-27 20:23:48 +02:00
end;
destructor TMVCSwaggerMiddleware.Destroy;
begin
inherited Destroy;
end;
procedure TMVCSwaggerMiddleware.DocumentApi(ASwagDoc: TSwagDoc);
var
lRttiContext: TRttiContext;
lObjType: TRttiType;
lController: TMVCControllerDelegate;
lSwagPath: TSwagPath;
lAttr: TCustomAttribute;
lControllerPath: string;
lMethodPath: string;
lMethod: TRttiMethod;
lFoundAttr: Boolean;
lMVCHttpMethods: TMVCHTTPMethods;
lSwagPathOp: TSwagPathOperation;
2019-07-27 20:23:48 +02:00
I: TMVCHTTPMethodType;
lPathUri: string;
lIndex: Integer;
lAuthTypeName: string;
lIsIgnoredPath: Boolean;
2019-07-27 20:23:48 +02:00
begin
lRttiContext := TRttiContext.Create;
2019-07-27 20:23:48 +02:00
try
for lController in fEngine.Controllers do
2019-07-27 20:23:48 +02:00
begin
lControllerPath := '';
lObjType := lRttiContext.GetType(lController.Clazz);
for lAttr in lObjType.GetAttributes do
2019-07-27 20:23:48 +02:00
begin
if lAttr is MVCSwagIgnorePathAttribute then
2019-07-27 20:23:48 +02:00
begin
lControllerPath := '';
2019-07-27 20:23:48 +02:00
Break;
end;
if lAttr is MVCPathAttribute then
begin
lControllerPath := MVCPathAttribute(lAttr).Path;
end;
2019-07-27 20:23:48 +02:00
end;
if lControllerPath.IsEmpty then
2019-07-27 20:23:48 +02:00
Continue;
for lMethod in lObjType.GetDeclaredMethods do
2019-07-27 20:23:48 +02:00
begin
lIsIgnoredPath := False;
lFoundAttr := False;
lMVCHttpMethods := [];
lMethodPath := '';
2019-07-27 20:23:48 +02:00
for lAttr in lMethod.GetAttributes do
2019-07-27 20:23:48 +02:00
begin
if lAttr is MVCSwagIgnorePathAttribute then
begin
lIsIgnoredPath := True;
end;
if lAttr is MVCPathAttribute then
2019-07-27 20:23:48 +02:00
begin
lMethodPath := MVCPathAttribute(lAttr).Path;
lFoundAttr := True;
2019-07-27 20:23:48 +02:00
end;
if lAttr is MVCHTTPMethodsAttribute then
2019-07-27 20:23:48 +02:00
begin
lMVCHttpMethods := MVCHTTPMethodsAttribute(lAttr).MVCHTTPMethods;
2019-07-27 20:23:48 +02:00
end;
end;
if (not lIsIgnoredPath) and lFoundAttr then
2019-07-27 20:23:48 +02:00
begin
lSwagPath := nil;
lPathUri := TMVCSwagger.MVCPathToSwagPath(lControllerPath + lMethodPath);
for lIndex := 0 to Pred(ASwagDoc.Paths.Count) do
2019-08-05 16:59:35 +02:00
begin
if SameText(ASwagDoc.Paths[lIndex].Uri, lPathUri) then
2019-08-05 16:59:35 +02:00
begin
lSwagPath := ASwagDoc.Paths[lIndex];
2019-08-05 16:59:35 +02:00
Break;
end;
end;
if not Assigned(lSwagPath) then
2019-08-05 16:59:35 +02:00
begin
lSwagPath := TSwagPath.Create;
lSwagPath.Uri := lPathUri;
ASwagDoc.Paths.Add(lSwagPath);
2019-08-05 16:59:35 +02:00
end;
2019-07-27 20:23:48 +02:00
for I in lMVCHttpMethods do
2019-07-27 20:23:48 +02:00
begin
lSwagPathOp := TSwagPathOperation.Create;
TMVCSwagger.FillOperationSummary(lSwagPathOp, lMethod, ASwagDoc.Definitions, I);
if TMVCSwagger.MethodRequiresAuthentication(lMethod, lObjType, lAuthTypeName) then
2019-11-03 16:16:35 +01:00
begin
lSwagPathOp.Security.Add(lAuthTypeName);
2019-11-03 16:16:35 +01:00
end;
lSwagPathOp.Parameters.AddRange(TMVCSwagger.GetParamsFromMethod(lSwagPath.Uri, lMethod,
ASwagDoc.Definitions));
lSwagPathOp.Operation := TMVCSwagger.MVCHttpMethodToSwagPathOperation(I);
lSwagPath.Operations.Add(lSwagPathOp);
2019-07-27 20:23:48 +02:00
end;
end;
end;
end;
finally
lRttiContext.Free;
2019-07-27 20:23:48 +02:00
end;
end;
procedure TMVCSwaggerMiddleware.DocumentApiInfo(const ASwagDoc: TSwagDoc);
begin
ASwagDoc.Info.Title := fSwaggerInfo.Title;
ASwagDoc.Info.Version := fSwaggerInfo.Version;
ASwagDoc.Info.TermsOfService := fSwaggerInfo.TermsOfService;
ASwagDoc.Info.Description := fSwaggerInfo.Description;
ASwagDoc.Info.Contact.Name := fSwaggerInfo.ContactName;
ASwagDoc.Info.Contact.Email := fSwaggerInfo.ContactEmail;
ASwagDoc.Info.Contact.Url := fSwaggerInfo.ContactUrl;
ASwagDoc.Info.License.Name := fSwaggerInfo.LicenseName;
ASwagDoc.Info.License.Url := fSwaggerInfo.LicenseUrl;
2019-07-27 20:23:48 +02:00
end;
procedure TMVCSwaggerMiddleware.DocumentApiAuthentication(const ASwagDoc: TSwagDoc);
var
lMiddleware: IMVCMiddleware;
lJWTMiddleware: TMVCJWTAuthenticationMiddleware;
lRttiContext: TRttiContext;
lObjType: TRttiType;
lJwtUrlField: TRttiField;
lJwtUrlSegment: string;
lSecurityDefsBearer: TSwagSecurityDefinitionApiKey;
lSecurityDefsBasic: TSwagSecurityDefinitionBasic;
begin
lJWTMiddleware := nil;
for lMiddleware in fEngine.Middlewares do
begin
if lMiddleware is TMVCJWTAuthenticationMiddleware then
begin
lJWTMiddleware := lMiddleware as TMVCJWTAuthenticationMiddleware;
Break;
end;
end;
if Assigned(lJWTMiddleware) or fEnableBasicAuthentication then
begin
lSecurityDefsBasic := TSwagSecurityDefinitionBasic.Create;
lSecurityDefsBasic.SchemeName := SECURITY_BASIC_NAME;
lSecurityDefsBasic.Description := 'Send username and password for authentication';
ASwagDoc.SecurityDefinitions.Add(lSecurityDefsBasic);
end;
if Assigned(lJWTMiddleware) then
begin
lRttiContext := TRttiContext.Create;
try
lObjType := lRttiContext.GetType(lJWTMiddleware.ClassInfo);
lJwtUrlField := lObjType.GetField('FLoginURLSegment');
if Assigned(lJwtUrlField) then
begin
lJwtUrlSegment := lJwtUrlField.GetValue(lJWTMiddleware).AsString;
if lJwtUrlSegment.StartsWith(ASwagDoc.BasePath) then
lJwtUrlSegment := lJwtUrlSegment.Remove(0, ASwagDoc.BasePath.Length);
if not lJwtUrlSegment.StartsWith('/') then
lJwtUrlSegment.Insert(0, '/');
// Path operation Middleware JWT
ASwagDoc.Paths.Add(TMVCSwagger.GetJWTAuthenticationPath(lJwtUrlSegment,
lJWTMiddleware.UserNameHeaderName, lJWTMiddleware.PasswordHeaderName));
// Methods that have the MVCRequiresAuthentication attribute use bearer authentication.
lSecurityDefsBearer := TSwagSecurityDefinitionApiKey.Create;
lSecurityDefsBearer.SchemeName := SECURITY_BEARER_NAME;
lSecurityDefsBearer.InLocation := kilHeader;
lSecurityDefsBearer.Name := 'Authorization';
lSecurityDefsBearer.Description := fJWTDescription;
ASwagDoc.SecurityDefinitions.Add(lSecurityDefsBearer);
end;
finally
lRttiContext.Free;
end;
end;
end;
2019-07-27 20:23:48 +02:00
procedure TMVCSwaggerMiddleware.DocumentApiSettings(AContext: TWebContext; ASwagDoc: TSwagDoc);
begin
ASwagDoc.Host := fHost;
if ASwagDoc.Host.IsEmpty then
begin
ASwagDoc.Host := Format('%s:%d', [AContext.Request.RawWebRequest.Host, AContext.Request.RawWebRequest.ServerPort]);
end;
2019-07-27 20:23:48 +02:00
ASwagDoc.BasePath := fBasePath;
2019-07-27 20:23:48 +02:00
if ASwagDoc.BasePath.IsEmpty then
begin
ASwagDoc.BasePath := fEngine.Config[TMVCConfigKey.PathPrefix];
end;
if ASwagDoc.BasePath.IsEmpty then
begin
2019-07-27 20:23:48 +02:00
ASwagDoc.BasePath := '/';
end;
2019-07-27 20:23:48 +02:00
ASwagDoc.Schemes := [tpsHttp, tpsHttps];
end;
procedure TMVCSwaggerMiddleware.InternalRender(AContent: string; AContext: TWebContext);
var
LContentType: string;
LEncoding: TEncoding;
begin
LContentType := BuildContentType(TMVCMediaType.APPLICATION_JSON, TMVCConstants.DEFAULT_CONTENT_CHARSET);
AContext.Response.RawWebResponse.ContentType := LContentType;
LEncoding := TEncoding.GetEncoding(TMVCConstants.DEFAULT_CONTENT_CHARSET);
try
AContext.Response.SetContentStream(TBytesStream.Create(TEncoding.Convert(TEncoding.Default, LEncoding,
2019-10-09 23:14:56 +02:00
TEncoding.Default.GetBytes(AContent))), LContentType);
2019-07-27 20:23:48 +02:00
finally
LEncoding.Free;
end;
end;
procedure TMVCSwaggerMiddleware.OnAfterControllerAction(AContext: TWebContext; const AActionName: string;
const AHandled: Boolean);
begin
// do nothing
end;
procedure TMVCSwaggerMiddleware.OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
begin
// do nothing
2019-07-27 20:23:48 +02:00
end;
2019-10-09 23:14:56 +02:00
procedure TMVCSwaggerMiddleware.OnBeforeControllerAction(AContext: TWebContext;
const AControllerQualifiedClassName, AActionName: string; var AHandled: Boolean);
2019-07-27 20:23:48 +02:00
begin
// do nothing
2019-07-27 20:23:48 +02:00
end;
procedure TMVCSwaggerMiddleware.OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
var
LSwagDoc: TSwagDoc;
begin
if SameText(AContext.Request.PathInfo, fSwagDocURL) and (AContext.Request.HTTPMethod in [httpGET, httpPOST]) then
2019-07-27 20:23:48 +02:00
begin
LSwagDoc := TSwagDoc.Create;
2019-10-09 23:14:56 +02:00
try
2019-08-13 16:57:42 +02:00
DocumentApiInfo(LSwagDoc);
DocumentApiSettings(AContext, LSwagDoc);
DocumentApiAuthentication(LSwagDoc);
2019-08-13 16:57:42 +02:00
DocumentApi(LSwagDoc);
2020-01-03 20:49:53 +01:00
SortApiPaths(LSwagDoc);
2019-08-13 16:57:42 +02:00
LSwagDoc.GenerateSwaggerJson;
InternalRender(LSwagDoc.SwaggerJson.ToJSON, AContext);
2019-08-13 16:57:42 +02:00
AHandled := True;
2019-10-09 23:14:56 +02:00
2019-07-27 20:23:48 +02:00
finally
LSwagDoc.Free;
end;
end;
end;
2020-01-03 20:49:53 +01:00
procedure TMVCSwaggerMiddleware.SortApiPaths(ASwagDoc: TSwagDoc);
var
lPathComparer: IComparer<TSwagPath>;
lOperationComparer: IComparer<TSwagPathOperation>;
lSwagPath: TSwagPath;
{$IF not defined(RIOORBETTER)}
lSwagPathList: TArray<TSwagPath>;
lSwagOperationList: TArray<TSwagPathOperation>;
{$ENDIF}
2020-01-03 20:49:53 +01:00
begin
// Sort paths
lPathComparer := TDelegatedComparer<TSwagPath>.Create(
function(const Left, Right: TSwagPath): Integer
begin
if (Left.Operations.Count = 0) or (Left.Operations[0].Tags.Count = 0) or
(Right.Operations.Count = 0) or (Right.Operations[0].Tags.Count = 0) then
begin
Result := 1;
end
else if SameText(Left.Operations[0].Tags[0], JWT_AUTHENTICATION_TAG) or
SameText(Right.Operations[0].Tags[0], JWT_AUTHENTICATION_TAG) then
begin
Result := -1;
end
else
begin
Result := CompareText(Left.Operations[0].Tags[0], Right.Operations[0].Tags[0]);
end;
end);
{$IF defined(RIOORBETTER)}
ASwagDoc.Paths.Sort(lPathComparer);
{$ELSE}
ASwagDoc.Paths.TrimExcess;
lSwagPathList := ASwagDoc.Paths.ToArray;
ASwagDoc.Paths.OwnsObjects := False;
ASwagDoc.Paths.Clear;
TArrayHelper.QuickSort<TSwagPath>(lSwagPathList, lPathComparer, Low(lSwagPathList), High(lSwagPathList));
ASwagDoc.Paths.AddRange(lSwagPathList);
ASwagDoc.Paths.OwnsObjects := True;
{$ENDIF}
// Sort paths operations
lOperationComparer := TDelegatedComparer<TSwagPathOperation>.Create(
function(const Left, Right: TSwagPathOperation): Integer
begin
if Ord(Left.Operation) > Ord(Right.Operation) then
Result := -1
else if Ord(Left.Operation) < Ord(Right.Operation) then
Result := 1
else
Result := 0;
end);
for lSwagPath in ASwagDoc.Paths do
begin
{$IF defined(RIOORBETTER)}
lSwagPath.Operations.Sort(lOperationComparer);
{$ELSE}
lSwagPath.Operations.TrimExcess;
lSwagOperationList := lSwagPath.Operations.ToArray;
lSwagPath.Operations.OwnsObjects := False;
lSwagPath.Operations.Clear;
TArrayHelper.QuickSort<TSwagPathOperation>(lSwagOperationList, lOperationComparer,
Low(lSwagOperationList), High(lSwagOperationList));
lSwagPath.Operations.AddRange(lSwagOperationList);
lSwagPath.Operations.OwnsObjects := True;
{$ENDIF}
end;
2020-01-03 20:49:53 +01:00
end;
2019-07-27 20:23:48 +02:00
end.