delphimvcframework/sources/MVCFramework.Swagger.Commons.pas

1011 lines
34 KiB
ObjectPascal
Raw Normal View History

2019-10-09 23:14:56 +02:00
// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2020 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)
2019-10-09 23:14:56 +02:00
//
// ***************************************************************************
//
// 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.Swagger.Commons;
interface
uses
Json.Schema,
System.Rtti,
Swag.Common.Types,
2019-11-03 16:16:35 +01:00
System.SysUtils,
2019-07-27 20:23:48 +02:00
MVCFramework.Commons,
Swag.Doc.Path.Operation.RequestParameter,
Swag.Doc.Path.Operation,
Swag.Doc.Path,
2019-11-03 16:16:35 +01:00
System.Json,
Json.Schema.Field,
Json.Schema.Field.Objects,
Swag.Doc.Definition,
System.Generics.Collections;
2019-07-27 20:23:48 +02:00
type
TMVCSwagParamLocation = (plNotDefined, plQuery, plHeader, plPath, plFormData, plBody);
TMVCSwagParamType = (ptNotDefined, ptString, ptNumber, ptInteger, ptBoolean, ptArray, ptFile);
2019-11-03 16:16:35 +01:00
TMVCSwagSchemaType = (stUnknown, stInteger, stInt64, stNumber, stDateTime, stDate, stTime, stEnumeration, stBoolean,
stObject, stArray, stString, stChar, stGuid);
TMVCSwagAuthenticationType = (atBasic, atJsonWebToken);
/// <summary>
/// Swagger info object
/// </summary>
2019-07-27 20:23:48 +02:00
TMVCSwaggerInfo = record
Title: string;
Version: string;
TermsOfService: string;
Description: string;
ContactName: string;
ContactEmail: string;
ContactUrl: string;
LicenseName: string;
LicenseUrl: string;
end;
/// <summary>
/// Specify swagger path summary. See <see href="https://swagger.io/docs/specification/2-0/paths-and-operations/">
/// Swagger path and operations</see>
/// </summary>
2019-07-27 20:23:48 +02:00
MVCSwagSummaryAttribute = class(TCustomAttribute)
private
2019-11-03 16:16:35 +01:00
fTags: string;
fDeprecated: Boolean;
fDescription: string;
fOperationID: string;
2019-07-27 20:23:48 +02:00
public
2019-11-03 16:16:35 +01:00
constructor Create(const ATags, ADescription: string; const AOperationId: string = '';
ADeprecated: Boolean = False);
2019-07-27 20:23:48 +02:00
function GetTags: TArray<string>;
2019-11-03 16:16:35 +01:00
property Tags: string read fTags;
property Description: string read fDescription;
property OperationID: string read fOperationID;
property Deprecated: Boolean read fDeprecated;
2019-07-27 20:23:48 +02:00
end;
2019-11-03 16:16:35 +01:00
/// <summary>
/// Specifies your controller authentication type
/// </summary>
MVCSwagAuthenticationAttribute = class(TCustomAttribute)
private
FAuthenticationType: TMVCSwagAuthenticationType;
public
constructor Create(const AAuthenticationType: TMVCSwagAuthenticationType = atJsonWebToken);
property AuthenticationType: TMVCSwagAuthenticationType read FAuthenticationType;
end;
/// <summary>
/// Specify swagger path responses.
/// </summary>
2019-07-27 20:23:48 +02:00
MVCSwagResponsesAttribute = class(TCustomAttribute)
private
2019-11-03 16:16:35 +01:00
fStatusCode: Integer;
fDescription: string;
fJsonSchema: string;
fJsonSchemaClass: TClass;
fIsArray: Boolean;
2019-07-27 20:23:48 +02:00
public
2019-11-03 16:16:35 +01:00
constructor Create(const AStatusCode: Integer; const ADescription: string; const AJsonSchema: string = '');
overload;
constructor Create(const AStatusCode: Integer; const ADescription: string; const AJsonSchemaClass: TClass;
const AIsArray: Boolean = False); overload;
property StatusCode: Integer read fStatusCode;
property Description: string read fDescription;
property JsonSchema: string read fJsonSchema;
property JsonSchemaClass: TClass read fJsonSchemaClass;
property IsArray: Boolean read fIsArray;
2019-07-27 20:23:48 +02:00
end;
/// <summary>
/// Specify swagger path params.
/// </summary>
2019-07-27 20:23:48 +02:00
MVCSwagParamAttribute = class(TCustomAttribute)
private
2019-11-03 16:16:35 +01:00
fParamLocation: TMVCSwagParamLocation;
fParamName: string;
fParamDescription: string;
fParamType: TMVCSwagParamType;
fRequired: Boolean;
fJsonSchema: string;
fJsonSchemaClass: TClass;
fDefaultValue: string;
fEnumValues: string;
function GetEnumValues: TArray<string>;
2019-07-27 20:23:48 +02:00
public
2019-11-03 16:16:35 +01:00
constructor Create(const AParamLocation: TMVCSwagParamLocation; const AParamName: string;
const AParamDescription: string; const AParamType: TMVCSwagParamType; const ARequired: Boolean = True;
const ADefaultValue: string = ''; const AEnumValues: string = ''; const AJsonSchema: string = ''); overload;
2019-11-03 16:16:35 +01:00
constructor Create(const AParamLocation: TMVCSwagParamLocation; const AParamName: string;
const AParamDescription: string; const AJsonSchemaClass: TClass;
const AParamType: TMVCSwagParamType = ptNotDefined; const ARequired: Boolean = True;
const ADefaultValue: string = ''; const AEnumValues: string = ''); overload;
2019-11-03 16:16:35 +01:00
property ParamLocation: TMVCSwagParamLocation read fParamLocation;
property ParamName: string read fParamName;
property ParamDescription: string read fParamDescription;
property ParamType: TMVCSwagParamType read fParamType;
property Required: Boolean read fRequired;
property DefaultValue: string read fDefaultValue;
property EnumValues: TArray<string> read GetEnumValues;
2019-11-03 16:16:35 +01:00
property JsonSchema: string read fJsonSchema;
property JsonSchemaClass: TClass read fJsonSchemaClass;
2019-07-27 20:23:48 +02:00
end;
/// <summary>
/// Specifies the field definition in the json schema.
/// Use this attribute on a class property that will be mapped to a json schema
/// </summary>
2019-11-03 16:16:35 +01:00
MVCSwagJSONSchemaFieldAttribute = class(TCustomAttribute)
private
FSchemaFieldType: TMVCSwagSchemaType;
FFieldName: string;
2019-11-03 16:16:35 +01:00
fDescription: string;
fRequired: Boolean;
FNullable: Boolean;
public
2019-11-03 16:16:35 +01:00
constructor Create(const ASchemaFieldType: TMVCSwagSchemaType; const AFieldName: string; const ADescription: string;
const ARequired: Boolean = True; const ANullable: Boolean = False); overload;
constructor Create(const AFieldName: string; const ADescription: string; const ARequired: Boolean = True;
const ANullable: Boolean = False); overload;
property SchemaFieldType: TMVCSwagSchemaType read FSchemaFieldType;
property FieldName: string read FFieldName;
2019-11-03 16:16:35 +01:00
property Description: string read fDescription;
property Required: Boolean read fRequired;
property Nullable: Boolean read FNullable;
end;
/// <summary>
/// Use this attribute in the class or method to ignore the path in creating swagger documentation.
/// </summary>
MVCSwagIgnorePathAttribute = class(TCustomAttribute);
/// <summary>
/// SwaggerDoc Methods
/// </summary>
2019-07-27 20:23:48 +02:00
TMVCSwagger = class sealed
private
class var FRttiContext: TRttiContext;
class function GetMVCSwagParamsFromMethod(const AMethod: TRttiMethod): TArray<MVCSwagParamAttribute>;
2019-11-03 16:16:35 +01:00
class function MVCParamLocationToSwagRequestParamInLocation(const AMVCSwagParamLocation: TMVCSwagParamLocation)
: TSwagRequestParameterInLocation;
class function MVCParamTypeToSwagTypeParameter(const AMVSwagParamType: TMVCSwagParamType): TSwagTypeParameter;
class procedure ExtractJsonSchemaFromClass(const AJsonFieldRoot: TJsonFieldObject; const AClass: TClass); overload;
class function ExtractJsonSchemaFromClass(const AClass: TClass; const AIsArray: Boolean = False)
: TJSONObject; overload;
class function GetJsonFieldClass(const ASchemaFieldType: TMVCSwagSchemaType): TJsonFieldClass;
class function TypeKindToMVCSwagSchemaType(APropType: TRttiType): TMVCSwagSchemaType; static;
2019-11-03 16:16:35 +01:00
class function TypeIsEnumerable(const ARttiType: TRttiType): Boolean; static;
2019-07-27 20:23:48 +02:00
public
class constructor Create;
class destructor Destroy;
class function MVCHttpMethodToSwagPathOperation(const AMVCHTTPMethod: TMVCHTTPMethodType): TSwagPathTypeOperation;
class function MVCPathToSwagPath(const AResourcePath: string): string;
class function GetParamsFromMethod(const AResourcePath: string; const AMethod: TRttiMethod;
const ASwagDefinitions: TObjectList<TSwagDefinition>): TArray<TSwagRequestParameter>;
2019-07-27 20:23:48 +02:00
class function RttiTypeToSwagType(const ARttiType: TRttiType): TSwagTypeParameter;
class procedure FillOperationSummary(const ASwagPathOperation: TSwagPathOperation; const AMethod: TRttiMethod;
const ASwagDefinitions: TObjectList<TSwagDefinition>);
class function MethodRequiresAuthentication(const AMethod: TRttiMethod; const AType: TRttiType;
out AAuthenticationTypeName: string): Boolean;
class function GetJWTAuthenticationPath(const AJWTUrlSegment: string; AUserNameHeaderName, APasswordHeaderName: string): TSwagPath;
2019-07-27 20:23:48 +02:00
end;
const
SECURITY_BEARER_NAME = 'bearer';
SECURITY_BASIC_NAME = 'basic';
2019-11-03 16:16:35 +01:00
JWT_JSON_SCHEMA = '{' + sLineBreak + ' "type": "object",' + sLineBreak + ' "properties": {' + sLineBreak +
' "token": {' + sLineBreak + ' "type": "string",' + sLineBreak + ' "description": "JWT Token"' +
sLineBreak + ' }' + sLineBreak + ' }' + sLineBreak + '}';
JWT_DEFAULT_DESCRIPTION = 'For accessing the API a valid JWT token must be passed in all the queries ' +
'in the ''Authorization'' header.' + sLineBreak + sLineBreak +
'A valid JWT token is generated by the API and retourned as answer of a call ' + 'to the route defined ' +
'in the JWT middleware giving a valid username and password.' + sLineBreak + sLineBreak +
'The following syntax must be used in the ''Authorization'' header :' + sLineBreak + sLineBreak +
' Bearer xxxxxx.yyyyyyy.zzzzzz' + sLineBreak;
2019-07-27 20:23:48 +02:00
implementation
uses
System.Classes,
2019-07-27 20:23:48 +02:00
System.RegularExpressions,
MVCFramework,
MVCFramework.Serializer.Abstract,
MVCFramework.Serializer.Commons,
Swag.Doc.Path.Operation.Response,
Json.Schema.Field.Numbers,
Json.Schema.Field.Strings,
Json.Schema.Field.Arrays,
Json.Schema.Field.DateTimes,
Json.Schema.Field.Enums,
Json.Schema.Field.Booleans, System.Generics.Defaults;
2019-07-27 20:23:48 +02:00
{ TSwaggerUtils }
class constructor TMVCSwagger.Create;
begin
FRttiContext := TRttiContext.Create;
end;
class function TMVCSwagger.RttiTypeToSwagType(const ARttiType: TRttiType): TSwagTypeParameter;
begin
case ARttiType.TypeKind of
tkInteger, tkInt64:
Result := stpInteger;
tkChar, tkString, tkWChar, tkLString, tkWString, tkUString:
Result := stpString;
tkFloat:
2019-11-03 16:16:35 +01:00
if (ARttiType.Handle = TypeInfo(TDateTime)) or (ARttiType.Handle = TypeInfo(TDate)) or
2019-07-27 20:23:48 +02:00
(ARttiType.Handle = TypeInfo(TTime)) then
Result := stpString
else
Result := stpNumber;
tkEnumeration:
if ARttiType.Handle = TypeInfo(Boolean) then
Result := stpBoolean
else
Result := stpArray;
else
Result := stpNotDefined;
end;
end;
class destructor TMVCSwagger.Destroy;
begin
FRttiContext.Free;
end;
2019-11-03 16:16:35 +01:00
class function TMVCSwagger.TypeIsEnumerable(const ARttiType: TRttiType): Boolean;
begin
Result := ARttiType.GetMethod('GetEnumerator') <> nil;
end;
type
TFieldSchemaDefinition = record
SchemaFieldType: TMVCSwagSchemaType;
FieldName: string;
Description: string;
Required: Boolean;
Nullable: Boolean;
class function Create: TFieldSchemaDefinition; static; inline;
end;
THackMVCAbstractSerializer = class(TMVCAbstractSerializer);
class procedure TMVCSwagger.ExtractJsonSchemaFromClass(const AJsonFieldRoot: TJsonFieldObject; const AClass: TClass);
var
2019-11-03 16:16:35 +01:00
lFieldSchemaDef: TFieldSchemaDefinition;
lObjType: TRttiType;
lProp: TRttiProperty;
lSkipProp: Boolean;
lAttr: TCustomAttribute;
lJSFieldAttr: MVCSwagJSONSchemaFieldAttribute;
lJsonFieldClass: TJsonFieldClass;
lJsonField: TJsonField;
lJsonFieldObject: TJsonFieldObject;
lAbstractSer: THackMVCAbstractSerializer;
lClass: TClass;
begin
2019-11-03 16:16:35 +01:00
lObjType := FRttiContext.GetType(AClass);
for lProp in lObjType.GetProperties do
begin
2019-11-03 16:16:35 +01:00
lSkipProp := False;
lFieldSchemaDef := TFieldSchemaDefinition.Create;
2019-11-03 16:16:35 +01:00
for lAttr in lProp.GetAttributes do
begin
2019-11-03 16:16:35 +01:00
if lAttr is MVCDoNotSerializeAttribute then
begin
2019-11-03 16:16:35 +01:00
lSkipProp := True;
Break;
end;
2019-11-03 16:16:35 +01:00
if lAttr is MVCSwagJSONSchemaFieldAttribute then
begin
2019-11-03 16:16:35 +01:00
lJSFieldAttr := MVCSwagJSONSchemaFieldAttribute(lAttr);
lFieldSchemaDef.SchemaFieldType := lJSFieldAttr.SchemaFieldType;
lFieldSchemaDef.FieldName := lJSFieldAttr.FieldName;
lFieldSchemaDef.Description := lJSFieldAttr.Description;
lFieldSchemaDef.Required := lJSFieldAttr.Required;
lFieldSchemaDef.Nullable := lJSFieldAttr.Nullable;
Break;
end;
end;
2019-11-03 16:16:35 +01:00
if lSkipProp then
Continue;
2019-11-03 16:16:35 +01:00
if lFieldSchemaDef.SchemaFieldType = stUnknown then
begin
2019-11-03 16:16:35 +01:00
lFieldSchemaDef.SchemaFieldType := TypeKindToMVCSwagSchemaType(lProp.PropertyType);
lFieldSchemaDef.FieldName := TMVCSerializerHelper.GetKeyName(lProp, lObjType);
end;
2019-08-08 23:35:53 +02:00
2019-11-03 16:16:35 +01:00
lJsonFieldClass := GetJsonFieldClass(lFieldSchemaDef.SchemaFieldType);
if not Assigned(lJsonFieldClass) then
Continue;
2019-11-03 16:16:35 +01:00
lJsonField := lJsonFieldClass.Create;
2019-11-03 16:16:35 +01:00
if (lJsonField is TJsonFieldObject) and (not TypeIsEnumerable(lProp.PropertyType)) then
begin
2019-11-03 16:16:35 +01:00
ExtractJsonSchemaFromClass((lJsonField as TJsonFieldObject), lProp.PropertyType.AsInstance.MetaClassType);
end;
2019-11-03 16:16:35 +01:00
if (lJsonField is TJsonFieldArray) and TypeIsEnumerable(lProp.PropertyType) then
begin
2019-11-03 16:16:35 +01:00
lJsonFieldObject := TJsonFieldObject.Create;
2019-11-03 16:16:35 +01:00
lAbstractSer := THackMVCAbstractSerializer.Create;
try
2019-11-03 16:16:35 +01:00
lClass := lAbstractSer.GetObjectTypeOfGenericList(lProp.PropertyType.Handle);
ExtractJsonSchemaFromClass(lJsonFieldObject, lClass);
(lJsonField as TJsonFieldArray).ItemFieldType := lJsonFieldObject;
finally
2019-11-03 16:16:35 +01:00
lAbstractSer.Free;
end;
end;
2019-11-03 16:16:35 +01:00
lJsonField.Name := lFieldSchemaDef.FieldName;
lJsonField.Required := lFieldSchemaDef.Required;
lJsonField.Nullable := lFieldSchemaDef.Nullable;
if not lFieldSchemaDef.Description.IsEmpty then
TJsonFieldInteger(lJsonField).Description := lFieldSchemaDef.Description;
2019-11-03 16:16:35 +01:00
AJsonFieldRoot.AddField(lJsonField);
end;
end;
class function TMVCSwagger.ExtractJsonSchemaFromClass(const AClass: TClass; const AIsArray: Boolean): TJSONObject;
var
2019-11-03 16:16:35 +01:00
lJsonSchema: TJsonField;
lJsonRoot: TJsonFieldObject;
begin
if AIsArray then
2019-11-03 16:16:35 +01:00
begin
lJsonSchema := TJsonFieldArray.Create
end
else
2019-11-03 16:16:35 +01:00
begin
lJsonSchema := TJsonFieldObject.Create;
end;
try
if AIsArray then
begin
2019-11-03 16:16:35 +01:00
lJsonRoot := TJsonFieldObject.Create;
TJsonFieldArray(lJsonSchema).ItemFieldType := lJsonRoot;
TJsonFieldArray(lJsonSchema).Name := 'items';
end
else
begin
2019-11-03 16:16:35 +01:00
lJsonRoot := lJsonSchema as TJsonFieldObject;
end;
2019-11-03 16:16:35 +01:00
ExtractJsonSchemaFromClass(lJsonRoot, AClass);
Result := lJsonSchema.ToJsonSchema;
finally
2019-11-03 16:16:35 +01:00
lJsonSchema.Free;
end;
end;
2019-07-27 20:23:48 +02:00
class procedure TMVCSwagger.FillOperationSummary(const ASwagPathOperation: TSwagPathOperation;
const AMethod: TRttiMethod; const ASwagDefinitions: TObjectList<TSwagDefinition>);
2019-07-27 20:23:48 +02:00
var
2019-11-03 16:16:35 +01:00
lAttr: TCustomAttribute;
lSwagResponse: TSwagResponse;
lSwagResponsesAttr: MVCSwagResponsesAttribute;
lComparer: IComparer<TSwagDefinition>;
lSwagDefinition: TSwagDefinition;
lSwagDef: TSwagDefinition;
lClassName: string;
lIndex: Integer;
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
for lAttr in AMethod.GetAttributes do
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
if lAttr is MVCSwagSummaryAttribute then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
ASwagPathOperation.Tags.AddRange(MVCSwagSummaryAttribute(lAttr).GetTags);
ASwagPathOperation.Description := MVCSwagSummaryAttribute(lAttr).Description;
ASwagPathOperation.OperationID := MVCSwagSummaryAttribute(lAttr).OperationID;
// dt [2019-10-31] Ensure OperationID is always defined
if ASwagPathOperation.OperationID.IsEmpty then
begin
ASwagPathOperation.OperationID := AMethod.Name;
end;
// dt [2019-10-31]
ASwagPathOperation.Deprecated := MVCSwagSummaryAttribute(lAttr).Deprecated;
2019-07-27 20:23:48 +02:00
end;
2019-11-03 16:16:35 +01:00
if lAttr is MVCConsumesAttribute then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
ASwagPathOperation.Consumes.Add(MVCConsumesAttribute(lAttr).Value)
2019-07-27 20:23:48 +02:00
end;
2019-11-03 16:16:35 +01:00
if lAttr is MVCProducesAttribute then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
ASwagPathOperation.Produces.Add(MVCProducesAttribute(lAttr).Value)
2019-07-27 20:23:48 +02:00
end;
2019-11-03 16:16:35 +01:00
if lAttr is MVCSwagResponsesAttribute then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
lSwagResponsesAttr := MVCSwagResponsesAttribute(lAttr);
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := lSwagResponsesAttr.StatusCode.ToString;
lSwagResponse.Description := lSwagResponsesAttr.Description;
if not lSwagResponsesAttr.JsonSchema.IsEmpty then
begin
lSwagResponse.Schema.JsonSchema := TJSONObject.ParseJSONValue(lSwagResponsesAttr.JsonSchema) as TJSONObject
end
else if Assigned(lSwagResponsesAttr.JsonSchemaClass) then
begin
lComparer := TDelegatedComparer<TSwagDefinition>.Create(
function(const Left, Right: TSwagDefinition): Integer
begin
Result := CompareText(Left.Name, Right.Name);
end);
lClassName := lSwagResponsesAttr.JsonSchemaClass.ClassName;
if lClassName.ToUpper.StartsWith('T') then
lClassName := lClassName.Remove(0, 1);
ASwagDefinitions.Sort(lComparer);
lSwagDef := TSwagDefinition.Create;
try
lSwagDef.Name := lClassName;
if not ASwagDefinitions.BinarySearch(lSwagDef, lIndex, lComparer) then
begin
lSwagDefinition := TSwagDefinition.Create;
lSwagDefinition.Name := lClassName;
lSwagDefinition.JsonSchema := ExtractJsonSchemaFromClass(lSwagResponsesAttr.JsonSchemaClass,
lSwagResponsesAttr.IsArray);
ASwagDefinitions.Add(lSwagDefinition);
end;
finally
lSwagDef.Free;
end;
lSwagResponse.Schema.Name := lClassName;
2019-11-03 16:16:35 +01:00
end;
ASwagPathOperation.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
2019-07-27 20:23:48 +02:00
end;
end;
if ASwagPathOperation.Tags.Count = 0 then
ASwagPathOperation.Tags.Add(AMethod.Parent.QualifiedName);
2019-07-27 20:23:48 +02:00
if ASwagPathOperation.Produces.Count <= 0 then
ASwagPathOperation.Produces.Add(TMVCMediaType.APPLICATION_JSON);
if ASwagPathOperation.Responses.Count <= 0 then
begin
2019-11-03 16:16:35 +01:00
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := IntToStr(HTTP_STATUS.OK);
2019-11-03 16:16:35 +01:00
lSwagResponse.Description := 'Ok';
ASwagPathOperation.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := IntToStr(HTTP_STATUS.InternalServerError);
2019-11-03 16:16:35 +01:00
lSwagResponse.Description := 'Internal server error';
ASwagPathOperation.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
2019-07-27 20:23:48 +02:00
end;
end;
class function TMVCSwagger.GetJsonFieldClass(const ASchemaFieldType: TMVCSwagSchemaType): TJsonFieldClass;
begin
case ASchemaFieldType of
stInteger:
Result := TJsonFieldInteger;
stInt64:
Result := TJsonFieldInt64;
stNumber:
Result := TJsonFieldNumber;
stDateTime:
Result := TJsonFieldDateTime;
stDate:
Result := TJsonFieldDate;
stTime:
Result := TJsonFieldTime;
stEnumeration:
Result := TJsonFieldEnum;
stBoolean:
Result := TJsonFieldBoolean;
stObject:
Result := TJsonFieldObject;
stArray:
Result := TJsonFieldArray;
stString, stChar:
Result := TJsonFieldString;
stGuid:
Result := TJsonFieldGuid;
else
Result := nil;
end;
end;
class function TMVCSwagger.TypeKindToMVCSwagSchemaType(APropType: TRttiType): TMVCSwagSchemaType;
begin
Result := stUnknown;
if APropType.TypeKind = tkUnknown then
Exit;
case APropType.TypeKind of
tkClass:
begin
2019-11-03 16:16:35 +01:00
if (APropType.Handle = TypeInfo(TStream)) or (APropType.Handle = TypeInfo(TMemoryStream)) or
(APropType.Handle = TypeInfo(TStringStream)) then
Result := stString
2019-11-03 16:16:35 +01:00
else if TypeIsEnumerable(APropType) then
Result := stArray
else
Result := stObject;
end;
tkArray:
Result := stArray;
tkString, tkUString, tkChar:
Result := stString;
tkRecord:
begin
if APropType.Handle = TypeInfo(TGUID) then
Result := stGuid
end;
tkInteger:
Result := stInteger;
tkInt64:
Result := stInt64;
tkEnumeration:
begin
if APropType.Handle = TypeInfo(Boolean) then
Result := stBoolean
else
Result := stEnumeration;
end;
tkFloat:
begin
if APropType.Handle = TypeInfo(TDateTime) then
Result := stDateTime
else if APropType.Handle = TypeInfo(TDate) then
Result := stDate
else if APropType.Handle = TypeInfo(TTime) then
Result := stTime
else
Result := stNumber;
end;
end;
end;
class function TMVCSwagger.GetJWTAuthenticationPath(const AJWTUrlSegment: string; AUserNameHeaderName, APasswordHeaderName: string): TSwagPath;
var
2019-11-03 16:16:35 +01:00
lSwagPathOp: TSwagPathOperation;
lSwagResponse: TSwagResponse;
lSwagParam: TSwagRequestParameter;
begin
2019-11-03 16:16:35 +01:00
lSwagPathOp := TSwagPathOperation.Create;
lSwagPathOp.Tags.Add('JWT Authentication');
lSwagPathOp.Operation := ohvPost;
lSwagPathOp.Security.Add(SECURITY_BASIC_NAME);
lSwagPathOp.Description := 'Create JSON Web Token';
lSwagPathOp.Produces.Add(TMVCMediaType.APPLICATION_JSON);
lSwagParam := TSwagRequestParameter.Create;
lSwagParam.Name := AUserNameHeaderName;
lSwagParam.TypeParameter := stpString;
lSwagParam.Required := true;
lSwagParam.InLocation := rpiHeader;
lSwagPathOp.Parameters.Add(lSwagParam);
lSwagParam := TSwagRequestParameter.Create;
lSwagParam.Name := APasswordHeaderName;
lSwagParam.TypeParameter := stpString;
lSwagParam.Required := true;
lSwagParam.InLocation := rpiHeader;
lSwagPathOp.Parameters.Add(lSwagParam);
2019-11-03 16:16:35 +01:00
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := IntToStr(HTTP_STATUS.Unauthorized);
2019-11-03 16:16:35 +01:00
lSwagResponse.Description := 'Invalid authorization type';
lSwagPathOp.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := IntToStr(HTTP_STATUS.Forbidden);
2019-11-03 16:16:35 +01:00
lSwagResponse.Description := 'Forbidden';
lSwagPathOp.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := IntToStr(HTTP_STATUS.InternalServerError);
2019-11-03 16:16:35 +01:00
lSwagResponse.Description := 'Internal server error';
lSwagPathOp.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
lSwagResponse := TSwagResponse.Create;
lSwagResponse.StatusCode := IntToStr(HTTP_STATUS.OK);
2019-11-03 16:16:35 +01:00
lSwagResponse.Description := 'OK';
lSwagResponse.Schema.JsonSchema := TJSONObject.ParseJSONValue(JWT_JSON_SCHEMA) as TJSONObject;
lSwagPathOp.Responses.Add(lSwagResponse.StatusCode, lSwagResponse);
Result := TSwagPath.Create;
2019-11-03 16:16:35 +01:00
Result.Uri := AJWTUrlSegment;
Result.Operations.Add(lSwagPathOp);
end;
2019-07-27 20:23:48 +02:00
class function TMVCSwagger.GetMVCSwagParamsFromMethod(const AMethod: TRttiMethod): TArray<MVCSwagParamAttribute>;
var
2019-11-03 16:16:35 +01:00
lAttr: TCustomAttribute;
2019-07-27 20:23:48 +02:00
begin
SetLength(Result, 0);
2019-11-03 16:16:35 +01:00
for lAttr in AMethod.GetAttributes do
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
if lAttr is MVCSwagParamAttribute then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
Insert([MVCSwagParamAttribute(lAttr)], Result, High(Result));
2019-07-27 20:23:48 +02:00
end;
end;
end;
class function TMVCSwagger.GetParamsFromMethod(const AResourcePath: string; const AMethod: TRttiMethod;
const ASwagDefinitions: TObjectList<TSwagDefinition>): TArray<TSwagRequestParameter>;
2019-07-27 20:23:48 +02:00
2019-11-03 16:16:35 +01:00
function TryGetMVCPathParamByName(const AParams: TArray<MVCSwagParamAttribute>; const AParamName: string;
out AMVCParam: MVCSwagParamAttribute; out AIndex: Integer): Boolean;
2019-07-27 20:23:48 +02:00
var
I: Integer;
begin
Result := False;
AMVCParam := nil;
2019-11-03 16:16:35 +01:00
AIndex := -1;
2019-07-27 20:23:48 +02:00
for I := Low(AParams) to High(AParams) do
if SameText(AParams[I].ParamName, AParamName) and (AParams[I].ParamLocation = plPath) then
begin
AMVCParam := AParams[I];
AIndex := I;
Exit(True);
end;
end;
var
2019-11-03 16:16:35 +01:00
lMatches: TMatchCollection;
lMatch: TMatch;
lParamName: string;
lMethodParam: TRttiParameter;
lSwagParam: TSwagRequestParameter;
lMVCSwagParams: TArray<MVCSwagParamAttribute>;
lMVCParam: MVCSwagParamAttribute;
lIndex: Integer;
2019-07-27 20:23:48 +02:00
I: Integer;
lComparer: IComparer<TSwagDefinition>;
lClassName: string;
lSwagDef: TSwagDefinition;
lSwagDefinition: TSwagDefinition;
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
lMVCSwagParams := GetMVCSwagParamsFromMethod(AMethod);
2019-07-27 20:23:48 +02:00
SetLength(Result, 0);
// Path parameters
2019-11-03 16:16:35 +01:00
lMatches := TRegEx.Matches(AResourcePath, '({)([\w_]+)(})', [roIgnoreCase, roMultiLine]);
for lMatch in lMatches do
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
lParamName := lMatch.Groups[2].Value;
for lMethodParam in AMethod.GetParameters do
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
if SameText(lMethodParam.Name, lParamName) then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
lSwagParam := TSwagRequestParameter.Create;
2019-07-27 20:23:48 +02:00
2019-11-03 16:16:35 +01:00
if TryGetMVCPathParamByName(lMVCSwagParams, lParamName, lMVCParam, lIndex) then
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
lSwagParam.Name := lParamName;
lSwagParam.InLocation := MVCParamLocationToSwagRequestParamInLocation(lMVCParam.ParamLocation);
lSwagParam.Required := lMVCParam.Required;
lSwagParam.Default := lMVCParam.DefaultValue;
lSwagParam.Enum.Text := string.Join(sLineBreak, lMVCParam.EnumValues);
2019-11-03 16:16:35 +01:00
lSwagParam.TypeParameter := MVCParamTypeToSwagTypeParameter(lMVCParam.ParamType);
lSwagParam.Description := lMVCParam.ParamDescription;
if not lMVCParam.JsonSchema.IsEmpty then
begin
lSwagParam.Schema.JsonSchema := TJSONObject.ParseJSONValue(lMVCParam.JsonSchema) as TJSONObject
end
else if Assigned(lMVCParam.JsonSchemaClass) then
begin
lComparer := TDelegatedComparer<TSwagDefinition>.Create(
function(const Left, Right: TSwagDefinition): Integer
begin
Result := CompareText(Left.Name, Right.Name);
end);
lClassName := lMVCParam.JsonSchemaClass.ClassName;
if lClassName.ToUpper.StartsWith('T') then
lClassName := lClassName.Remove(0, 1);
ASwagDefinitions.Sort(lComparer);
lSwagDef := TSwagDefinition.Create;
try
lSwagDef.Name := lClassName;
if not ASwagDefinitions.BinarySearch(lSwagDef, lIndex, lComparer) then
begin
lSwagDefinition := TSwagDefinition.Create;
lSwagDefinition.Name := lClassName;
lSwagDefinition.JsonSchema := ExtractJsonSchemaFromClass(lMVCParam.JsonSchemaClass,
2019-11-03 16:16:35 +01:00
lMVCParam.ParamType = ptArray);
ASwagDefinitions.Add(lSwagDefinition);
end;
finally
lSwagDef.Free;
end;
lSwagParam.Schema.Name := lClassName;
2019-11-03 16:16:35 +01:00
end;
Delete(lMVCSwagParams, lIndex, 1);
2019-07-27 20:23:48 +02:00
end
else
begin
2019-11-03 16:16:35 +01:00
lSwagParam.Name := lParamName;
lSwagParam.InLocation := rpiPath;
lSwagParam.Required := True;
lSwagParam.TypeParameter := RttiTypeToSwagType(lMethodParam.ParamType);
2019-07-27 20:23:48 +02:00
end;
2019-11-03 16:16:35 +01:00
Insert([lSwagParam], Result, High(Result));
2019-07-27 20:23:48 +02:00
end;
end;
end;
// Other parameters
2019-11-03 16:16:35 +01:00
for I := Low(lMVCSwagParams) to High(lMVCSwagParams) do
2019-07-27 20:23:48 +02:00
begin
2019-11-03 16:16:35 +01:00
lSwagParam := TSwagRequestParameter.Create;
lSwagParam.Name := lMVCSwagParams[I].ParamName;
lSwagParam.InLocation := MVCParamLocationToSwagRequestParamInLocation(lMVCSwagParams[I].ParamLocation);
lSwagParam.Required := lMVCSwagParams[I].Required;
lSwagParam.Default := lMVCSwagParams[I].DefaultValue;
lSwagParam.Enum.Text := string.Join(sLineBreak, lMVCSwagParams[I].EnumValues);
2019-11-03 16:16:35 +01:00
lSwagParam.TypeParameter := MVCParamTypeToSwagTypeParameter(lMVCSwagParams[I].ParamType);
lSwagParam.Description := lMVCSwagParams[I].ParamDescription;
if not lMVCSwagParams[I].JsonSchema.IsEmpty then
begin
lSwagParam.Schema.JsonSchema := TJSONObject.ParseJSONValue(lMVCSwagParams[I].JsonSchema) as TJSONObject
end
else if Assigned(lMVCSwagParams[I].JsonSchemaClass) then
begin
lSwagParam.Schema.JsonSchema := ExtractJsonSchemaFromClass(lMVCSwagParams[I].JsonSchemaClass,
lMVCSwagParams[I].ParamType = ptArray);
end;
Insert([lSwagParam], Result, High(Result));
2019-07-27 20:23:48 +02:00
end;
end;
class function TMVCSwagger.MethodRequiresAuthentication(const AMethod: TRttiMethod; const AType: TRttiType;
out AAuthenticationTypeName: string): Boolean;
var
2019-11-03 16:16:35 +01:00
lAttr: TCustomAttribute;
begin
Result := False;
AAuthenticationTypeName := '';
2019-11-03 16:16:35 +01:00
for lAttr in AMethod.GetAttributes do
if lAttr is MVCRequiresAuthenticationAttribute then
begin
AAuthenticationTypeName := SECURITY_BEARER_NAME;
Exit(True);
end
2019-11-03 16:16:35 +01:00
else if lAttr is MVCSwagAuthenticationAttribute then
begin
2019-11-03 16:16:35 +01:00
case MVCSwagAuthenticationAttribute(lAttr).AuthenticationType of
atBasic:
AAuthenticationTypeName := SECURITY_BASIC_NAME;
atJsonWebToken:
AAuthenticationTypeName := SECURITY_BEARER_NAME;
end;
Exit(True);
end;
2019-11-03 16:16:35 +01:00
for lAttr in AType.GetAttributes do
if lAttr is MVCRequiresAuthenticationAttribute then
begin
AAuthenticationTypeName := SECURITY_BEARER_NAME;
Exit(True);
end
2019-11-03 16:16:35 +01:00
else if lAttr is MVCSwagAuthenticationAttribute then
begin
2019-11-03 16:16:35 +01:00
case MVCSwagAuthenticationAttribute(lAttr).AuthenticationType of
atBasic:
AAuthenticationTypeName := SECURITY_BASIC_NAME;
atJsonWebToken:
AAuthenticationTypeName := SECURITY_BEARER_NAME;
end;
Exit(True);
end;
end;
2019-11-03 16:16:35 +01:00
class function TMVCSwagger.MVCHttpMethodToSwagPathOperation(const AMVCHTTPMethod: TMVCHTTPMethodType)
: TSwagPathTypeOperation;
2019-07-27 20:23:48 +02:00
begin
case AMVCHTTPMethod of
httpGET:
Result := ohvGet;
httpPOST:
Result := ohvPost;
httpPUT:
Result := ohvPut;
httpDELETE:
Result := ohvDelete;
httpHEAD:
Result := ohvHead;
httpOPTIONS:
Result := ohvOptions;
httpPATCH:
Result := ohvPatch;
else
Result := ohvNotDefined;
end;
end;
class function TMVCSwagger.MVCParamLocationToSwagRequestParamInLocation(const AMVCSwagParamLocation
2019-11-03 16:16:35 +01:00
: TMVCSwagParamLocation): TSwagRequestParameterInLocation;
2019-07-27 20:23:48 +02:00
begin
case AMVCSwagParamLocation of
plQuery:
Result := rpiQuery;
plHeader:
Result := rpiHeader;
plPath:
Result := rpiPath;
plFormData:
Result := rpiFormData;
plBody:
Result := rpiBody;
else
Result := rpiNotDefined;
end;
end;
class function TMVCSwagger.MVCParamTypeToSwagTypeParameter(const AMVSwagParamType: TMVCSwagParamType)
: TSwagTypeParameter;
2019-07-27 20:23:48 +02:00
begin
case AMVSwagParamType of
ptString:
Result := stpString;
ptNumber:
Result := stpNumber;
ptInteger:
Result := stpInteger;
ptBoolean:
Result := stpBoolean;
ptArray:
Result := stpArray;
ptFile:
Result := stpFile;
else
Result := stpNotDefined;
end;
end;
class function TMVCSwagger.MVCPathToSwagPath(const AResourcePath: string): string;
begin
Result := TRegEx.Replace(AResourcePath, '(\([($])([\w_]+)([)])', '{\2}', [roIgnoreCase, roMultiLine]);
end;
{ MVCSwagSummary }
2019-11-03 16:16:35 +01:00
constructor MVCSwagSummaryAttribute.Create(const ATags, ADescription: string; const AOperationId: string;
2019-07-27 20:23:48 +02:00
ADeprecated: Boolean);
begin
2019-11-03 16:16:35 +01:00
fTags := ATags;
fDescription := ADescription;
fOperationID := AOperationId;
fDeprecated := ADeprecated;
2019-07-27 20:23:48 +02:00
end;
function MVCSwagSummaryAttribute.GetTags: TArray<string>;
begin
2019-11-03 16:16:35 +01:00
Result := fTags.Split([',']);
2019-07-27 20:23:48 +02:00
end;
{ MVCSwagResponsesAttribute }
constructor MVCSwagResponsesAttribute.Create(const AStatusCode: Integer; const ADescription: string;
const AJsonSchema: string);
begin
2019-11-03 16:16:35 +01:00
fStatusCode := AStatusCode;
fDescription := ADescription;
fJsonSchema := AJsonSchema;
fJsonSchemaClass := nil;
end;
constructor MVCSwagResponsesAttribute.Create(const AStatusCode: Integer; const ADescription: string;
const AJsonSchemaClass: TClass; const AIsArray: Boolean);
begin
Create(AStatusCode, ADescription, '');
2019-11-03 16:16:35 +01:00
fJsonSchemaClass := AJsonSchemaClass;
fIsArray := AIsArray;
2019-07-27 20:23:48 +02:00
end;
{ MVCSwagParamAttribute }
2019-11-03 16:16:35 +01:00
constructor MVCSwagParamAttribute.Create(const AParamLocation: TMVCSwagParamLocation;
const AParamName, AParamDescription: string; const AParamType: TMVCSwagParamType; const ARequired: Boolean;
const ADefaultValue, AEnumValues, AJsonSchema: string);
begin
2019-11-03 16:16:35 +01:00
fParamLocation := AParamLocation;
fParamName := AParamName;
fParamDescription := AParamDescription;
fParamType := AParamType;
fRequired := ARequired;
fDefaultValue := ADefaultValue;
fEnumValues := AEnumValues;
2019-11-03 16:16:35 +01:00
fJsonSchema := AJsonSchema;
fJsonSchemaClass := nil;
end;
2019-11-03 16:16:35 +01:00
constructor MVCSwagParamAttribute.Create(const AParamLocation: TMVCSwagParamLocation;
const AParamName, AParamDescription: string; const AJsonSchemaClass: TClass; const AParamType: TMVCSwagParamType;
const ARequired: Boolean; const ADefaultValue, AEnumValues: string);
2019-07-27 20:23:48 +02:00
begin
Create(AParamLocation, AParamName, AParamDescription, AParamType, ARequired, ADefaultValue, AEnumValues, '');
2019-11-03 16:16:35 +01:00
fJsonSchemaClass := AJsonSchemaClass;
2019-07-27 20:23:48 +02:00
end;
function MVCSwagParamAttribute.GetEnumValues: TArray<string>;
begin
Result := fEnumValues.Split([',', ';']);
end;
2019-11-03 16:16:35 +01:00
{ MVCSwagJSONSchemaFieldAttribute }
2019-11-03 16:16:35 +01:00
constructor MVCSwagJSONSchemaFieldAttribute.Create(const AFieldName, ADescription: string;
const ARequired, ANullable: Boolean);
begin
Create(stUnknown, AFieldName, ADescription, ARequired, ANullable);
end;
2019-11-03 16:16:35 +01:00
constructor MVCSwagJSONSchemaFieldAttribute.Create(const ASchemaFieldType: TMVCSwagSchemaType;
const AFieldName, ADescription: string; const ARequired, ANullable: Boolean);
begin
FSchemaFieldType := ASchemaFieldType;
FFieldName := AFieldName;
2019-11-03 16:16:35 +01:00
fDescription := ADescription;
fRequired := ARequired;
FNullable := ANullable;
end;
{ TFieldSchemaDefinition }
class function TFieldSchemaDefinition.Create: TFieldSchemaDefinition;
begin
Result.SchemaFieldType := stUnknown;
Result.FieldName := '';
Result.Description := '';
Result.Required := False;
Result.Nullable := False;
end;
{ MVCSwagAuthenticationAttribute }
constructor MVCSwagAuthenticationAttribute.Create(const AAuthenticationType: TMVCSwagAuthenticationType);
begin
FAuthenticationType := AAuthenticationType;
end;
2019-07-27 20:23:48 +02:00
end.