mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-15 15:55:54 +01:00
543 lines
18 KiB
ObjectPascal
543 lines
18 KiB
ObjectPascal
// ***************************************************************************
|
||
//
|
||
// Delphi MVC Framework
|
||
//
|
||
// Copyright (c) 2010-2024 Daniele Teti and the DMVCFramework Team
|
||
//
|
||
// 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.
|
||
//
|
||
// *************************************************************************** }
|
||
|
||
unit MVCFramework.Middleware.Swagger;
|
||
|
||
{$I dmvcframework.inc}
|
||
|
||
interface
|
||
|
||
uses
|
||
MVCFramework,
|
||
MVCFramework.Logger,
|
||
Swag.Doc,
|
||
MVCFramework.Swagger.Commons,
|
||
Swag.Doc.SecurityDefinition,
|
||
Swag.Common.Types,
|
||
System.JSON, MVCFramework.Commons;
|
||
|
||
type
|
||
TMVCSwaggerMiddleware = class(TInterfacedObject, IMVCMiddleware)
|
||
private
|
||
fEngine: TMVCEngine;
|
||
fSwaggerInfo: TMVCSwaggerInfo;
|
||
fSwagDocURL: string;
|
||
fJWTDescription: string;
|
||
fEnableBasicAuthentication: Boolean;
|
||
fHost: string;
|
||
fBasePath: string;
|
||
fPathFilter: string;
|
||
fTransferProtocolSchemes: TMVCTransferProtocolSchemes;
|
||
procedure DocumentApiInfo(const ASwagDoc: TSwagDoc);
|
||
procedure DocumentApiSettings(AContext: TWebContext; ASwagDoc: TSwagDoc);
|
||
procedure DocumentApiAuthentication(const ASwagDoc: TSwagDoc);
|
||
procedure DocumentApi(ASwagDoc: TSwagDoc);
|
||
procedure SortApiPaths(ASwagDoc: TSwagDoc);
|
||
procedure InternalRender(AContent: string; AContext: TWebContext);
|
||
public
|
||
constructor Create(
|
||
const AEngine: TMVCEngine;
|
||
const ASwaggerInfo: TMVCSwaggerInfo;
|
||
const ASwaggerDocumentationURL: string = '/swagger.json';
|
||
const AJWTDescription: string = JWT_DEFAULT_DESCRIPTION;
|
||
const AEnableBasicAuthentication: Boolean = False;
|
||
const AHost: string = '';
|
||
const ABasePath: string = '';
|
||
const APathFilter: String = '';
|
||
const ATransferProtocolSchemes: TMVCTransferProtocolSchemes = [psHTTP, psHTTPS]);
|
||
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 AControllerQualifiedClassName: string; const AActionName: string;
|
||
const AHandled: Boolean);
|
||
procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
|
||
end;
|
||
|
||
implementation
|
||
|
||
uses
|
||
System.SysUtils,
|
||
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,
|
||
Swag.Doc.Definition,
|
||
System.Generics.Collections,
|
||
System.Generics.Defaults,
|
||
System.TypInfo,
|
||
Json.Common.Helpers;
|
||
|
||
{ TMVCSwaggerMiddleware }
|
||
|
||
constructor TMVCSwaggerMiddleware.Create(const AEngine: TMVCEngine; const ASwaggerInfo: TMVCSwaggerInfo;
|
||
const ASwaggerDocumentationURL, AJWTDescription: string; const AEnableBasicAuthentication: Boolean;
|
||
const AHost, ABasePath: string;
|
||
const APathFilter: String;
|
||
const ATransferProtocolSchemes: TMVCTransferProtocolSchemes);
|
||
begin
|
||
inherited Create;
|
||
fSwagDocURL := ASwaggerDocumentationURL;
|
||
fEngine := AEngine;
|
||
fSwaggerInfo := ASwaggerInfo;
|
||
fJWTDescription := AJWTDescription;
|
||
fEnableBasicAuthentication := AEnableBasicAuthentication;
|
||
fHost := AHost;
|
||
fBasePath := ABasePath;
|
||
fPathFilter := APathFilter;
|
||
fTransferProtocolSchemes := ATransferProtocolSchemes;
|
||
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;
|
||
I: TMVCHTTPMethodType;
|
||
lPathUri: string;
|
||
lIndex: Integer;
|
||
lAuthTypeName: string;
|
||
lIsIgnoredPath: Boolean;
|
||
lControllerDefaultModelClass: TClass;
|
||
lControllerDefaultSummaryTags: TArray<string>;
|
||
lPathAttributeFound: Boolean;
|
||
lVisitedMethodSignatures: TList<String>;
|
||
lMethodSignature: string;
|
||
lControllerDefaultModelSingularName: string;
|
||
lControllerDefaultModelPluralName: string;
|
||
begin
|
||
lVisitedMethodSignatures := TList<String>.Create;
|
||
try
|
||
lRttiContext := TRttiContext.Create;
|
||
try
|
||
for lController in fEngine.Controllers do
|
||
begin
|
||
lControllerDefaultModelClass := nil;
|
||
lControllerPath := '';
|
||
SetLength(lControllerDefaultSummaryTags, 0);
|
||
lPathAttributeFound := False;
|
||
lObjType := lRttiContext.GetType(lController.Clazz);
|
||
for lAttr in lObjType.GetAttributes do
|
||
begin
|
||
if lAttr is MVCSwagIgnorePathAttribute then
|
||
begin
|
||
lControllerPath := '';
|
||
Break;
|
||
end;
|
||
if lAttr is MVCPathAttribute then
|
||
begin
|
||
if not lPathAttributeFound then
|
||
begin
|
||
{in case of more than one MVCPath attribute, only the firstone
|
||
is considered by swagger}
|
||
lControllerPath := MVCPathAttribute(lAttr).Path;
|
||
lPathAttributeFound := fPathFilter.IsEmpty or lControllerPath.StartsWith(fPathFilter);
|
||
end;
|
||
end;
|
||
if lAttr is MVCSWAGDefaultModel then
|
||
begin
|
||
lControllerDefaultModelClass := MVCSWAGDefaultModel(lAttr).JsonSchemaClass;
|
||
lControllerDefaultModelSingularName := MVCSWAGDefaultModel(lAttr).SingularModelName;
|
||
lControllerDefaultModelPluralName := MVCSWAGDefaultModel(lAttr).PluralModelName;
|
||
end;
|
||
if lAttr is MVCSWAGDefaultSummaryTags then
|
||
begin
|
||
lControllerDefaultSummaryTags := MVCSWAGDefaultSummaryTags(lAttr).GetTags;
|
||
end;
|
||
end;
|
||
|
||
if not lPathAttributeFound then
|
||
Continue;
|
||
|
||
//for lMethod in lObjType.GetDeclaredMethods do
|
||
for lMethod in lObjType.GetMethods do
|
||
begin
|
||
{only public and puches methods are inspected}
|
||
if not (lMethod.Visibility in [mvPublished, mvPublic]) then
|
||
begin
|
||
continue;
|
||
end;
|
||
|
||
{here could arrive also overwritten methods, so we need to exclude
|
||
method which have been overwritten. We can do this checking if the method class
|
||
is the controller which we are inspecting }
|
||
// if lObjType <> lMethod.Parent then
|
||
// begin
|
||
// Continue;
|
||
// end;
|
||
|
||
lIsIgnoredPath := False;
|
||
lFoundAttr := False;
|
||
lMVCHttpMethods := [];
|
||
lMethodPath := '';
|
||
|
||
for lAttr in lMethod.GetAttributes do
|
||
begin
|
||
if lAttr is MVCSwagIgnorePathAttribute then
|
||
begin
|
||
lIsIgnoredPath := True;
|
||
end;
|
||
if lAttr is MVCPathAttribute then
|
||
begin
|
||
lMethodPath := MVCPathAttribute(lAttr).Path;
|
||
lFoundAttr := True;
|
||
end;
|
||
if lAttr is MVCHTTPMethodsAttribute then
|
||
begin
|
||
lMVCHttpMethods := MVCHTTPMethodsAttribute(lAttr).MVCHTTPMethods;
|
||
end;
|
||
end;
|
||
|
||
if (not lIsIgnoredPath) and lFoundAttr then
|
||
begin
|
||
lMethodSignature := lObjType.Name + '.' + lMethod.Name;
|
||
if lVisitedMethodSignatures.Contains(lMethodSignature) then
|
||
begin
|
||
Continue;
|
||
end
|
||
else
|
||
begin
|
||
lVisitedMethodSignatures.Add(lMethodSignature);
|
||
end;
|
||
|
||
//LogI(lObjType.Name + '.' + lMethod.Name + ' ' + lMethod.Parent.ToString);
|
||
lSwagPath := nil;
|
||
lPathUri := TMVCSwagger.MVCPathToSwagPath(lControllerPath + lMethodPath);
|
||
for lIndex := 0 to Pred(ASwagDoc.Paths.Count) do
|
||
begin
|
||
if SameText(ASwagDoc.Paths[lIndex].Uri, lPathUri) then
|
||
begin
|
||
lSwagPath := ASwagDoc.Paths[lIndex];
|
||
Break;
|
||
end;
|
||
end;
|
||
|
||
if not Assigned(lSwagPath) then
|
||
begin
|
||
lSwagPath := TSwagPath.Create;
|
||
lSwagPath.Uri := lPathUri;
|
||
ASwagDoc.Paths.Add(lSwagPath);
|
||
end;
|
||
|
||
for I in lMVCHttpMethods do
|
||
begin
|
||
lSwagPathOp := TSwagPathOperation.Create;
|
||
TMVCSwagger.FillOperationSummary(
|
||
lSwagPathOp,
|
||
lMethod,
|
||
ASwagDoc.Definitions,
|
||
I,
|
||
lControllerDefaultModelClass,
|
||
lControllerDefaultModelSingularName,
|
||
lControllerDefaultModelPluralName,
|
||
lControllerDefaultSummaryTags);
|
||
if TMVCSwagger.MethodRequiresAuthentication(lMethod, lObjType, lAuthTypeName) then
|
||
begin
|
||
lSwagPathOp.Security.Add(lAuthTypeName);
|
||
end;
|
||
lSwagPathOp.Parameters.AddRange(
|
||
TMVCSwagger.GetParamsFromMethod(
|
||
lSwagPath.Uri,
|
||
lMethod,
|
||
ASwagDoc.Definitions,
|
||
lControllerDefaultModelClass,
|
||
lControllerDefaultModelSingularName,
|
||
lControllerDefaultModelPluralName)
|
||
);
|
||
lSwagPathOp.Operation := TMVCSwagger.MVCHttpMethodToSwagPathOperation(I);
|
||
lSwagPath.Operations.Add(lSwagPathOp);
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
finally
|
||
lRttiContext.Free;
|
||
end;
|
||
finally
|
||
lVisitedMethodSignatures.Free;
|
||
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;
|
||
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;
|
||
|
||
procedure TMVCSwaggerMiddleware.DocumentApiSettings(AContext: TWebContext; ASwagDoc: TSwagDoc);
|
||
var
|
||
lSwagSchemes: TSwagTransferProtocolSchemes;
|
||
begin
|
||
ASwagDoc.Host := fHost;
|
||
if ASwagDoc.Host.IsEmpty then
|
||
begin
|
||
ASwagDoc.Host := Format('%s:%d', [AContext.Request.RawWebRequest.Host, AContext.Request.RawWebRequest.ServerPort]);
|
||
end;
|
||
|
||
ASwagDoc.BasePath := fBasePath;
|
||
if ASwagDoc.BasePath.IsEmpty then
|
||
begin
|
||
ASwagDoc.BasePath := fEngine.Config[TMVCConfigKey.PathPrefix];
|
||
end;
|
||
if ASwagDoc.BasePath.IsEmpty then
|
||
begin
|
||
ASwagDoc.BasePath := '/';
|
||
end;
|
||
|
||
lSwagSchemes := [];
|
||
if psHTTP in fTransferProtocolSchemes then
|
||
begin
|
||
Include(lSwagSchemes, tpsHttp);
|
||
end;
|
||
if psHTTPS in fTransferProtocolSchemes then
|
||
begin
|
||
Include(lSwagSchemes, tpsHttps);
|
||
end;
|
||
ASwagDoc.Schemes := lSwagSchemes;
|
||
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,
|
||
TEncoding.Default.GetBytes(AContent))), LContentType);
|
||
finally
|
||
LEncoding.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMVCSwaggerMiddleware.OnAfterControllerAction(AContext: TWebContext;
|
||
const AControllerQualifiedClassName: string; const AActionName: string;
|
||
const AHandled: Boolean);
|
||
begin
|
||
// do nothing
|
||
end;
|
||
|
||
procedure TMVCSwaggerMiddleware.OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
|
||
begin
|
||
// do nothing
|
||
end;
|
||
|
||
procedure TMVCSwaggerMiddleware.OnBeforeControllerAction(AContext: TWebContext;
|
||
const AControllerQualifiedClassName, AActionName: string; var AHandled: Boolean);
|
||
begin
|
||
// do nothing
|
||
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
|
||
begin
|
||
LSwagDoc := TSwagDoc.Create;
|
||
try
|
||
DocumentApiInfo(LSwagDoc);
|
||
DocumentApiSettings(AContext, LSwagDoc);
|
||
DocumentApiAuthentication(LSwagDoc);
|
||
DocumentApi(LSwagDoc);
|
||
SortApiPaths(LSwagDoc);
|
||
|
||
LSwagDoc.GenerateSwaggerJson;
|
||
InternalRender(LSwagDoc.SwaggerJson.Format, AContext);
|
||
AHandled := True;
|
||
|
||
finally
|
||
LSwagDoc.Free;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
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}
|
||
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;
|
||
|
||
end;
|
||
|
||
end.
|