From 02c0ae0f37411745281cc559e6d08d17f4dcc4c8 Mon Sep 17 00:00:00 2001 From: Daniele Teti Date: Thu, 22 Feb 2024 19:18:34 +0100 Subject: [PATCH] Added TEXT serializer - Improved content type handling in case of errors and no_route_found cases. --- sources/MVCFramework.Serializer.Commons.pas | 6 + sources/MVCFramework.Serializer.HTML.pas | 3 +- sources/MVCFramework.Serializer.Text.pas | 372 ++++++++++++++++++ sources/MVCFramework.pas | 199 +++++----- unittests/general/Several/LiveServerTestU.pas | 36 +- unittests/general/TestServer/TestServer.dpr | 5 +- unittests/general/TestServer/TestServer.dproj | 3 + .../general/TestServer/WebModuleUnit.pas | 2 +- 8 files changed, 525 insertions(+), 101 deletions(-) create mode 100644 sources/MVCFramework.Serializer.Text.pas diff --git a/sources/MVCFramework.Serializer.Commons.pas b/sources/MVCFramework.Serializer.Commons.pas index baa3cf24..5b1dedcb 100644 --- a/sources/MVCFramework.Serializer.Commons.pas +++ b/sources/MVCFramework.Serializer.Commons.pas @@ -458,6 +458,7 @@ implementation uses Data.FmtBcd, + Data.SqlTimSt, MVCFramework.Nullables, System.Generics.Defaults; @@ -1195,6 +1196,11 @@ begin begin aRTTIField.SetValue(AObject, AField.AsDateTime); end; + ftTimeStampOffset: + begin + aRTTIField.SetValue(AObject, + TValue.From(AField.AsSQLTimeStampOffset)); + end; ftBoolean: begin aRTTIField.SetValue(AObject, AField.AsBoolean); diff --git a/sources/MVCFramework.Serializer.HTML.pas b/sources/MVCFramework.Serializer.HTML.pas index 132bbbb2..9ff10b1a 100644 --- a/sources/MVCFramework.Serializer.HTML.pas +++ b/sources/MVCFramework.Serializer.HTML.pas @@ -313,7 +313,8 @@ function TMVCHTMLSerializer.SerializeObject(const AObject: TObject; const ASerializationAction: TMVCSerializationAction): string; function EmitExceptionClass(const ClazzName, Message: string): string; begin - Result := Result + '

' + HTMLEntitiesEncode(ClazzName) + ': ' + Message + '

'; + Result := Result + '

' + + IfThen(not ClazzName.IsEmpty, HTMLEntitiesEncode(ClazzName) + ': ') + Message + '

