function OKResponse(const Body: TObject): IMVCResponse; overload;
    function OKResponse: IMVCResponse; overload;
    function NotFoundResponse(const Body: TObject): IMVCResponse; overload;
    function NotFoundResponse: IMVCResponse; overload;
    function NoContentResponse: IMVCResponse;
    function UnauthorizedResponse: IMVCResponse;
    function BadRequestResponse: IMVCResponse; overload;
    function BadRequestResponse(const Error: TObject): IMVCResponse; overload;
    function CreatedResponse(const Location: string = ''; const Body: TObject = nil): IMVCResponse;
    function AcceptedResponse(const Location: string = ''; const Body: TObject = nil): IMVCResponse;
    function ConflictResult: IMVCResponse;
    function InternalServerErrorResponse: IMVCResponse;
This commit is contained in:
Daniele Teti 2024-04-03 16:11:38 +02:00
parent 8c80ed7a3e
commit af0984e050
4 changed files with 188 additions and 60 deletions

Binary file not shown.

View File

@ -8,7 +8,7 @@ uses
FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf,
FireDAC.DApt.Intf, Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client, FireDAC.DApt.Intf, Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client,
Vcl.Grids, Vcl.DBGrids, Vcl.ExtCtrls, Vcl.StdCtrls, MVCFramework.RESTClient.Intf, MVCFramework.RESTClient, Vcl.Grids, Vcl.DBGrids, Vcl.ExtCtrls, Vcl.StdCtrls, MVCFramework.RESTClient.Intf, MVCFramework.RESTClient,
Vcl.DBCtrls, MVCFramework.DataSet.Utils; Vcl.DBCtrls, MVCFramework.DataSet.Utils, Vcl.Buttons;
type type
TMainForm = class(TForm) TMainForm = class(TForm)
@ -38,11 +38,8 @@ type
procedure btnFilterClick(Sender: TObject); procedure btnFilterClick(Sender: TObject);
private private
fFilter: string; fFilter: string;
fLoading: Boolean;
fRESTClient: IMVCRESTClient; fRESTClient: IMVCRESTClient;
fAPIBinder: TMVCAPIBinder; fAPIBinder: TMVCAPIBinder;
{ Private declarations }
procedure ShowError(const AResponse: IMVCRESTResponse);
procedure SetFilter(const Value: string); procedure SetFilter(const Value: string);
public public
property Filter: string read fFilter write SetFilter; property Filter: string read fFilter write SetFilter;
@ -102,18 +99,4 @@ begin
EditFilter.Text := Value; EditFilter.Text := Value;
end; end;
procedure TMainForm.ShowError(const AResponse: IMVCRESTResponse);
begin
if not AResponse.Success then
MessageDlg(
AResponse.StatusCode.ToString + ': ' + AResponse.StatusText + sLineBreak +
'[' + AResponse.Content + ']',
mtError, [mbOK], 0)
else
MessageDlg(
AResponse.StatusCode.ToString + ': ' + AResponse.StatusText + sLineBreak +
AResponse.Content,
mtError, [mbOK], 0);
end;
end. end.

View File

