delphimvcframework/sources/MVCFramework.Controllers.Swagger.pas

577 lines
19 KiB
ObjectPascal
Raw Normal View History

unit MVCFramework.Controllers.Swagger;
interface
uses
Classes
, System.TypInfo
, MVCFramework
, MVCFramework.Commons
, System.Generics.Collections
, Swag.Doc
, Swag.Doc.Path
, Swag.Doc.Path.Operation
, Swag.Doc.Definition
, Swag.Common.Types
, Swag.Doc.Path.Operation.Response
, Swag.Doc.Path.Operation.RequestParameter
, Json.Schema
, Json.Schema.Field
, Json.Schema.Field.Strings
, Json.Schema.Field.Arrays
, Json.Schema.Field.Objects
2019-08-05 13:37:42 +02:00
, Json.Schema.Field.Numbers
, Json.Schema.Field.DateTimes
;
type
TMVCStatusResponses = class
strict private
FStatusCode : Integer;
FStatusDescription : String;
FReturnType : TClass;
2019-08-05 13:37:42 +02:00
FKind: TTypeKind;
public
property StatusCode: Integer read FStatusCode write FStatusCode;
property StatusDescription : string read FStatusDescription write FStatusDescription;
property ReturnType: TClass read FReturnType write FReturnType;
2019-08-05 13:37:42 +02:00
property Kind: TTypeKind read FKind write FKind;
function GetReturnTypeName:string;
constructor Create(AAttribute: MVCResponseAttribute); overload;
constructor Create(AAttribute: MVCResponseListAttribute); overload;
end;
TMVCEndPoint = class
strict private
FDoc : string;
FMethod : TMVCHTTPMethods;
FOperationId: string;
FProduces : string;
FConsumes : string;
FPath : string;
public
2019-08-05 13:37:42 +02:00
Statuses : TObjectList<TMVCStatusResponses>;
Params : TStringList;
property Documentation : string read FDoc write FDoc;
property Method : TMVCHTTPMethods read FMethod write FMethod;
property OperationId : string read FOperationId write FOperationId;
property Produces : string read FProduces write FProduces;
property Consumes : string read FConsumes write FConsumes;
property Path : string read FPath write FPath;
public
constructor Create;
destructor Destroy; override;
end;
type
[MVCPath('/')]
TMVCSwaggerController = class(TMVCController)
private
fEndpoints : TObjectList<TMVCEndPoint>;
fSwagDoc: TSwagDoc;
fDefinitions : TList<TClass>;
2019-08-05 13:37:42 +02:00
procedure ConvertFieldAttributesToSwagger(AField: TJsonField; AAttributes: TArray<TCustomAttribute>);
private
procedure ProcessControllerMethods(aClass: TClass);
2019-08-05 13:37:42 +02:00
procedure ProcessObject(schema: TJSONSchema; aClass:TClass);
procedure ProcessObjectForDefinition(aClass:TClass);
function JsonFieldFromRttiTypeInfo(LSchema: TJsonSchema; LTypeKind : TTypeKind; TypeHandle: PTypeInfo; LPropertyName: string): TJsonField;
public
class function ProcessAndRewriteURL(params: TStringList; const rootPath:string; const path:string): string;
class function MVCMethodToSwaggerOperation(inMethod:TMVCHTTPMethodType): TSwagPathTypeOperation;
[MVCDoc('This is some documentation')]
[MVCPath('/swagger')]
[MVCHTTPMethod([httpGET])]
procedure Swagger;
public
constructor Create; override;
destructor Destroy; override;
end;
implementation
uses
RTTI
, windows
, System.SysUtils
, System.RegularExpressions
, System.RegularExpressionsCore
;
{ TMVCSwaggerController }
constructor TMVCSwaggerController.Create;
begin
inherited;
fEndpoints := TObjectList<TMVCEndPoint>.Create;
fDefinitions := TList<TClass>.Create;
fSwagDoc := TSwagDoc.Create;
end;
destructor TMVCSwaggerController.Destroy;
begin
FreeAndNil(fEndpoints);
FreeAndNil(fSwagDoc);
FreeAndNil(fDefinitions);
inherited;
end;
2019-08-05 13:37:42 +02:00
procedure TMVCSwaggerController.ConvertFieldAttributesToSwagger(AField: TJsonField; AAttributes: TArray<TCustomAttribute>);
var
LAttribute: TCustomAttribute;
begin
for LAttribute in AAttributes do
begin
if LAttribute is MVCDocAttribute then
begin
AField.Description := MVCDocAttribute(LAttribute).Value;
end
else if LAttribute is MVCPatternAttribute then
begin
(AField as TJSONFieldString).Pattern := MVCPatternAttribute(LAttribute).Value;
end
else if LAttribute is MVCMaxLengthAttribute then
begin
(AField as TJSONFieldString).MaxLength := MVCMaxLengthAttribute(LAttribute).Value;
end
else if LAttribute is MVCMinimumAttribute then
begin
if AField is TJsonFieldInt64 then
(AField as TJsonFieldInt64).MinValue := MVCMinimumAttribute(LAttribute).Value
else if AField is TJsonFieldInteger then
(AField as TJsonFieldInteger).MinValue := MVCMinimumAttribute(LAttribute).Value
else if AField is TJsonFieldNumber then
(AField as TJsonFieldNumber).MinValue := MVCMinimumAttribute(LAttribute).Value
else
raise Exception.Create('Minimum not valid on ' + AField.ClassName);
end
else if LAttribute is MVCMaximumAttribute then
begin
if AField is TJsonFieldInt64 then
(AField as TJsonFieldInt64).MaxValue := MVCMaximumAttribute(LAttribute).Value
else if AField is TJsonFieldInteger then
(AField as TJsonFieldInteger).MaxValue := MVCMaximumAttribute(LAttribute).Value
else if AField is TJsonFieldNumber then
(AField as TJsonFieldNumber).MaxValue := MVCMaximumAttribute(LAttribute).Value
else
raise Exception.Create('Maximum not valid on ' + AField.ClassName);
end
else if LAttribute is MVCFormatAttribute then
begin
if AField is TJsonFieldString then
(AField as TJsonFieldString).Format := MVCFormatAttribute(LAttribute).Value
else if AField is TJsonFieldString then
(AField as TJsonFieldInteger).Format := MVCFormatAttribute(LAttribute).Value
else if AField is TJsonFieldInt64 then
(AField as TJsonFieldInt64).Format := MVCFormatAttribute(LAttribute).Value
else if AField is TJsonFieldNumber then
(AField as TJsonFieldNumber).Format := MVCFormatAttribute(LAttribute).Value
else
raise Exception.Create('Format not valid on ' + AField.ClassName);
end;
end;
end;
class function TMVCSwaggerController.MVCMethodToSwaggerOperation(inMethod:TMVCHTTPMethodType): TSwagPathTypeOperation;
begin
2019-08-05 13:37:42 +02:00
case inMethod of
httpGET: Result := TSwagPathTypeOperation.ohvGet;
httpPOST: Result := TSwagPathTypeOperation.ohvPost;
httpPUT: Result := TSwagPathTypeOperation.ohvPut;
httpDELETE: Result := TSwagPathTypeOperation.ohvDelete;
httpHEAD: Result := TSwagPathTypeOperation.ohvHead;
httpOPTIONS: Result := TSwagPathTypeOperation.ohvOptions;
httpPATCH: Result := TSwagPathTypeOperation.ohvPatch;
httpTRACE: Result := TSwagPathTypeOperation.ohvTrace;
else
Result := TSwagPathTypeOperation.ohvNotDefined;
2019-08-05 13:37:42 +02:00
end;
end;
class function TMVCSwaggerController.ProcessAndRewriteURL(params: TStringList; const rootPath:string; const path:string):string;
var
LRegEx : TRegEx;
LMatches: TMatchCollection;
i: Integer;
begin
if (rootPath = '/') and path.StartsWith('/') then
Result := path
else
Result := rootPath + Path;
LRegEx := TRegEx.Create('\(\$\w+\)');
LMatches := LRegEx.Matches(Result);
for i := LMatches.Count - 1 downto 0 do
begin
params.Add(Copy(LMatches.Item[i].Value,3,LMatches.Item[i].Value.length-3));
Result[LMatches.Item[i].Index + LMatches.Item[i].Length - 1] := '}';
Result[LMatches.Item[i].Index] := '{';
Delete(Result, LMatches.Item[i].Index + 1,1);
end;
end;
procedure TMVCSwaggerController.ProcessControllerMethods(aClass: TClass);
var
LRttiContext: TRttiContext;
LRttiType: TRttiType;
LAttribute: TCustomAttribute;
LMethods : TArray<TRttiMethod>;
LMethod: TRttiMethod;
LRootPath : string;
LEndpoint : TMVCEndPoint;
LStatus : TMVCStatusResponses;
begin
try
LRttiContext := TRttiContext.Create;
LRttiType := LRttiContext.GetType(aClass);
for LAttribute in LRttiType.GetAttributes do
begin
if LAttribute is MVCPathAttribute then
begin
LRootPath := MVCPathAttribute(LAttribute).Path;
end
end;
LMethods := LRttiType.GetMethods;
for LMethod in LMethods do
begin
LEndpoint := TMVCEndPoint.Create;
for LAttribute in LMethod.GetAttributes do
begin
if LAttribute is MVCPathAttribute then
begin
2019-08-05 13:37:42 +02:00
LEndpoint.Path := TMVCSwaggerController.ProcessAndRewriteURL(LEndpoint.params, LRootPath, MVCPathAttribute(LAttribute).Path);
end
else if LAttribute is MVCHTTPMethodAttribute then
begin
2019-08-05 13:37:42 +02:00
LEndpoint.Method := MVCHTTPMethodAttribute(LAttribute).MVCHTTPMethods;
end
else if LAttribute is MVCDocAttribute then
begin
2019-08-05 13:37:42 +02:00
LEndpoint.Documentation := MVCDocAttribute(LAttribute).Value;
end
else if LAttribute is MVCProducesAttribute then
begin
2019-08-05 13:37:42 +02:00
LEndpoint.Produces := MVCProducesAttribute(LAttribute).Value;
end
else if LAttribute is MVCConsumesAttribute then
begin
2019-08-05 13:37:42 +02:00
LEndpoint.Consumes := MVCConsumesAttribute(LAttribute).Value;
end
else if LAttribute is MVCResponseAttribute then
begin
LStatus := TMVCStatusResponses.Create(MVCResponseAttribute(LAttribute));
LEndpoint.statuses.Add(LStatus);
end
else if LAttribute is MVCResponseListAttribute then
begin
LStatus := TMVCStatusResponses.Create(MVCResponseListAttribute(LAttribute));
LEndpoint.statuses.Add(LStatus);
end
end;
LEndpoint.OperationId := LMethod.Name;
fEndpoints.Add(LEndpoint);
end;
finally
LRttiContext.Free;
end;
end;
function TMVCSwaggerController.JsonFieldFromRttiTypeInfo(LSchema: TJsonSchema; LTypeKind : TTypeKind; TypeHandle: PTypeInfo; LPropertyName: string): TJsonField;
begin
Result := nil;
case LTypeKind of
tkClassRef, tkPointer, tkProcedure, tkMRecord, tkInterface,
tkMethod, tkVariant, tkSet: ;
tkWChar, tkLString, tkWString, tkString, tkUString, tkChar:
Result := LSchema.AddField<String>(LPropertyName);
tkEnumeration:
if (TypeHandle = TypeInfo(Boolean)) then
LSchema.AddField<Boolean>(LPropertyName);
tkRecord:
if (TypeHandle = TypeInfo(TGUID)) then
LSchema.AddField<String>(LPropertyName);
tkInteger:
Result := LSchema.AddField<Integer>(LPropertyName);
tkInt64:
Result := LSchema.AddField<Int64>(LPropertyName);
tkFloat:
if TypeHandle = TypeInfo(TDateTime) then
Result := LSchema.AddField<TJsonFieldDateTime>(LPropertyName)
else if TypeHandle = TypeInfo(TDate) then
Result := LSchema.AddField<TJsonFieldDate>(LPropertyName)
else if TypeHandle = TypeInfo(TTime) then
Result := LSchema.AddField<TJsonFieldTime>(LPropertyName)
else
Result := LSchema.AddField<Double>(LPropertyName);
end;
end;
2019-08-05 13:37:42 +02:00
procedure TMVCSwaggerController.ProcessObjectForDefinition(aClass:TClass);
var
LRttiContext: TRttiContext;
LRttiType: TRttiType;
LField : TJsonField;
LChildschema : TJsonSchema;
LDefinition : TSwagDefinition;
LSchema : TJsonSchema;
LIndexedProperty: TRttiIndexedProperty;
2019-08-05 13:37:42 +02:00
LProperty : TRttiProperty;
LChildObject : TJSONFieldObject;
2019-08-05 13:37:42 +02:00
LTypeKind : TTypeKind;
LPropertyName : string;
LAttributes : TArray<TCustomAttribute>;
begin
LSchema := TJsonSchema.Create;
LRttiContext := TRttiContext.Create;
try
LRttiType := LRttiContext.GetType(aClass);
for LIndexedProperty in LRttiType.GetIndexedProperties do
begin
2019-08-05 13:37:42 +02:00
LTypeKind := LIndexedProperty.PropertyType.TypeKind;
LPropertyName := LIndexedProperty.Name;
LField := JsonFieldFromRttiTypeInfo(LSchema, LTypeKind, LIndexedProperty.PropertyType.Handle, LPropertyName);
if LTypeKind = tkClass then
2019-08-05 13:37:42 +02:00
begin
LField := LSchema.AddField<TJsonFieldArray>(LPropertyName);
LChildObject := TJsonFieldObject.Create;
LChildObject.Ref := '#/definitions/' + TRttiInstanceType(LIndexedProperty.PropertyType).MetaclassType.ClassName;
OutputDebugString(PChar(LIndexedProperty.PropertyType.Name));
2019-08-05 13:37:42 +02:00
(LField as TJsonFieldArray).ItemFieldType := LChildObject;
OutputDebugString(PChar(LIndexedProperty.PropertyType.Name));
end
else if LTypeKind = tkArray then
OutputDebugString(PChar(LIndexedProperty.PropertyType.Name))
else if LTypeKind = tkDynArray then
2019-08-05 13:37:42 +02:00
OutputDebugString(PChar(LIndexedProperty.PropertyType.Name));
LAttributes := LIndexedProperty.GetAttributes;
ConvertFieldAttributesToSwagger(LField, LAttributes);
end;
for LProperty in LRttiType.GetProperties do
begin
2019-08-05 13:37:42 +02:00
LPropertyName := LProperty.Name;
LTypeKind := LProperty.PropertyType.TypeKind;
LField := JsonFieldFromRttiTypeInfo(LSchema, LTypeKind, LProperty.PropertyType.Handle, LPropertyName);
if (LProperty.PropertyType.TypeKind = tkClass) then
begin
if LProperty.PropertyType.Name='TRttiInstanceType' then continue;
if LProperty.PropertyType.Name.StartsWith('TRtti') then continue;
LChildschema := TJsonSchema.Create;
LChildschema.Ref := '#/definitions/' + TRttiInstanceType(LProperty.PropertyType).MetaclassType.ClassName;
OutputDebugString(PChar(LProperty.PropertyType.Name));
LField := LSchema.AddField(LChildschema);
2019-08-05 13:37:42 +02:00
LField.Name := LPropertyName;
end
else
begin
2019-08-05 13:37:42 +02:00
LField := LSchema.AddField<string>(LPropertyName);
end;
2019-08-05 13:37:42 +02:00
LAttributes := LProperty.GetAttributes;
ConvertFieldAttributesToSwagger(LField, LAttributes);
end;
LDefinition := TSwagDefinition.Create;
LDefinition.SetJsonSchema(aClass.ClassName, LSchema);
fSwagDoc.Definitions.Add(LDefinition);
finally
LRttiContext.Free;
end;
end;
2019-08-05 13:37:42 +02:00
procedure TMVCSwaggerController.ProcessObject(schema: TJSONSchema; aClass:TClass);
var
LRttiContext: TRttiContext;
LRttiType: TRttiType;
LProperty : TRttiProperty;
LAttribute : TCustomAttribute;
LField : TJsonField;
LChildschema : TJsonSchema;
begin
LRttiContext := TRttiContext.Create;
2019-08-05 13:37:42 +02:00
LField := nil;
try
LRttiType := LRttiContext.GetType(aClass);
for LProperty in LRttiType.GetProperties do
begin
if (LProperty.PropertyType.TypeKind = tkInteger) then
begin
2019-08-05 13:37:42 +02:00
LField := schema.AddField<Integer>(LProperty.Name);
end
else if (LProperty.PropertyType.TypeKind = tkInt64) then
begin
2019-08-05 13:37:42 +02:00
LField := schema.AddField<Int64>(LProperty.Name);
end
else if (LProperty.PropertyType.TypeKind = tkFloat) then
begin
2019-08-05 13:37:42 +02:00
LField := schema.AddField<Double>(LProperty.Name);
end
else if (LProperty.PropertyType.TypeKind = tkArray) then
begin
OutputDebugString(PChar(LProperty.PropertyType.Name));
end
else if (LProperty.PropertyType.TypeKind = tkDynArray) then
begin
OutputDebugString(PChar(LProperty.PropertyType.Name));
end
else if (LProperty.PropertyType.TypeKind = tkClass) then
begin
if LProperty.PropertyType.Name='TRttiInstanceType' then
continue;
if LProperty.PropertyType.Name.StartsWith('TRtti') then
continue;
LChildschema := TJsonSchema.Create;
OutputDebugString(PChar(LProperty.PropertyType.Name));
2019-08-05 13:37:42 +02:00
ProcessObject(LChildschema, TRttiInstanceType(LProperty.PropertyType).MetaclassType);
fDefinitions.Add(TRttiInstanceType(LProperty.PropertyType).MetaclassType);
LField := schema.AddField(LChildschema);
LField.Name := LProperty.Name;
end
else if (LProperty.PropertyType.TypeKind = tkWString) then
begin
2019-08-05 13:37:42 +02:00
LField := schema.AddField<WideString>(LProperty.Name);
end
else
begin
2019-08-05 13:37:42 +02:00
LField := schema.AddField<string>(LProperty.Name);
end;
for LAttribute in LProperty.GetAttributes do
begin
if LAttribute is MVCDocAttribute then
begin
LField.Description := MVCDocAttribute(LAttribute).Value;
end
else if LAttribute is MVCPatternAttribute then
begin
// Property doesnt currently exist on TJSONField
if LField is TJsonFieldString then
(LField as TJsonFieldString).Pattern := MVCPatternAttribute(LAttribute).Value;
end;
end;
end;
finally
LRttiContext.Free;
end;
end;
procedure TMVCSwaggerController.Swagger;
var
i, p, j: Integer;
k: Integer;
LPath : TSwagPath;
LPathOperation : TSwagPathOperation;
LSwagResponse : TSwagResponse;
LHttpMethod : TMVCHTTPMethodType;
LParam : TSwagRequestParameter;
LSchema : TJsonSchema;
2019-08-05 13:37:42 +02:00
LController : TMVCControllerDelegate;
LDefinition : TClass;
begin
2019-08-05 13:37:42 +02:00
for LController in Engine.Controllers do
begin
2019-08-05 13:37:42 +02:00
ProcessControllerMethods(LController.Clazz);
end;
2019-08-05 13:37:42 +02:00
Context.Response.ContentType := 'application/json';
for i := 0 to fEndpoints.Count - 1 do
begin
LPath := TSwagPath.Create;
LPath.Uri := fEndpoints[i].path;
for LHttpMethod in fEndpoints[i].method do
begin
LPathOperation := TSwagPathOperation.Create;
LPathOperation.Operation := TMVCSwaggerController.MVCMethodToSwaggerOperation(LHttpMethod);
LPathOperation.Description := fEndpoints[i].Documentation;
for p := fEndpoints[i].params.Count - 1 downto 0 do
begin
LParam := TSwagRequestParameter.Create;
LParam.Name := fEndpoints[i].params[p];
LParam.InLocation := rpiPath;
LPathOperation.Parameters.Add(LParam);
end;
2019-08-05 13:37:42 +02:00
if fEndpoints[i].Produces.length > 0 then
LPathOperation.Produces.Add(fEndpoints[i].produces);
2019-08-05 13:37:42 +02:00
if fEndpoints[i].Consumes.length > 0 then
LPathOperation.Consumes.Add(fEndpoints[i].consumes);
for j := 0 to fEndpoints[i].statuses.Count - 1 do
begin
LSwagResponse := TSwagResponse.Create;
2019-08-05 13:37:42 +02:00
LSwagResponse.StatusCode := fEndpoints[i].Statuses[j].statusCode.ToString;
LSwagResponse.Description := fEndpoints[i].Statuses[j].statusDescription;
LSchema := TJsonSchema.Create;
2019-08-05 13:37:42 +02:00
if Assigned(fEndpoints[i].Statuses[j].ReturnType) then
begin
2019-08-05 13:37:42 +02:00
ProcessObjectForDefinition(fEndpoints[i].Statuses[j].ReturnType);
ProcessObject(LSchema, fEndpoints[i].Statuses[j].ReturnType);
LSwagResponse.Schema.SetJsonSchema(fEndpoints[i].Statuses[j].ReturnType.ClassName, LSchema);
end;
2019-08-05 13:37:42 +02:00
LPathOperation.Responses.Add(fEndpoints[i].Statuses[j].StatusCode.ToString, LSwagResponse);
end;
OutputDebugString(PChar(TRttiEnumerationType.GetName(LPathOperation.Operation) + ' ' + LPath.Uri ));
2019-08-05 13:37:42 +02:00
LPathOperation.OperationId := fEndpoints[i].OperationId;
LPath.Operations.Add(LPathOperation);
end;
fSwagDoc.Paths.Add(LPath);
end;
2019-08-05 13:37:42 +02:00
for LDefinition in fDefinitions do
begin
2019-08-05 13:37:42 +02:00
ProcessObjectForDefinition(LDefinition);
end;
fSwagDoc.GenerateSwaggerJson;
Context.Response.Content := fSwagDoc.SwaggerJson.ToJSON;
end;
{ TMVCEndPoint }
constructor TMVCEndPoint.Create;
begin
statuses := TObjectList<TMVCStatusResponses>.Create;
params := TStringList.Create;
end;
destructor TMVCEndPoint.Destroy;
begin
FreeAndNil(statuses);
FreeAndNil(params);
inherited;
end;
{ TStatusResponses }
constructor TMVCStatusResponses.Create(AAttribute: MVCResponseAttribute);
begin
StatusCode := AAttribute.StatusCode;
StatusDescription := AAttribute.Description;
ReturnType := AAttribute.ResponseClass;
Kind := tkClass;
end;
constructor TMVCStatusResponses.Create(AAttribute: MVCResponseListAttribute);
begin
StatusCode := AAttribute.StatusCode;
StatusDescription := AAttribute.Description;
ReturnType := AAttribute.ResponseClass;
Kind := tkArray;
end;
// Possibly use this method to strip the Default 'T' off the start of an object name
function TMVCStatusResponses.GetReturnTypeName: string;
begin
Result := ReturnType.ClassName;
end;
end.