diff --git a/README.md b/README.md index 671a2adf..1794265a 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Render(lPerson, False, - New! `TMVCActiveRecord.DeleteRQL` deletes records using an `RQL` expression as `where` clause. - New! Microsoft SQLServer Support in `MVCActiveRecord` and RQL (*thanks to one of the biggest Delphi based company in Italy which heavily uses DMVCFramework* and *[DMSContainer](http://www.bittimeprofessionals.it/prodotti/dmscontainer)*) - New! SQLite support in `MVCActiveRecord` and RQL, so that `MVCActiveRecord` can be used also for Delphi mobile projects! +- Default JSON Serializer can verbatim pass properties with type `JsonDataObjects.TJSONObject` without using `string` as carrier of JSON - Improved! `ActiveRecordShowCase` sample is much better now. - Improved! In case of unhandled exception `TMVCEngine` is compliant with the default response content-type (usually it did would reply using `text/plain`). - **Breaking Change!** In `MVCActiveRecord` attribute `MVCPrimaryKey` has been removed and merged with `MVCTableField`, so now `TMVCActiveRecordFieldOption` is a set of `foPrimaryKey`, `foAutoGenerated`, `foTransient` (check `activerecord_showcase.dproj` sample). diff --git a/samples/commons/BusinessObjectsU.pas b/samples/commons/BusinessObjectsU.pas index b62507c2..d5906552 100644 --- a/samples/commons/BusinessObjectsU.pas +++ b/samples/commons/BusinessObjectsU.pas @@ -28,7 +28,7 @@ interface uses MVCFramework.Serializer.Commons, - Generics.Collections, Vcl.Graphics; + Generics.Collections, Vcl.Graphics, JsonDataObjects; type @@ -72,6 +72,19 @@ type property DOB: TDate read GetDOB write SetDOB; end; + TObjectWithJSONObject = class + private + fJSONObject: TJSONObject; + FStringProp: String; + procedure SetStringProp(const Value: String); + public + constructor Create; + destructor Destroy; override; + property StringProp: String read FStringProp write SetStringProp; + property JSONObject: TJSONObject read fJSONObject; + end; + + [MVCNameCase(ncCamelCase)] TInterfacedPerson = class(TInterfacedObject, IPerson) private @@ -416,6 +429,25 @@ begin fName := Value; end; +{ TObjectWithJSONObject } + +constructor TObjectWithJSONObject.Create; +begin + inherited; + fJSONObject := TJsonObject.Create; +end; + +destructor TObjectWithJSONObject.Destroy; +begin + fJSONObject.Free; + inherited; +end; + +procedure TObjectWithJSONObject.SetStringProp(const Value: String); +begin + FStringProp := Value; +end; + initialization Randomize; diff --git a/samples/jsonrpc_with_published_objects/MainClientFormU.dfm b/samples/jsonrpc_with_published_objects/MainClientFormU.dfm index a3c4a459..ad132448 100644 --- a/samples/jsonrpc_with_published_objects/MainClientFormU.dfm +++ b/samples/jsonrpc_with_published_objects/MainClientFormU.dfm @@ -22,10 +22,8 @@ object MainForm: TMainForm ActivePage = TabSheet1 Align = alClient TabOrder = 0 - ExplicitWidth = 846 object TabSheet1: TTabSheet Caption = 'Invoking Plain PODO' - ExplicitWidth = 838 object GroupBox1: TGroupBox Left = 3 Top = 22 @@ -182,18 +180,18 @@ object MainForm: TMainForm TabOrder = 13 end object btnDates: TButton - Left = 635 + Left = 716 Top = 30 - Width = 165 + Width = 84 Height = 25 Caption = 'PlayWithDates' TabOrder = 14 OnClick = btnDatesClick end object btnFloatsTests: TButton - Left = 545 + Left = 626 Top = 30 - Width = 75 + Width = 84 Height = 25 Caption = 'Floats' TabOrder = 15 @@ -337,7 +335,6 @@ object MainForm: TMainForm object TabSheet2: TTabSheet Caption = 'Invoking DataModule Methods' ImageIndex = 1 - ExplicitWidth = 838 object GroupBox5: TGroupBox Left = 11 Top = 18 @@ -378,6 +375,15 @@ object MainForm: TMainForm end end end + object btnWithJSON: TButton + Left = 552 + Top = 76 + Width = 75 + Height = 25 + Caption = 'JSON Prop' + TabOrder = 1 + OnClick = btnWithJSONClick + end object DataSource1: TDataSource DataSet = FDMemTable1 Left = 767 diff --git a/samples/jsonrpc_with_published_objects/MainClientFormU.pas b/samples/jsonrpc_with_published_objects/MainClientFormU.pas index 28008152..05b02139 100644 --- a/samples/jsonrpc_with_published_objects/MainClientFormU.pas +++ b/samples/jsonrpc_with_published_objects/MainClientFormU.pas @@ -76,6 +76,7 @@ type CheckBox1: TCheckBox; btnDates: TButton; btnFloatsTests: TButton; + btnWithJSON: TButton; procedure btnSubstractClick(Sender: TObject); procedure btnReverseStringClick(Sender: TObject); procedure edtGetCustomersClick(Sender: TObject); @@ -90,6 +91,7 @@ type procedure btnSearchClick(Sender: TObject); procedure btnDatesClick(Sender: TObject); procedure btnFloatsTestsClick(Sender: TObject); + procedure btnWithJSONClick(Sender: TObject); private FExecutor: IMVCJSONRPCExecutor; FExecutor2: IMVCJSONRPCExecutor; @@ -307,6 +309,25 @@ begin edtResult.Text := lResp.Result.AsInteger.ToString; end; +procedure TMainForm.btnWithJSONClick(Sender: TObject); +var + lPerson: TJsonObject; + lReq: IJSONRPCRequest; + lResp: IJSONRPCResponse; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'SaveObjectWithJSON'; + lReq.RequestID := 1234; + lPerson := TJsonObject.Create; + lReq.Params.Add(lPerson, pdTJDOJsonObject); + lPerson.S['StringProp'] := 'Hello World'; + lPerson.O['JSONObject'] := TJsonObject.Parse('{"name":"Daniele"}') as TJsonObject; + lResp := FExecutor.ExecuteRequest(lReq); + + lPerson := lResp.Result.AsObject as TJsonObject; + ShowMessage(lPerson.ToJSON(False)); +end; + procedure TMainForm.edtGetCustomersClick(Sender: TObject); var lReq: IJSONRPCRequest; diff --git a/samples/jsonrpc_with_published_objects/MyObjectU.pas b/samples/jsonrpc_with_published_objects/MyObjectU.pas index 1e90b33c..5fc24433 100644 --- a/samples/jsonrpc_with_published_objects/MyObjectU.pas +++ b/samples/jsonrpc_with_published_objects/MyObjectU.pas @@ -64,19 +64,44 @@ type function SavePerson(const aPerson: TJsonObject): Integer; function FloatsTest(const aDouble: Double; const aExtended: Extended): Extended; procedure DoSomething; + function SaveObjectWithJSON(const WithJSON: TJsonObject): TJsonObject; // invalid parameters modifiers procedure InvalidMethod1(var MyVarParam: Integer); procedure InvalidMethod2(out MyOutParam: Integer); end; + TUtils = class sealed + class function JSONObjectAs(const JSON: TJsonObject): T; + end; + implementation uses System.SysUtils, MVCFramework.Logger, System.StrUtils, - System.DateUtils; + System.DateUtils, MVCFramework.Serializer.JsonDataObjects; + +class function TUtils.JSONObjectAs(const JSON: TJsonObject): T; +var + lObj: TObject; + lSerializer: TMVCJsonDataObjectsSerializer; +begin + lObj := T.Create; + try + lSerializer := TMVCJsonDataObjectsSerializer.Create; + try + lSerializer.JsonObjectToObject(JSON, lObj, TMVCSerializationType.stProperties, []); + finally + lSerializer.Free; + end; + except + lObj.Free; + raise; + end; + Result := T(lObj); +end; { TMyDerivedController } @@ -85,8 +110,7 @@ begin end; -function TMyObject.FloatsTest(const aDouble: Double; - const aExtended: Extended): Extended; +function TMyObject.FloatsTest(const aDouble: Double; const aExtended: Extended): Extended; begin Result := aDouble + aExtended; end; @@ -224,6 +248,19 @@ begin Result := Result.ToUpper; end; +function TMyObject.SaveObjectWithJSON(const WithJSON: TJsonObject): TJsonObject; +var + lObj: TObjectWithJSONObject; +begin + lObj := TUtils.JSONObjectAs(WithJSON); + try + LogD(lObj); + Result := WithJSON.Clone as TJsonObject; + finally + lObj.Free; + end; +end; + function TMyObject.SavePerson(const aPerson: TJsonObject): Integer; // var // lPerson: TPerson; diff --git a/samples/renders/RenderSampleControllerU.pas b/samples/renders/RenderSampleControllerU.pas index 803e112c..d367bde6 100644 --- a/samples/renders/RenderSampleControllerU.pas +++ b/samples/renders/RenderSampleControllerU.pas @@ -167,6 +167,11 @@ type [MVCPath('/simplearray')] procedure GetSimpleArrays; + [MVCHTTPMethod([httpGET])] + [MVCPath('/objectwithjson')] + procedure GetObjectWithJSONProperty; + + end; implementation @@ -524,6 +529,17 @@ begin Render(GetPeopleList, False); end; +procedure TRenderSampleController.GetObjectWithJSONProperty; +var + lObj: TObjectWithJSONObject; +begin + lObj := TObjectWithJSONObject.Create; + lObj.StringProp := 'Daniele Teti'; + lObj.JSONObject.S['stringprop'] := 'String Prop'; + lObj.JSONObject.O['innerobj'].S['innerstringprop'] := 'Inner String Prop'; + Render(lObj); +end; + procedure TRenderSampleController.GetPerson_AsHTML; begin ResponseStream.Append('
    ').Append('
  • FirstName: Daniele
  • ').Append('
  • LastName: Teti') diff --git a/samples/renders/renders.dpr b/samples/renders/renders.dpr index c2e5de2b..98407ec0 100644 --- a/samples/renders/renders.dpr +++ b/samples/renders/renders.dpr @@ -36,17 +36,17 @@ uses Web.WebReq, Web.WebBroker, MVCFramework.Console, - WebModuleU in 'WebModuleU.pas' {WebModule1: TWebModule} , + WebModuleU in 'WebModuleU.pas' {WebModule1: TWebModule}, RenderSampleControllerU in 'RenderSampleControllerU.pas', BusinessObjectsU in '..\commons\BusinessObjectsU.pas', - MyDataModuleU in 'MyDataModuleU.pas' {MyDataModule: TDataModule} , + MyDataModuleU in 'MyDataModuleU.pas' {MyDataModule: TDataModule}, CustomTypesU in 'CustomTypesU.pas', CustomTypesSerializersU in 'CustomTypesSerializersU.pas', InMemoryDataU in 'InMemoryDataU.pas', MVCFramework.DataSet.Utils in '..\..\sources\MVCFramework.DataSet.Utils.pas', RandomUtilsU in '..\commons\RandomUtilsU.pas', - MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes - in '..\..\sources\MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes.pas'; + MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes in '..\..\sources\MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes.pas', + MVCFramework.Serializer.JsonDataObjects.CustomTypes in '..\..\sources\MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas'; {$R *.res} diff --git a/samples/renders/renders.dproj b/samples/renders/renders.dproj index 3c025f66..111b137c 100644 --- a/samples/renders/renders.dproj +++ b/samples/renders/renders.dproj @@ -148,6 +148,7 @@ + @@ -322,6 +323,12 @@ true + + + .\ + true + + .\ @@ -333,37 +340,31 @@ true + + + .\ + true + + .\ true - - - .\ - true - - - - - .\ - true - - renders.exe true - - + + .\ true - + .\ true diff --git a/samples/swaggerui/sources/WebModuleU.pas b/samples/swaggerui/sources/WebModuleU.pas index f2159ed9..efcd6b24 100644 --- a/samples/swaggerui/sources/WebModuleU.pas +++ b/samples/swaggerui/sources/WebModuleU.pas @@ -54,8 +54,8 @@ begin // Max request size in bytes Config[TMVCConfigKey.MaxRequestSize] := IntToStr(TMVCConstants.DEFAULT_MAX_REQUEST_SIZE); end); - FMVC.AddController(TMyController); - // To enable compression (deflate, gzip) just add this middleware as the last one +// FMVC.AddController(TMyController); + // To enable compression (deflate, gzip) just add this middleware as the last one FMVC.AddMiddleware(TMVCCompressionMiddleware.Create); end; diff --git a/samples/swaggerui/sources/swaggeruiwebserver.dpr b/samples/swaggerui/sources/swaggeruiwebserver.dpr index c2352567..88f394f3 100644 --- a/samples/swaggerui/sources/swaggeruiwebserver.dpr +++ b/samples/swaggerui/sources/swaggeruiwebserver.dpr @@ -3,6 +3,10 @@ program swaggeruiwebserver; {$APPTYPE CONSOLE} uses +{$IFDEF MSWINDOWS} + WinAPI.ShellAPI, + WinAPI.Windows, +{$ENDIF} System.SysUtils, MVCFramework.Logger, MVCFramework.Commons, diff --git a/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas b/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas index 6e0d7615..15d9e2b7 100644 --- a/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas +++ b/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas @@ -43,8 +43,6 @@ type TMVCStreamSerializerJsonDataObject = class(TInterfacedObject, IMVCTypeSerializer) protected - // procedure Serialize(const AElementValue: TValue; var ASerializerObject: TObject; - // const AAttributes: TArray); procedure SerializeAttribute(const AElementValue: TValue; const APropertyName: string; const ASerializerObject: TObject; const AAttributes: TArray); procedure SerializeRoot(const AObject: TObject; out ASerializerObject: TObject; diff --git a/sources/MVCFramework.Serializer.JsonDataObjects.pas b/sources/MVCFramework.Serializer.JsonDataObjects.pas index 122de240..d025e590 100644 --- a/sources/MVCFramework.Serializer.JsonDataObjects.pas +++ b/sources/MVCFramework.Serializer.JsonDataObjects.pas @@ -186,7 +186,6 @@ begin fStringDictionarySerializer := TMVCStringDictionarySerializer.Create; GetTypeSerializers.Add(TypeInfo(TMVCStringDictionary), TMVCStringDictionarySerializer.Create); GetTypeSerializers.Add(TypeInfo(TGUID), TMVCGUIDSerializer.Create); - end; procedure TMVCJsonDataObjectsSerializer.AttributeToJsonDataValue(const AJsonObject: TJDOJsonObject; const AName: string; @@ -315,6 +314,10 @@ begin ChildJsonArray := AJsonObject.A[AName]; DataSetToJsonArray(TDataSet(ChildObject), ChildJsonArray, TMVCNameCase.ncLowerCase, []); end + else if ChildObject is TJsonObject then + begin + AJsonObject.O[AName] := TJsonObject(ChildObject).Clone as TJsonObject; + end else begin ChildList := TDuckTypedList.Wrap(ChildObject); @@ -1092,6 +1095,16 @@ var AttributeValue: TValue; lKeyName: string; begin + if AObject is TJsonObject then + begin + if not Assigned(AObject) then + begin + raise EMVCDeserializationException.Create(AObject.ClassName + ' is not assigned'); + end; + TJsonObject(AObject).Assign(AJsonObject); + Exit; + end; + ObjType := GetRttiContext.GetType(AObject.ClassType); case AType of stDefault, stProperties: diff --git a/unittests/general/Several/BOs.pas b/unittests/general/Several/BOs.pas index 7ebc9d43..e10b32db 100644 --- a/unittests/general/Several/BOs.pas +++ b/unittests/general/Several/BOs.pas @@ -28,7 +28,7 @@ interface uses system.TimeSpan, system.SysUtils, generics.collections, system.Classes, - system.Rtti, MVCFramework.Serializer.Commons; + system.Rtti, MVCFramework.Serializer.Commons, JsonDataObjects; type TMyObject = class @@ -47,6 +47,7 @@ type FPropTimeStamp: TTimeStamp; FPropTime: TTime; FPropCurrency: Currency; + fPropJSONObject: TJSONObject; procedure SetPropAnsiString(const Value: AnsiString); procedure SetPropString(const Value: string); procedure SetPropInt64(const Value: Int64); @@ -62,6 +63,8 @@ type procedure SetPropTime(const Value: TTime); procedure SetPropCurrency(const Value: Currency); public + constructor Create; + destructor Destroy; override; function Equals(Obj: TMyObject): boolean; reintroduce; property PropString: string read FPropString write SetPropString; property PropAnsiString: AnsiString read FPropAnsiString @@ -79,6 +82,7 @@ type property PropTimeStamp: TTimeStamp read FPropTimeStamp write SetPropTimeStamp; property PropCurrency: Currency read FPropCurrency write SetPropCurrency; + property PropJSONObject: TJSONObject read fPropJSONObject; end; TMyChildObject = class @@ -345,6 +349,25 @@ begin Result.PropTime := EncodeTime(10, 20, 30, 40); Result.PropDateTime := Result.PropDate + Result.PropTime; Result.PropTimeStamp := DateTimeToTimeStamp(Result.PropDateTime + 1); + Result.PropJSONObject.S['stringprop1'] := 'This is a string prop'; + Result.PropJSONObject.I['intprop1'] := 1234; + Result.PropJSONObject.A['arrprop'].Add(1234); + Result.PropJSONObject.A['arrprop'].Add('Hello World'); + Result.PropJSONObject.O['objprop'].S['innerprop1'] := 'value1'; + Result.PropJSONObject.O['objprop'].S['innerprop2'] := 'value2'; + Result.PropJSONObject.O['objprop'].S['innerprop3'] := 'value3'; +end; + +constructor TMyObject.Create; +begin + inherited; + fPropJSONObject := TJsonObject.Create; +end; + +destructor TMyObject.Destroy; +begin + fPropJSONObject.Free; + inherited; end; function TMyObject.Equals(Obj: TMyObject): boolean; @@ -366,6 +389,7 @@ begin Obj.PropDateTime) = 0); Result := Result and (Self.PropTimeStamp.Date = Obj.PropTimeStamp.Date) and (Self.PropTimeStamp.Time = Obj.PropTimeStamp.Time); + Result := Result and (Self.fPropJSONObject.ToJSON() = Obj.PropJSONObject.ToJSON()); end; procedure TMyObject.SetPropAnsiString(const Value: AnsiString); diff --git a/unittests/general/Several/Serializers.JsonDataObjectsTestU.pas b/unittests/general/Several/Serializers.JsonDataObjectsTestU.pas index ed259cf2..88ffc6e0 100644 --- a/unittests/general/Several/Serializers.JsonDataObjectsTestU.pas +++ b/unittests/general/Several/Serializers.JsonDataObjectsTestU.pas @@ -58,6 +58,10 @@ type { serialize declarations } [Test] + procedure TestSerializeAllTypes; + [Test] + procedure TestSerializeAllTypesInList; + [Test] procedure TestSerializeEntity; [Test] procedure TestSerializeNil; @@ -160,7 +164,7 @@ implementation uses MVCFramework.Serializer.JsonDataObjects.CustomTypes, - MVCFramework.Commons, System.TypInfo; + MVCFramework.Commons, System.TypInfo, BOs; const LINE_BREAK = #$A; @@ -661,6 +665,59 @@ begin end; end; +procedure TMVCTestSerializerJsonDataObjects.TestSerializeAllTypes; +var + lObj1, lObj2: TMyObject; + lSer: string; +begin + lObj1 := GetMyObject; + try + lSer := FSerializer.SerializeObject(lObj1); + lObj2 := TMyObject.Create; + try + FSerializer.DeserializeObject(lSer, lObj2); + Assert.IsTrue(lObj1.Equals(lObj2)); + finally + lObj2.Free; + end; + finally + lObj1.Free; + end; +end; + +procedure TMVCTestSerializerJsonDataObjects.TestSerializeAllTypesInList; +var + lList1, lList2: TObjectList; + lSer: string; + I: Integer; + lObj: TMyObject; +begin + lList1 := TObjectList.Create; + try + for I := 0 to 9 do + begin + lObj :=GetMyObject; + lObj.PropJSONObject.I['value'] := I; + lList1.Add(lObj); + end; + + lSer := FSerializer.SerializeCollection(lList1); + + lList2 := TObjectList.Create; + try + FSerializer.DeserializeCollection(lSer, lList2, TMyObject); + for I := 0 to 9 do + begin + Assert.IsTrue(lList1[I].Equals(lList2[I])); + end; + finally + lList2.Free; + end; + finally + lList1.Free; + end; +end; + procedure TMVCTestSerializerJsonDataObjects.TestSerializeCollection; const JSON =