@ -82,6 +82,11 @@ type
function GetMVCResponseWithObjectDictionary: IMVCResponse; function GetMVCResponseWithObjectDictionary: IMVCResponse;
[MVCPath('/mvcresponse/message/builder/headers')] [MVCPath('/mvcresponse/message/builder/headers')]
function GetMVCResponseSimpleBuilderWithHeaders: IMVCResponse; function GetMVCResponseSimpleBuilderWithHeaders: IMVCResponse;
[MVCPath('/mvcresponse/message/builder/nobody')]
function GetMVCResponseNoBody: IMVCResponse;
// Standard Responses
[MVCPath('/mvcresponse/ok')]
function GetOKResponse: IMVCResponse;
end; end;
implementation implementation
@ -132,6 +137,15 @@ end;
function TMyController.GetMVCResponseNoBody: IMVCResponse;
begin
Result := MVCResponseBuilder
.StatusCode(HTTP_STATUS.OK)
.Header('header1', 'Hello World')
.Header('header2', 'foo bar')
.Build;
end;
function TMyController.GetMVCResponseSimple: IMVCResponse; function TMyController.GetMVCResponseSimple: IMVCResponse;
begin begin
Result := MVCResponseBuilder Result := MVCResponseBuilder
@ -152,10 +166,7 @@ end;
function TMyController.GetMVCResponseWithData: IMVCResponse; function TMyController.GetMVCResponseWithData: IMVCResponse;
begin begin
Result := MVCResponseBuilder Result := OKResponse(TPerson.Create('Daniele','Teti', 99));
.StatusCode(HTTP_STATUS.OK)
.Body(TPerson.Create('Daniele','Teti', 99))
.Build;
end; end;
function TMyController.GetMVCResponseWithDataAndMessage: IMVCResponse; function TMyController.GetMVCResponseWithDataAndMessage: IMVCResponse;
@ -191,22 +202,21 @@ end;
function TMyController.GetMVCResponseWithObjectList: IMVCResponse; function TMyController.GetMVCResponseWithObjectList: IMVCResponse;
begin begin
Result := MVCResponseBuilder Result := OKResponse(TObjectList<TPerson>.Create([
.StatusCode(HTTP_STATUS.OK) TPerson.Create('Daniele','Teti', 99),
.Body(TObjectList<TPerson>.Create([ TPerson.Create('Peter','Parker', 25),
TPerson.Create('Daniele','Teti', 99), TPerson.Create('Bruce','Banner', 45)
TPerson.Create('Peter','Parker', 25), ]));
TPerson.Create('Bruce','Banner', 45) end;
])
).Build; function TMyController.GetOKResponse: IMVCResponse;
begin
Result := OKResponse;
end; end;
function TMyController.GetMVCResponseWithJSON: IMVCResponse; function TMyController.GetMVCResponseWithJSON: IMVCResponse;
begin begin
Result := MVCResponseBuilder Result := OKResponse(StrToJSONObject('{"name":"Daniele","surname":"Teti"}'));
.StatusCode(HTTP_STATUS.OK)
.Body(StrToJSONObject('{"name":"Daniele","surname":"Teti"}'))
.Build;
end; end;
function TMyController.GetSingleObject: TPerson; function TMyController.GetSingleObject: TPerson;

View File