'; end; function EmitTitle(const HTTPStatusCode: Word): string; begin diff --git a/sources/MVCFramework.Serializer.Text.pas b/sources/MVCFramework.Serializer.Text.pas new file mode 100644 index 00000000..8b16472c --- /dev/null +++ b/sources/MVCFramework.Serializer.Text.pas @@ -0,0 +1,372 @@ +// *************************************************************************** +// +// Delphi MVC Framework +// +// Copyright (c) 2010-2024 Daniele Teti and the DMVCFramework Team +// +// https://github.com/danieleteti/delphimvcframework +// +// *************************************************************************** +// +// 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.Serializer.Text; + +{$I dmvcframework.inc} +{$WARN SYMBOL_DEPRECATED OFF} + +interface + +uses + System.SysUtils, + System.Classes, + System.Rtti, + System.TypInfo, + System.Variants, + System.Generics.Collections, + Data.SqlTimSt, + Data.FmtBcd, + Data.DB, + MVCFramework.Commons, + MVCFramework.Serializer.Intf, + MVCFramework.Serializer.Abstract, + MVCFramework.DuckTyping, + MVCFramework.Serializer.Commons; + +type + TMVCTextSerializer = class(TMVCAbstractSerializer, IMVCSerializer) + protected + procedure RaiseNotImplemented; + public + procedure AfterConstruction; override; + { IMVCSerializer } + + procedure RegisterTypeSerializer(const ATypeInfo: PTypeInfo; AInstance: IMVCTypeSerializer); + + function SerializeObject( + const AObject: TObject; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ASerializationAction: TMVCSerializationAction = nil + ): string; overload; + + function SerializeObject( + const AObject: IInterface; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ASerializationAction: TMVCSerializationAction = nil + ): string; overload; + + function SerializeRecord( + const ARecord: Pointer; + const ARecordTypeInfo: PTypeInfo; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ASerializationAction: TMVCSerializationAction = nil + ): string; overload; + + function SerializeCollection( + const AList: TObject; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ASerializationAction: TMVCSerializationAction = nil + ): string; overload; + + function SerializeCollection( + const AList: IInterface; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ASerializationAction: TMVCSerializationAction = nil + ): string; overload; + + function SerializeDataSet( + const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList = []; + const ANameCase: TMVCNameCase = ncAsIs; + const ASerializationAction: TMVCDatasetSerializationAction = nil + ): string; + + function SerializeDataSetRecord( + const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList = []; + const ANameCase: TMVCNameCase = ncAsIs; + const ASerializationAction: TMVCDatasetSerializationAction = nil + ): string; + + procedure DeserializeObject( + const ASerializedObject: string; + const AObject: TObject; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ARootNode: string = '' + ); overload; + + procedure DeserializeObject( + const ASerializedObject: string; + const AObject: IInterface; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil + ); overload; + + procedure DeserializeCollection( + const ASerializedList: string; + const AList: TObject; + const AClazz: TClass; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ARootNode: string = '' + ); overload; + + procedure DeserializeCollection( + const ASerializedList: string; + const AList: IInterface; + const AClazz: TClass; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil + ); overload; + + procedure DeserializeDataSet( + const ASerializedDataSet: string; + const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList = []; + const ANameCase: TMVCNameCase = ncAsIs + ); + + procedure DeserializeDataSetRecord( + const ASerializedDataSetRecord: string; + const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList = []; + const ANameCase: TMVCNameCase = ncAsIs + ); + + function SerializeArrayOfRecord( + var ATValueContainingAnArray: TValue; + const AType: TMVCSerializationType = stDefault; + const AIgnoredAttributes: TMVCIgnoredList = nil; + const ASerializationAction: TMVCSerializationAction = nil): string; + end; + +implementation + +uses + System.NetEncoding, + MVCFramework, + MVCFramework.Logger, + MVCFramework.DataSet.Utils, + MVCFramework.Nullables, System.StrUtils; + +{ TMVCTextSerializer } + +procedure TMVCTextSerializer.AfterConstruction; +begin + inherited AfterConstruction; +end; + +procedure TMVCTextSerializer.DeserializeCollection( + const ASerializedList: string; const AList: IInterface; const AClazz: TClass; + const AType: TMVCSerializationType; + const AIgnoredAttributes: TMVCIgnoredList); +begin + RaiseNotImplemented; +end; + +procedure TMVCTextSerializer.DeserializeCollection( + const ASerializedList: string; const AList: TObject; const AClazz: TClass; + const AType: TMVCSerializationType; const AIgnoredAttributes: TMVCIgnoredList; + const ARootNode: string); +begin + RaiseNotImplemented; +end; + +procedure TMVCTextSerializer.DeserializeDataSet( + const ASerializedDataSet: string; const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList; const ANameCase: TMVCNameCase); +begin + RaiseNotImplemented; +end; + +procedure TMVCTextSerializer.DeserializeDataSetRecord( + const ASerializedDataSetRecord: string; const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList; const ANameCase: TMVCNameCase); +begin + RaiseNotImplemented; +end; + +procedure TMVCTextSerializer.DeserializeObject(const ASerializedObject: string; + const AObject: TObject; const AType: TMVCSerializationType; + const AIgnoredAttributes: TMVCIgnoredList; const ARootNode: string); +begin + RaiseNotImplemented; +end; + +procedure TMVCTextSerializer.DeserializeObject(const ASerializedObject: string; + const AObject: IInterface; const AType: TMVCSerializationType; + const AIgnoredAttributes: TMVCIgnoredList); +begin + RaiseNotImplemented; +end; + +procedure TMVCTextSerializer.RaiseNotImplemented; +begin + raise EMVCException.Create('Not Implemented'); +end; + +procedure TMVCTextSerializer.RegisterTypeSerializer(const ATypeInfo: PTypeInfo; + AInstance: IMVCTypeSerializer); +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeCollection(const AList: TObject; + const AType: TMVCSerializationType; const AIgnoredAttributes: TMVCIgnoredList; + const ASerializationAction: TMVCSerializationAction): string; +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeArrayOfRecord( + var ATValueContainingAnArray: TValue; const AType: TMVCSerializationType; + const AIgnoredAttributes: TMVCIgnoredList; + const ASerializationAction: TMVCSerializationAction): string; +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeCollection(const AList: IInterface; + const AType: TMVCSerializationType; const AIgnoredAttributes: TMVCIgnoredList; + const ASerializationAction: TMVCSerializationAction): string; +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeDataSet(const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList; const ANameCase: TMVCNameCase; + const ASerializationAction: TMVCDatasetSerializationAction): string; +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeDataSetRecord(const ADataSet: TDataSet; + const AIgnoredFields: TMVCIgnoredList; const ANameCase: TMVCNameCase; + const ASerializationAction: TMVCDatasetSerializationAction): string; +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeObject(const AObject: TObject; + const AType: TMVCSerializationType; const AIgnoredAttributes: TMVCIgnoredList; + const ASerializationAction: TMVCSerializationAction): string; + function EmitExceptionClass(const ClazzName, Message: string): string; + begin + Result := Result + IfThen(not ClazzName.IsEmpty, ClazzName + ': ') + Message; + end; + function EmitTitle(const HTTPStatusCode: Word): string; + begin + Result := HTTPStatusCode.ToString + ': ' + HTTP_STATUS.ReasonStringFor(HTTPStatusCode); + end; + + function GetText( + const HTTPStatusCode: Integer; + const Message: String; + const DetailedMessage: String; + const ClazzName: String; + const AppErrorCode: Integer; + const ErrorItems: TArray): String; + var + lErr: String; + begin + Result := + EmitTitle(HTTPStatusCode) + + IfThen(not ClazzName.IsEmpty, sLineBreak + EmitExceptionClass(ClazzName, Message)) + + IfThen(not DetailedMessage.IsEmpty, sLineBreak + DetailedMessage); + if Assigned(FConfig) then + begin + if FConfig[TMVCConfigKey.ExposeServerSignature] = 'true' then + begin + Result := Result + sLineBreak + FConfig[TMVCConfigKey.ServerName]; + end; + end; + if AppErrorCode <> 0 then + begin + Result := sLineBreak + Result + 'Application Error Code: ' + AppErrorCode.ToString + sLineBreak; + end; + if Assigned(ErrorItems) and (Length(ErrorItems) > 0) then + begin + Result := sLineBreak + Result + 'Error Items: '; + for lErr in ErrorItems do + begin + Result := sLineBreak + Result + '- ' + lErr; + end; + end; + end; +var + lMVCException: EMVCException; + lException: Exception; + lErrResponse: TMVCErrorResponse; +begin + if AObject is Exception then + begin + if AObject is EMVCException then + begin + lMVCException := EMVCException(AObject); + Result := GetText( + lMVCException.HTTPStatusCode, + lMVCException.Message, + lMVCException.DetailedMessage, + lMVCException.ClassName, + lMVCException.ApplicationErrorCode, + lMVCException.ErrorItems); + end + else + begin + lException := Exception(AObject); + Result := EmitTitle(500) + sLineBreak + + EmitExceptionClass(lException.ClassName, lException.Message) + sLineBreak; + end; + end else if AObject is TMVCErrorResponse then + begin + lErrResponse := TMVCErrorResponse(AObject); + Result := GetText( + lErrResponse.StatusCode, + lErrResponse.Message, + lErrResponse.DetailedMessage, + lErrResponse.ClassName, + lErrResponse.AppErrorCode, + nil); + end; + + if Result.IsEmpty then + begin + RaiseNotImplemented + end; +end; + +function TMVCTextSerializer.SerializeObject(const AObject: IInterface; + const AType: TMVCSerializationType; + const AIgnoredAttributes: TMVCIgnoredList; + const ASerializationAction: TMVCSerializationAction): string; +begin + RaiseNotImplemented; +end; + +function TMVCTextSerializer.SerializeRecord(const ARecord: Pointer; + const ARecordTypeInfo: PTypeInfo; const AType: TMVCSerializationType; + const AIgnoredAttributes: TMVCIgnoredList; + const ASerializationAction: TMVCSerializationAction): string; +begin + raise Exception.Create('Not implemented'); +end; + +end. diff --git a/sources/MVCFramework.pas b/sources/MVCFramework.pas index 1cbd7206..e9d4fdfc 100644 --- a/sources/MVCFramework.pas +++ b/sources/MVCFramework.pas @@ -397,6 +397,9 @@ type function Accept: string; function BestAccept: string; + function AcceptHTML: boolean; + function CanAcceptMediaType(const MediaType: String): boolean; + function ContentParam(const AName: string): string; function Cookie(const AName: string): string; function Body: string; @@ -643,7 +646,7 @@ type procedure Redirect(const AUrl: string); procedure ResponseStatus(const AStatusCode: Integer; const AReasonString: string = ''); procedure Render201Created(const Location: string = ''); - // Serializer access + // Serializers access function Serializer: IMVCSerializer; overload; function Serializer(const AContentType: string; const ARaiseExcpIfNotExists: Boolean = True) : IMVCSerializer; overload; @@ -1040,6 +1043,7 @@ type procedure OnWebContextDestroy(const WebContextDestroyEvent: TWebContextDestroyEvent); { end - webcontext events} + function Serializer(const AContentType: string; const ARaiseExceptionIfNotExists: Boolean = True): IMVCSerializer; function AddSerializer(const AContentType: string; const ASerializer: IMVCSerializer) : TMVCEngine; function AddMiddleware(const AMiddleware: IMVCMiddleware): TMVCEngine; @@ -1053,15 +1057,13 @@ type function SetViewEngine(const AViewEngineClass: TMVCViewEngineClass): TMVCEngine; function SetExceptionHandler(const AExceptionHandlerProc: TMVCExceptionHandlerProc): TMVCEngine; - procedure HTTP404(const AContext: TWebContext); - procedure HTTP500(const AContext: TWebContext; const AReasonString: string = ''); - procedure SendRawHTTPStatus(const AContext: TWebContext; const HTTPStatusCode: Integer; - const AReasonString: string; const AClassName: string = ''); + procedure SendHTTPStatus(const AContext: TWebContext; const HTTPStatusCode: Integer; + const AReasonString: string = ''; const AClassName: string = ''); property ViewEngineClass: TMVCViewEngineClass read GetViewEngineClass; property WebModule: TWebModule read FWebModule; property Config: TMVCConfig read FConfig; - property Serializers: TDictionary read FSerializers; + //property Serializers: TDictionary read FSerializers; property Middlewares: TList read FMiddlewares; property Controllers: TObjectList read FControllers; property ApplicationSession: TWebApplicationSession read FApplicationSession @@ -1254,8 +1256,9 @@ uses MVCFramework.JSONRPC, MVCFramework.Router, MVCFramework.Rtti.Utils, - MVCFramework.Serializer.HTML, MVCFramework.Serializer.Abstract, - MVCFramework.Utils; + MVCFramework.Serializer.HTML, + MVCFramework.Serializer.Abstract, + MVCFramework.Utils, MVCFramework.Serializer.Text; var gIsShuttingDown: Boolean = False; @@ -1424,6 +1427,11 @@ begin Result := FWebRequest.Accept; end; +function TMVCWebRequest.AcceptHTML: boolean; +begin + Result := CanAcceptMediaType(TMVCMediaType.TEXT_HTML); +end; + function TMVCWebRequest.BestAccept: string; begin if Accept.Contains(',') then @@ -1556,6 +1564,11 @@ begin end; end; +function TMVCWebRequest.CanAcceptMediaType(const MediaType: String): boolean; +begin + Result := Accept.Contains(MediaType); +end; + function TMVCWebRequest.ClientIp: string; var lValue: string; @@ -2788,7 +2801,7 @@ begin begin lContext.Response.StatusCode := http_status.NotFound; lContext.Response.ReasonString := 'Not Found'; - lContext.Response.SetContentStream(TStringStream.Create(), FConfigCache_DefaultContentType); + SendHTTPStatus(lContext, HTTP_STATUS.NotFound); fOnRouterLog(lRouter, rlsRouteNotFound, lContext); end else @@ -2823,7 +2836,7 @@ begin end else begin - SendRawHTTPStatus(lContext, E.HTTPStatusCode, E.Message, E.Classname); + SendHTTPStatus(lContext, E.HTTPStatusCode, E.Message, E.Classname); end; end; end; @@ -2840,7 +2853,7 @@ begin end else begin - SendRawHTTPStatus(lContext, http_status.InternalServerError, + SendHTTPStatus(lContext, http_status.InternalServerError, Format('[%s] %s', [EIO.Classname, EIO.Message]), EIO.Classname); end; end; @@ -2858,7 +2871,7 @@ begin end else begin - SendRawHTTPStatus(lContext, http_status.InternalServerError, + SendHTTPStatus(lContext, http_status.InternalServerError, Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname); end; end; @@ -2881,7 +2894,7 @@ begin end else begin - SendRawHTTPStatus(lContext, http_status.InternalServerError, + SendHTTPStatus(lContext, http_status.InternalServerError, Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname); end; end; @@ -3340,76 +3353,59 @@ begin end; end; -procedure TMVCEngine.HTTP404(const AContext: TWebContext); -begin - AContext.Response.SetStatusCode(http_status.NotFound); - AContext.Response.SetContentType(BuildContentType(TMVCMediaType.TEXT_PLAIN, - AContext.Config[TMVCConfigKey.DefaultContentCharset])); - AContext.Response.SetReasonString('Not Found'); - AContext.Response.SetContent('Not Found' + sLineBreak + FConfigCache_ServerSignature); -end; - -procedure TMVCEngine.HTTP500(const AContext: TWebContext; const AReasonString: string); -begin - AContext.Response.SetStatusCode(http_status.InternalServerError); - AContext.Response.SetContentType(BuildContentType(TMVCMediaType.TEXT_PLAIN, - AContext.Config[TMVCConfigKey.DefaultContentCharset])); - AContext.Response.SetReasonString('Internal server error'); - AContext.Response.SetContent('Internal server error' + sLineBreak + FConfigCache_ServerSignature + - ': ' + AReasonString); -end; - -procedure TMVCEngine.SendRawHTTPStatus(const AContext: TWebContext; const HTTPStatusCode: Integer; +procedure TMVCEngine.SendHTTPStatus(const AContext: TWebContext; const HTTPStatusCode: Integer; const AReasonString: string; const AClassName: string); var lSer: IMVCSerializer; lError: TMVCErrorResponse; lIgnored: TMVCIgnoredList; + lContentType, lItem: String; + lPreferredAcceptContentType: TArray; begin + lPreferredAcceptContentType := [ + AContext.Request.BestAccept, + FConfigCache_DefaultContentType, + TMVCMediaType.TEXT_HTML, + TMVCMediaType.TEXT_PLAIN]; + lError := TMVCErrorResponse.Create; try lError.Classname := AClassName; lError.StatusCode := HTTPStatusCode; - lError.Message := AReasonString; + lError.Message := IfThen(not AReasonString.IsEmpty, AReasonString, HTTP_STATUS.ReasonStringFor(HTTPStatusCode)); - if AContext.Request.ClientPreferHTML then + lIgnored := ['ObjectDictionary']; + if lError.fAppErrorCode = 0 then + lIgnored := lIgnored + ['AppErrorCode']; + if lError.Data = nil then + lIgnored := lIgnored + ['Data']; + if lError.Classname.IsEmpty then + lIgnored := lIgnored + ['ClassName']; + if lError.DetailedMessage.IsEmpty then + lIgnored := lIgnored + ['DetailedMessage']; + if lError.Items.Count = 0 then begin - if not Serializers.TryGetValue(TMVCMediaType.TEXT_HTML, lSer) then - begin - raise EMVCConfigException.Create('Cannot find HTML serializer'); - end; - AContext.Response.SetContent(lSer.SerializeObject(lError)); - AContext.Response.SetContentType(BuildContentType(TMVCMediaType.TEXT_HTML, - AContext.Config[TMVCConfigKey.DefaultContentCharset])); - end - else if AContext.Request.ClientPrefer(AContext.Config[TMVCConfigKey.DefaultContentType]) and - Serializers.TryGetValue(AContext.Config[TMVCConfigKey.DefaultContentType], lSer) then - begin - lIgnored := ['ObjectDictionary']; - if lError.fAppErrorCode = 0 then - lIgnored := lIgnored + ['AppErrorCode']; - if lError.Data = nil then - lIgnored := lIgnored + ['Data']; - if lError.DetailedMessage.IsEmpty then - lIgnored := lIgnored + ['DetailedMessage']; - if lError.Items.Count = 0 then - begin - lIgnored := lIgnored + ['Items']; - end; - - AContext.Response.SetContent(lSer.SerializeObject(lError, stDefault, lIgnored)); - AContext.Response.SetContentType - (BuildContentType(AContext.Config[TMVCConfigKey.DefaultContentType], - AContext.Config[TMVCConfigKey.DefaultContentCharset])); - end - else - begin - AContext.Response.SetContentType(BuildContentType(TMVCMediaType.TEXT_PLAIN, - AContext.Config[TMVCConfigKey.DefaultContentCharset])); - AContext.Response.SetContent(FConfigCache_ServerSignature + sLineBreak + 'HTTP ' + - HTTPStatusCode.ToString + ': ' + AReasonString); + lIgnored := lIgnored + ['Items']; end; + + for lItem in lPreferredAcceptContentType do + begin + lSer := Serializer(lItem, False); + if lSer <> nil then + begin + lContentType := lItem; + Break; + end; + end; + if lSer = nil then + begin + raise EMVCConfigException.Create('Cannot find a proper serializer among ' + string.Join(',', lPreferredAcceptContentType)); + end; + + AContext.Response.SetContentType( + BuildContentType(lItem, AContext.Config[TMVCConfigKey.DefaultContentCharset])); + AContext.Response.SetContent(lSer.SerializeObject(lError, stDefault, lIgnored)); AContext.Response.SetStatusCode(HTTPStatusCode); - AContext.Response.SetReasonString(AReasonString); + AContext.Response.SetReasonString(lError.Message); finally lError.Free; end; @@ -3515,6 +3511,13 @@ begin begin FSerializers.Add(lDefaultSerializerContentType, TMVCHTMLSerializer.Create(Config)); end; + + // this is used only for TMVCError (dt 2024-02-22) + lDefaultSerializerContentType := BuildContentType(TMVCMediaType.TEXT_PLAIN, ''); + if not FSerializers.ContainsKey(lDefaultSerializerContentType) then + begin + FSerializers.Add(lDefaultSerializerContentType, TMVCTextSerializer.Create(Config)); + end; end; procedure TMVCEngine.ResponseErrorPage(const AException: Exception; const ARequest: TWebRequest; @@ -3576,6 +3579,30 @@ begin Result := ASessionId; end; +function TMVCEngine.Serializer(const AContentType: string; const ARaiseExceptionIfNotExists: Boolean): IMVCSerializer; +var + lContentMediaType: string; + lContentCharSet: string; +begin + SplitContentMediaTypeAndCharset(AContentType.ToLower, lContentMediaType, lContentCharSet); + if FSerializers.ContainsKey(lContentMediaType) then + begin + Result := FSerializers.Items[lContentMediaType]; + end + else + begin + if ARaiseExceptionIfNotExists then + begin + raise EMVCException.CreateFmt('The serializer for %s could not be found. [HINT] Register on TMVCEngine instance using "AddSerializer" method.', + [lContentMediaType]); + end + else + begin + Result := nil; + end; + end; +end; + function TMVCEngine.SetExceptionHandler(const AExceptionHandlerProc: TMVCExceptionHandlerProc) : TMVCEngine; begin @@ -4163,26 +4190,8 @@ end; function TMVCRenderer.Serializer( const AContentType: string; const ARaiseExceptionIfNotExists: Boolean): IMVCSerializer; -var lContentMediaType: string; - lContentCharSet: string; begin - SplitContentMediaTypeAndCharset(AContentType.ToLower, lContentMediaType, lContentCharSet); - if Engine.Serializers.ContainsKey(lContentMediaType) then - begin - Result := Engine.Serializers.Items[lContentMediaType]; - end - else - begin - if ARaiseExceptionIfNotExists then - begin - raise EMVCException.CreateFmt('The serializer for %s could not be found. [HINT] Register on TMVCEngine instance using "AddSerializer" method.', - [lContentMediaType]); - end - else - begin - Result := nil; - end; - end; + Result := Engine.Serializer(AContentType, ARaiseExceptionIfNotExists); end; function TMVCController.SessionAs: T; @@ -4528,13 +4537,11 @@ begin end; end; - if Serializer(GetContentType, False) = nil then - begin - if Serializer(FContext.Request.BestAccept, False) <> nil then - GetContext.Response.ContentType := FContext.Request.BestAccept - else - GetContext.Response.ContentType := GetConfig[TMVCConfigKey.DefaultContentType]; - end; + if Serializer(FContext.Request.BestAccept, False) <> nil then + GetContext.Response.ContentType := FContext.Request.BestAccept + else + GetContext.Response.ContentType := Engine.FConfigCache_DefaultContentType; + Render(R, False, nil, R.GetIgnoredList); finally R.Free; diff --git a/unittests/general/Several/LiveServerTestU.pas b/unittests/general/Several/LiveServerTestU.pas index 06e50401..a5e12a78 100644 --- a/unittests/general/Several/LiveServerTestU.pas +++ b/unittests/general/Several/LiveServerTestU.pas @@ -32,7 +32,7 @@ uses MVCFramework.RESTClient, MVCFramework.JSONRPC.Client, System.DateUtils, - System.Hash, System.Rtti; + System.Hash, System.Rtti, MVCFramework.Commons; type @@ -237,6 +237,23 @@ type [Category('renders,exceptions')] procedure TestEMVCException4; + [Test] + [Category('renders,exceptions')] + [TestCase('404'+'invalid_accept', '404,/invalidurl,invalid_accept,' + TMVCMediaType.APPLICATION_JSON)] + [TestCase('404'+TMVCMediaType.TEXT_HTML, '404,/invalidurl,' + TMVCMediaType.TEXT_HTML + ',' + TMVCMediaType.TEXT_HTML)] + [TestCase('404'+TMVCMediaType.TEXT_PLAIN, '404,/invalidurl,' + TMVCMediaType.TEXT_PLAIN + ',' + TMVCMediaType.TEXT_PLAIN)] + [TestCase('404'+TMVCMediaType.APPLICATION_JSON, '404,/invalidurl,' + TMVCMediaType.APPLICATION_JSON + ',' + TMVCMediaType.APPLICATION_JSON)] + [TestCase('500'+'invalid_accept', '500,/exception/emvcexception1,invalid_accept,' + TMVCMediaType.APPLICATION_JSON)] + [TestCase('500'+TMVCMediaType.TEXT_HTML, '500,/exception/emvcexception1,' + TMVCMediaType.TEXT_HTML + ',' + TMVCMediaType.TEXT_HTML)] + [TestCase('500'+TMVCMediaType.TEXT_PLAIN, '500,/exception/emvcexception1,' + TMVCMediaType.TEXT_PLAIN + ',' + TMVCMediaType.TEXT_PLAIN)] + [TestCase('500'+TMVCMediaType.APPLICATION_JSON, '500,/exception/emvcexception1,' + TMVCMediaType.APPLICATION_JSON + ',' + TMVCMediaType.APPLICATION_JSON)] + + procedure TestResponseContentTypes( + const ExpectedStatus: Integer; + const URL: String; + const RequestAccept: String; + const ResponseContentType: String); + // test nullables [Test] procedure TestDeserializeNullablesWithValue; @@ -449,7 +466,6 @@ uses MVCFramework.Serializer.Defaults, JsonDataObjects, MVCFramework.Serializer.JsonDataObjects, - MVCFramework.Commons, System.SyncObjs, System.Generics.Collections, System.SysUtils, @@ -1878,6 +1894,22 @@ begin Assert.areEqual(HTTP_STATUS.OK, lRes.StatusCode); end; +procedure TServerTest.TestResponseContentTypes( + const ExpectedStatus: Integer; + const URL: String; + const RequestAccept: String; + const ResponseContentType: String); +var + res: IMVCRESTResponse; +begin + res := RESTClient + .ClearHeaders + .Accept(RequestAccept) + .Get(URL); + Assert.AreEqual(ExpectedStatus, res.StatusCode); + Assert.StartsWith(ResponseContentType, res.ContentType); +end; + procedure TServerTest.TestObjectDict; var lRes: IMVCRESTResponse; diff --git a/unittests/general/TestServer/TestServer.dpr b/unittests/general/TestServer/TestServer.dpr index 388a4eaf..1bd64a31 100644 --- a/unittests/general/TestServer/TestServer.dpr +++ b/unittests/general/TestServer/TestServer.dpr @@ -28,7 +28,10 @@ uses EntitiesProcessors in '..\Several\EntitiesProcessors.pas', MVCFramework.JSONRPC.Client in '..\..\..\sources\MVCFramework.JSONRPC.Client.pas', MVCFramework.JSONRPC in '..\..\..\sources\MVCFramework.JSONRPC.pas', - MVCFramework.Serializer.Commons; + MVCFramework.Serializer.Commons, + MVCFramework in '..\..\..\sources\MVCFramework.pas', + MVCFramework.Serializer.Text in '..\..\..\sources\MVCFramework.Serializer.Text.pas', + MVCFramework.Serializer.HTML in '..\..\..\sources\MVCFramework.Serializer.HTML.pas'; {$R *.res} diff --git a/unittests/general/TestServer/TestServer.dproj b/unittests/general/TestServer/TestServer.dproj index 29ac0153..9e124999 100644 --- a/unittests/general/TestServer/TestServer.dproj +++ b/unittests/general/TestServer/TestServer.dproj @@ -133,6 +133,9 @@ + + + Base diff --git a/unittests/general/TestServer/WebModuleUnit.pas b/unittests/general/TestServer/WebModuleUnit.pas index 1259f483..aedfd8dc 100644 --- a/unittests/general/TestServer/WebModuleUnit.pas +++ b/unittests/general/TestServer/WebModuleUnit.pas @@ -133,7 +133,7 @@ begin .AddMiddleware(TMVCCompressionMiddleware.Create); {$IFDEF MSWINDOWS} MVCEngine.SetViewEngine(TMVCMustacheViewEngine); - RegisterOptionalCustomTypesSerializers(MVCEngine.Serializers[TMVCMediaType.APPLICATION_JSON]); + RegisterOptionalCustomTypesSerializers(MVCEngine.Serializer(TMVCMediaType.APPLICATION_JSON)); {$ENDIF} end;