@ -675,12 +675,37 @@ type
var AIsAuthorized: Boolean); var AIsAuthorized: Boolean);
end; end;
// std responses
IMVCResponse = interface
['{9DFEC741-EE38-4AC9-9C2C-9EA0D15D08D5}']
function GetData: TObject;
function GetMessage: string;
function GetStatusCode: Integer;
function GetHeaders: TStringList;
procedure SetData(const Value: TObject);
procedure SetMessage(const Value: string);
procedure SetHeaders(const Headers: TStringList);
procedure SetObjectDictionary(const Value: IMVCObjectDictionary);
function GetObjectDictionary: IMVCObjectDictionary;
procedure SetStatusCode(const Value: Integer);
function GetIgnoredList: TMVCIgnoredList;
function HasHeaders: Boolean;
function HasBody: Boolean;
property StatusCode: Integer read GetStatusCode write SetStatusCode;
property Message: string read GetMessage write SetMessage;
property Data: TObject read GetData write SetData;
property ObjectDictionary: IMVCObjectDictionary read GetObjectDictionary write SetObjectDictionary;
property Headers: TStringList read GetHeaders write SetHeaders;
end;
TMVCRenderer = class(TMVCBase) TMVCRenderer = class(TMVCBase)
protected protected
FContext: TWebContext; FContext: TWebContext;
FContentCharset: string; FContentCharset: string;
FResponseStream: TStringBuilder; FResponseStream: TStringBuilder;
function ToMVCList(const AObject: TObject; AOwnsObject: Boolean = False): IMVCList; function ToMVCList(const AObject: TObject; AOwnsObject: Boolean = False): IMVCList;
function StatusCodeResponseWithOptionalBody(const StatusCode: Word; const Body: TObject): IMVCResponse;
public { this must be public because of entity processors } public { this must be public because of entity processors }
function GetContentType: string; function GetContentType: string;
function GetStatusCode: Integer; function GetStatusCode: Integer;
@ -690,6 +715,9 @@ type
procedure Redirect(const AUrl: string); virtual; procedure Redirect(const AUrl: string); virtual;
procedure ResponseStatus(const AStatusCode: Integer; const AReasonString: string = ''); virtual; procedure ResponseStatus(const AStatusCode: Integer; const AReasonString: string = ''); virtual;
class procedure InternalRenderMVCResponse(const Controller: TMVCRenderer; const MVCResponse: TMVCResponse); class procedure InternalRenderMVCResponse(const Controller: TMVCRenderer; const MVCResponse: TMVCResponse);
////////////////////////////////////////////////////////////////////////////
///
/// <summary> /// <summary>
/// HTTP Status 201 indicates that as a result of HTTP POST request, one or more new resources have been successfully created on server. /// HTTP Status 201 indicates that as a result of HTTP POST request, one or more new resources have been successfully created on server.
/// The response may contain URI in Location header field in HTTP headers list, which can have reference to the newly created resource. Also, response payload also may include an entity containing a list of resource characteristics and location(s) from which the user or user agent can choose the one most appropriate. /// The response may contain URI in Location header field in HTTP headers list, which can have reference to the newly created resource. Also, response payload also may include an entity containing a list of resource characteristics and location(s) from which the user or user agent can choose the one most appropriate.
@ -699,7 +727,36 @@ type
/// https://restfulapi.net/http-status-201-created/ /// https://restfulapi.net/http-status-201-created/
/// </remarks> /// </remarks>
procedure Render201Created(const Location: string = ''; procedure Render201Created(const Location: string = '';
const Reason: string = ''); virtual; const Reason: string = ''); virtual; deprecated;
//Response Result
{
BadRequestResult
ConflictResult
NoContentResult
NotFoundResult
OkResult
UnauthorizedResult
UnprocessableEntityResult
UnsupportedMediaTypeResult
ConflictResult
InternalServerErrorResult
}
function OKResponse(const Body: TObject): IMVCResponse; overload;
function OKResponse: IMVCResponse; overload;
function NotFoundResponse(const Body: TObject): IMVCResponse; overload;
function NotFoundResponse: IMVCResponse; overload;
function NoContentResponse: IMVCResponse;
function UnauthorizedResponse: IMVCResponse;
function BadRequestResponse: IMVCResponse; overload;
function BadRequestResponse(const Error: TObject): IMVCResponse; overload;
function CreatedResponse(const Location: string = ''; const Body: TObject = nil): IMVCResponse;
function AcceptedResponse(const Location: string = ''; const Body: TObject = nil): IMVCResponse;
function ConflictResult: IMVCResponse;
function InternalServerErrorResponse: IMVCResponse;
/// <summary> /// <summary>
/// Allow a server to accept a request for some other process (perhaps a batch-oriented process that is only run once per day) without requiring that the user agent’s connection to the server persist until the process is completed. /// Allow a server to accept a request for some other process (perhaps a batch-oriented process that is only run once per day) without requiring that the user agent’s connection to the server persist until the process is completed.
/// The entity returned with this response SHOULD describe the request’s current status and point to (or embed) a status monitor that can provide the user with (or without) an estimate of when the request will be fulfilled. /// The entity returned with this response SHOULD describe the request’s current status and point to (or embed) a status monitor that can provide the user with (or without) an estimate of when the request will be fulfilled.
@ -708,13 +765,18 @@ type
/// https://restfulapi.net/http-status-202-accepted/ /// https://restfulapi.net/http-status-202-accepted/
/// </remarks> /// </remarks>
procedure Render202Accepted(const HREF: string; const ID: string; procedure Render202Accepted(const HREF: string; const ID: string;
const Reason: string = 'Accepted'); virtual; const Reason: string = 'Accepted'); virtual; deprecated;
/// <summary> /// <summary>
/// HTTP Status 204 (No Content) indicates that the server has successfully fulfilled the request and that there is no content to send in the response payload body. The server might want to return updated meta information in the form of entity-headers, which if present SHOULD be applied to current document’s active view if any. /// HTTP Status 204 (No Content) indicates that the server has successfully fulfilled the request and that there is no content to send in the response payload body. The server might want to return updated meta information in the form of entity-headers, which if present SHOULD be applied to current document’s active view if any.
/// The 204 response MUST NOT include a message-body and thus is always terminated by the first empty line after the header fields. /// The 204 response MUST NOT include a message-body and thus is always terminated by the first empty line after the header fields.
/// </summary> /// </summary>
procedure Render204NoContent(const Location: string = ''; procedure Render204NoContent(const Location: string = '';
const Reason: string = ''); virtual; const Reason: string = ''); virtual; deprecated;
////////////////////////////////////////////////////////////////////////////
function Serializer: IMVCSerializer; overload; function Serializer: IMVCSerializer; overload;
function Serializer(const AContentType: string; function Serializer(const AContentType: string;
const ARaiseExceptionIfNotExists: Boolean = True): IMVCSerializer; overload; const ARaiseExceptionIfNotExists: Boolean = True): IMVCSerializer; overload;
@ -1091,29 +1153,6 @@ type
end; end;
// std responses
IMVCResponse = interface
['{9DFEC741-EE38-4AC9-9C2C-9EA0D15D08D5}']
function GetData: TObject;
function GetMessage: string;
function GetStatusCode: Integer;
function GetHeaders: TStringList;
procedure SetData(const Value: TObject);
procedure SetMessage(const Value: string);
procedure SetHeaders(const Headers: TStringList);
procedure SetObjectDictionary(const Value: IMVCObjectDictionary);
function GetObjectDictionary: IMVCObjectDictionary;
procedure SetStatusCode(const Value: Integer);
function GetIgnoredList: TMVCIgnoredList;
function HasHeaders: Boolean;
function HasBody: Boolean;
property StatusCode: Integer read GetStatusCode write SetStatusCode;
property Message: string read GetMessage write SetMessage;
property Data: TObject read GetData write SetData;
property ObjectDictionary: IMVCObjectDictionary read GetObjectDictionary write SetObjectDictionary;
property Headers: TStringList read GetHeaders write SetHeaders;
end;
TMVCBaseResponse = class abstract (TInterfacedObject, IMVCResponse) TMVCBaseResponse = class abstract (TInterfacedObject, IMVCResponse)
protected protected
@ -3902,6 +3941,55 @@ begin
'Hint: Messaging extensions require a valid clientid. Did you call /messages/clients/YOUR_CLIENT_ID ?'); 'Hint: Messaging extensions require a valid clientid. Did you call /messages/clients/YOUR_CLIENT_ID ?');
end; end;
function TMVCRenderer.BadRequestResponse: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.BadRequest, nil);
end;
function TMVCRenderer.AcceptedResponse(const Location: string;
const Body: TObject): IMVCResponse;
var
lRespBuilder: IMVCResponseBuilder;
begin
lRespBuilder := MVCResponseBuilder;
if not Location.IsEmpty then
begin
lRespBuilder.Header('location', Location)
end;
if Assigned(Body) then
begin
lRespBuilder.Body(Body, True);
end;
Result := lRespBuilder.StatusCode(HTTP_STATUS.Accepted).Build;
end;
function TMVCRenderer.BadRequestResponse(const Error: TObject): IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.BadRequest, Error);
end;
function TMVCRenderer.ConflictResult: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.Conflict, nil);
end;
function TMVCRenderer.CreatedResponse(const Location: string;
const Body: TObject): IMVCResponse;
var
lRespBuilder: IMVCResponseBuilder;
begin
lRespBuilder := MVCResponseBuilder;
if not Location.IsEmpty then
begin
lRespBuilder.Header('location', Location)
end;
if Assigned(Body) then
begin
lRespBuilder.Body(Body, True);
end;
Result := lRespBuilder.StatusCode(HTTP_STATUS.Created).Build;
end;
function TMVCRenderer.GetContentType: string; function TMVCRenderer.GetContentType: string;
begin begin
Result := GetContext.Response.ContentType.Trim; Result := GetContext.Response.ContentType.Trim;
@ -3990,6 +4078,36 @@ end;
end; end;
function TMVCRenderer.InternalServerErrorResponse: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.InternalServerError, nil);
end;
function TMVCRenderer.NoContentResponse: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.NoContent, nil);
end;
function TMVCRenderer.NotFoundResponse: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.NotFound, nil);
end;
function TMVCRenderer.NotFoundResponse(const Body: TObject): IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.NotFound, Body);
end;
function TMVCRenderer.OKResponse: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.OK, nil);
end;
function TMVCRenderer.OKResponse(const Body: TObject): IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.OK, Body);
end;
function TMVCController.GetViewData(const aModelName: string): TValue; function TMVCController.GetViewData(const aModelName: string): TValue;
begin begin
if not FViewModel.TryGetValue(aModelName, Result) then if not FViewModel.TryGetValue(aModelName, Result) then
@ -4274,11 +4392,28 @@ begin
GetContext.Response.StatusCode := AValue; GetContext.Response.StatusCode := AValue;
end; end;
function TMVCRenderer.StatusCodeResponseWithOptionalBody(const StatusCode: Word; const Body: TObject): IMVCResponse;
begin
if Body = nil then
begin
Result := MVCResponseBuilder.StatusCode(StatusCode).Build;
end
else
begin
Result := MVCResponseBuilder.StatusCode(StatusCode).Body(Body, True).Build;
end;
end;
function TMVCRenderer.ToMVCList(const AObject: TObject; AOwnsObject: Boolean): IMVCList; function TMVCRenderer.ToMVCList(const AObject: TObject; AOwnsObject: Boolean): IMVCList;
begin begin
Result := MVCFramework.DuckTyping.WrapAsList(AObject, AOwnsObject); Result := MVCFramework.DuckTyping.WrapAsList(AObject, AOwnsObject);
end; end;
function TMVCRenderer.UnauthorizedResponse: IMVCResponse;
begin
Result := StatusCodeResponseWithOptionalBody(HTTP_STATUS.Unauthorized, nil);
end;
procedure TMVCController.SetETag(const Data: String); procedure TMVCController.SetETag(const Data: String);
begin begin
Context.Response.SetCustomHeader('ETag', GetSHA1HashFromString(Data)); Context.Response.SetCustomHeader('ETag', GetSHA1HashFromString(Data));