From ddc5f4d97dcd322e254182bd7c0144ad30dced8f Mon Sep 17 00:00:00 2001 From: Daniele Teti Date: Sat, 2 May 2020 16:39:32 +0200 Subject: [PATCH] JSON-RPC named parameters support --- README.md | 144 ++++--- docs/favicons/favicon_io.zip | Bin 0 -> 56401 bytes .../MainClientFormU.dfm | 45 ++- .../MainClientFormU.pas | 39 +- .../MyObjectU.pas | 18 +- sources/MVCFramework.Commons.pas | 2 +- sources/MVCFramework.JSONRPC.pas | 364 ++++++++++++++---- .../MVCFramework.Middleware.StaticFiles.pas | 12 +- ...Serializer.JsonDataObjects.CustomTypes.pas | 5 + unittests/general/Several/JSONRPCTestsU.pas | 42 ++ unittests/general/Several/LiveServerTestU.pas | 178 ++++++++- unittests/general/TestServer/TestServer.dproj | 26 -- .../TestServerControllerJSONRPCU.pas | 24 +- .../general/TestServer/WebModuleUnit.pas | 4 +- unittests/general/TestServer/www/static.html | 1 + 15 files changed, 691 insertions(+), 213 deletions(-) create mode 100644 docs/favicons/favicon_io.zip create mode 100644 unittests/general/TestServer/www/static.html diff --git a/README.md b/README.md index 63fe21df..994f13da 100644 --- a/README.md +++ b/README.md @@ -278,17 +278,16 @@ end; >ObjectDict is the suggested way to renders data. However, the other ones are still there and works as usual. - New! Added SQLGenerator and RQL compiler for PostgreSQL, SQLite and MSSQLServer (in addition to MySQL, MariaDB, Firebird and Interbase) -- New! *MVCNameAs* attribute got the param `Fixed` (default: false). If Fixed is true, then the name is not processed by the `MVCNameCase` attribute assigned to the owner type. +- New! *MVCNameAs* attribute got the param `Fixed` (default: false). If `Fixed` is true, then the name is not processed by the `MVCNameCase` attribute assigned to the owner type. - New! Added support for interfaces serialization - now it is possible to serialize Spring4D collections (thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) - New! Added support for Spring4D Nullable Types - check (thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) - New! Added `OnRouterLog` event to log custom information for each request (thanks to [Andrea Ciotti](https://github.com/andreaciotti) for the first implementation and its PR) - New! Optionally load system controllers (those who provide `/describeserver.info`, `/describeplatform.info` and `/serverconfig.info` system actions) setting `Config[TMVCConfigKey.LoadSystemControllers] := 'false';` in the configuration block. -- Added `TMVCJSONRPCExecutor.ConfigHTTPClient` to fully customize the inner `THTTPClient` (e.g. `ConnectionTimeout`, `ResponseTimeout` and so on) - Improved! Now the router consider `Accept:*/*` compatible for every `MVCProduces` values - Improved! Greatly improved support for [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) in renders. Check `TRenderSampleController.GetPeople_AsObjectList_HATEOS` and all the others actions end with `HATEOS` in `renders.dproj` sample) ```delphi -//Now is really easy to add "_links" property automatically for each collection element while rendering +//Now is really easy to add "links" property automatically for each collection element while rendering Render(People, True, procedure(const APerson: TPerson; const Links: IMVCLinks) begin @@ -369,24 +368,6 @@ end; - Improved! Exceptions rendering while using MIME types different to `application/json`. -- Improved! JSONRPC Automatic Object Publishing can not invoke inherited methods if not explicitly defined with `MVCInheritable` attribute. - -- New! JSONRPC Hooks for published objects - - ```delphi - //Called before as soon as the HTTP arrives - procedure TMyPublishedObject.OnBeforeRouting(const JSON: TJDOJsonObject); - - //Called before the invoked method - procedure TMyPublishedObject.OnBeforeCall(const JSONRequest: TJDOJsonObject); - - //Called just before to send response to the client - procedure TMyPublishedObject.OnBeforeSendResponse(const JSONResponse: TJDOJsonObject); - - ``` - - - - SSL Server support for `TMVCListener` (Thanks to [Sven Harazim](https://github.com/landrix)) - Improved! Datasets serialization speed improvement. In some case the performance [improves of 2 order of magnitude](https://github.com/danieleteti/delphimvcframework/issues/205#issuecomment-479513158). (Thanks to https://github.com/pedrooliveira01) @@ -397,8 +378,6 @@ end; - New! `TMVCActiveRecord` can handle non autogenerated primary key. -- New! Calling `/describe` returns the methods list available for that endpoint. - - New! Experimental (alpha stage) support for Android servers! - New! Added support for `X-HTTP-Method-Override` to work behind corporate firewalls. @@ -479,55 +458,69 @@ end; - New! The **MVCAREntitiesGenerator** can optionally register all the generated entities also in the `ActiveRecordMappingRegistry` (Thanks to [Fabrizio Bitti](https://twitter.com/fabriziobitti) from [bit Time Software](http://www.bittime.it)) -- Fixed! [issue38](https://github.com/danieleteti/delphimvcframework/issues/38) +- **JSON-RPC Improvements** -- Fixed! [issue184](https://github.com/danieleteti/delphimvcframework/issues/184) + - New! Added `TMVCJSONRPCExecutor.ConfigHTTPClient` to fully customize the inner `THTTPClient` (e.g. `ConnectionTimeout`, `ResponseTimeout` and so on) -- Fixed! [issue278](https://github.com/danieleteti/delphimvcframework/issues/278) + - Improved! JSONRPC Automatic Object Publishing can not invoke inherited methods if not explicitly defined with `MVCInheritable` attribute. -- Fixed! [issue164](https://github.com/danieleteti/delphimvcframework/issues/164) + - New! Calling `/describe` returns the methods list available for that endpoint. -- Fixed! [issue182](https://github.com/danieleteti/delphimvcframework/issues/182) + - New! Full support for named parameters in JSON-RPC call (server and client) -- Fixed! [issue232](https://github.com/danieleteti/delphimvcframework/issues/232) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) + - Positional parameters example -- Fixed! [issue289](https://github.com/danieleteti/delphimvcframework/issues/289) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) + ```delphi + procedure TMainForm.btnSubtractClick(Sender: TObject); + var + lReq: IJSONRPCRequest; + lResp: IJSONRPCResponse; + begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'subtract'; + lReq.RequestID := Random(1000); + lReq.Params.Add(StrToInt(edtValue1.Text)); + lReq.Params.Add(StrToInt(edtValue2.Text)); + lResp := FExecutor.ExecuteRequest(lReq); + edtResult.Text := lResp.Result.AsInteger.ToString; + end; + ``` -- Fixed! [issue291](https://github.com/danieleteti/delphimvcframework/issues/291) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) + - Named parameters example -- Fixed! [issue305](https://github.com/danieleteti/delphimvcframework/issues/305) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) + ```delphi + procedure TMainForm.btnSubtractWithNamedParamsClick(Sender: TObject); + var + lReq: IJSONRPCRequest; + lResp: IJSONRPCResponse; + begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'subtract'; + lReq.RequestID := Random(1000); + lReq.Params.AddByName('Value1', StrToInt(Edit1.Text)); + lReq.Params.AddByName('Value2', StrToInt(Edit2.Text)); + lResp := FExecutor.ExecuteRequest(lReq); + Edit3.Text := lResp.Result.AsInteger.ToString; + end; + ``` -- Fixed! [issue312](https://github.com/danieleteti/delphimvcframework/issues/312) + - Check [official JSONRPC 2.0 documentation](https://www.jsonrpc.org/specification#examples) for more examples. -- Fixed! [issue330](https://github.com/danieleteti/delphimvcframework/issues/330) + - New! JSONRPC Hooks for published objects -- Fixed! [issue333](https://github.com/danieleteti/delphimvcframework/issues/333) + ```delphi + //Called before as soon as the HTTP arrives + procedure TMyPublishedObject.OnBeforeRouting(const JSON: TJDOJsonObject); + + //Called before the invoked method + procedure TMyPublishedObject.OnBeforeCall(const JSONRequest: TJDOJsonObject); + + //Called just before to send response to the client + procedure TMyPublishedObject.OnBeforeSendResponse(const JSONResponse: TJDOJsonObject); + + ``` -- Fixed! [issue334](https://github.com/danieleteti/delphimvcframework/issues/334) - -- Fixed! [issue336](https://github.com/danieleteti/delphimvcframework/issues/336) - -- Fixed! [issue337](https://github.com/danieleteti/delphimvcframework/issues/337) - -- Fixed! [issue338](https://github.com/danieleteti/delphimvcframework/issues/338) - -- Fixed! [issue345](https://github.com/danieleteti/delphimvcframework/issues/345) - -- Fixed! [issue349](https://github.com/danieleteti/delphimvcframework/issues/349) - -- Fixed! [issue350](https://github.com/danieleteti/delphimvcframework/issues/350) - -- Fixed! [issue355](https://github.com/danieleteti/delphimvcframework/issues/355) - -- Fixed! [issue356](https://github.com/danieleteti/delphimvcframework/issues/356) - -- Fixed! [issue362](https://github.com/danieleteti/delphimvcframework/issues/362) - -- Fixed! [issue363](https://github.com/danieleteti/delphimvcframework/issues/363) - -- Fixed! [issue364](https://github.com/danieleteti/delphimvcframework/issues/364) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) - -- Fixed! [issue366](https://github.com/danieleteti/delphimvcframework/issues/366) + - **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). @@ -535,8 +528,6 @@ end; - **Deprecated!** `TDataSetHolder` is deprecated! Use the shining new `ObjectDict(boolean)` instead. -- Fixed! Has been patched a serious security bug affecting deployment configurations which uses internal WebServer to serve static files (do not affect all Apache, IIS or proxied deployments). Thanks to **Stephan Munz** to have discovered it. *Update to dmvcframework-3.2-RC5+ is required for all such kind of deployments.* - - Added ability to serialize/deserialize types enumerated by an array of mapped values (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) ```delphi @@ -581,6 +572,35 @@ end; |Delphi 10.1 Berlin| `packages\d101\dmvcframework_group.groupproj`| |Delphi 10.0 Seattle| `packages\d100\dmvcframework_group.groupproj`| +### Bug Fixes in 3.2.0-boron + +- Fixed! [issue38](https://github.com/danieleteti/delphimvcframework/issues/38) +- Fixed! [issue184](https://github.com/danieleteti/delphimvcframework/issues/184) +- Fixed! [issue278](https://github.com/danieleteti/delphimvcframework/issues/278) +- Fixed! [issue164](https://github.com/danieleteti/delphimvcframework/issues/164) +- Fixed! [issue182](https://github.com/danieleteti/delphimvcframework/issues/182) +- Fixed! [issue232](https://github.com/danieleteti/delphimvcframework/issues/232) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) +- Fixed! [issue289](https://github.com/danieleteti/delphimvcframework/issues/289) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) +- Fixed! [issue291](https://github.com/danieleteti/delphimvcframework/issues/291) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) +- Fixed! [issue305](https://github.com/danieleteti/delphimvcframework/issues/305) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) +- Fixed! [issue312](https://github.com/danieleteti/delphimvcframework/issues/312) +- Fixed! [issue330](https://github.com/danieleteti/delphimvcframework/issues/330) +- Fixed! [issue333](https://github.com/danieleteti/delphimvcframework/issues/333) +- Fixed! [issue334](https://github.com/danieleteti/delphimvcframework/issues/334) +- Fixed! [issue336](https://github.com/danieleteti/delphimvcframework/issues/336) +- Fixed! [issue337](https://github.com/danieleteti/delphimvcframework/issues/337) +- Fixed! [issue338](https://github.com/danieleteti/delphimvcframework/issues/338) +- Fixed! [issue345](https://github.com/danieleteti/delphimvcframework/issues/345) +- Fixed! [issue349](https://github.com/danieleteti/delphimvcframework/issues/349) +- Fixed! [issue350](https://github.com/danieleteti/delphimvcframework/issues/350) +- Fixed! [issue355](https://github.com/danieleteti/delphimvcframework/issues/355) +- Fixed! [issue356](https://github.com/danieleteti/delphimvcframework/issues/356) +- Fixed! [issue362](https://github.com/danieleteti/delphimvcframework/issues/362) +- Fixed! [issue363](https://github.com/danieleteti/delphimvcframework/issues/363) +- Fixed! [issue364](https://github.com/danieleteti/delphimvcframework/issues/364) (Thanks to [João Antônio Duarte](https://github.com/joaoduarte19)) +- Fixed! [issue366](https://github.com/danieleteti/delphimvcframework/issues/366) +- Fixed! Has been patched a serious security bug affecting deployment configurations which uses internal WebServer to serve static files (do not affect all Apache, IIS or proxied deployments). Thanks to **Stephan Munz** to have discovered it. *Update to dmvcframework-3.2-RC5+ is required for all such kind of deployments.* + ## DelphiMVCFramework 3.1.0-lithium (Current Release) - New! Added `TMVCActiveRecord` framework (check sample `activerecord_showcase` and `activerecord_crud`) diff --git a/docs/favicons/favicon_io.zip b/docs/favicons/favicon_io.zip new file mode 100644 index 0000000000000000000000000000000000000000..ed60970a0c8e921386046b4c4f19d19a6c1eb4d2 GIT binary patch literal 56401 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 lRes := RoundTo(lRes, -4); - Assert(SameValue(lRes, 3580.2467), 'Wrong result: ' + FloatToStrF(lRes, ffGeneral, 18,9)); + Assert(SameValue(lRes, 3580.2467), 'Wrong result: ' + FloatToStrF(lRes, ffGeneral, 18, 9)); lReq := TJSONRPCRequest.Create(1234, 'floatstest'); lReq.Params.Add(123); @@ -164,7 +169,7 @@ begin lResp := FExecutor.ExecuteRequest(lReq); lRes := lResp.Result.AsType; lRes := RoundTo(lRes, -4); - Assert(SameValue(lRes, 357), 'Wrong result: ' + FloatToStrF(lRes, ffGeneral, 18,9)); + Assert(SameValue(lRes, 357), 'Wrong result: ' + FloatToStrF(lRes, ffGeneral, 18, 9)); end; procedure TMainForm.btnGetUserClick(Sender: TObject); @@ -241,8 +246,8 @@ begin lReq := TJSONRPCRequest.Create; lReq.Method := 'reversestring'; lReq.RequestID := Random(1000); - lReq.Params.Add(edtReverseString.Text); - lReq.Params.Add(CheckBox1.Checked); + lReq.Params.AddByName('aString', edtReverseString.Text); + lReq.Params.AddByName('aUpperCase', CheckBox1.Checked); lResp := FExecutor.ExecuteRequest(lReq); edtReversedString.Text := lResp.Result.AsString; end; @@ -257,7 +262,7 @@ begin lReq.Method := 'saveperson'; lReq.RequestID := Random(1000); lPerson := TPerson.Create; - lReq.Params.Add(lPerson, pdtObject); + lReq.Params.AddByName('Person', lPerson, pdtObject); lPerson.FirstName := edtFirstName.Text; lPerson.LastName := edtLastName.Text; lPerson.Married := chkMarried.Checked; @@ -289,7 +294,7 @@ begin for I := 0 to lJSON.Count - 1 do begin lJObj := lJSON[I].ObjectValue; - ListBox1.Items.Add(Format('%6s: %-34s € %5.2f',[lJObj.S['codice'], lJObj.S['descrizione'], lJObj.F['prezzo']])); + ListBox1.Items.Add(Format('%6s: %-34s € %5.2f', [lJObj.S['codice'], lJObj.S['descrizione'], lJObj.F['prezzo']])); end; // lbPerson.Items.Add('First Name:'.PadRight(15) + lJSON.S['firstname']); // lbPerson.Items.Add('Last Name:'.PadRight(15) + lJSON.S['lastname']); @@ -311,6 +316,20 @@ begin edtResult.Text := lResp.Result.AsInteger.ToString; end; +procedure TMainForm.btnSubtractWithNamedParamsClick(Sender: TObject); +var + lReq: IJSONRPCRequest; + lResp: IJSONRPCResponse; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'subtract'; + lReq.RequestID := Random(1000); + lReq.Params.AddByName('Value1', StrToInt(Edit1.Text)); + lReq.Params.AddByName('Value2', StrToInt(Edit2.Text)); + lResp := FExecutor.ExecuteRequest(lReq); + Edit3.Text := lResp.Result.AsInteger.ToString; +end; + procedure TMainForm.btnWithJSONClick(Sender: TObject); var lPerson: TJsonObject; @@ -337,7 +356,7 @@ var begin FDMemTable1.Active := False; lReq := TJSONRPCRequest.Create(Random(1000), 'getcustomers'); - lReq.Params.Add(edtFilter.Text); + lReq.Params.AddByName('FilterString', edtFilter.Text); lResp := FExecutor.ExecuteRequest(lReq); FDMemTable1.Active := True; FDMemTable1.LoadFromTValue(lResp.Result); @@ -352,11 +371,11 @@ begin // these are the methods to handle http headers in JSONRPC // the following line and the check on the server is just for demo - assert(FExecutor.HTTPHeadersCount = 0); + Assert(FExecutor.HTTPHeadersCount = 0); FExecutor.AddHTTPHeader(TNetHeader.Create('x-token', TGUID.NewGuid.ToString)); - assert(FExecutor.HTTPHeadersCount = 1); + Assert(FExecutor.HTTPHeadersCount = 1); FExecutor.ClearHTTPHeaders; - assert(FExecutor.HTTPHeadersCount = 0); + Assert(FExecutor.HTTPHeadersCount = 0); FExecutor.AddHTTPHeader(TNetHeader.Create('x-token', TGUID.NewGuid.ToString)); end; diff --git a/samples/jsonrpc_with_published_objects/MyObjectU.pas b/samples/jsonrpc_with_published_objects/MyObjectU.pas index d70d2133..e98a74f9 100644 --- a/samples/jsonrpc_with_published_objects/MyObjectU.pas +++ b/samples/jsonrpc_with_published_objects/MyObjectU.pas @@ -58,18 +58,18 @@ type const JSONResponse: TJDOJsonObject); public [MVCDoc('You know, returns aValue1 - aValue2')] - function Subtract(aValue1, aValue2: Integer): Integer; + function Subtract(Value1, Value2: Integer): Integer; [MVCDoc('Returns the revers of the string passed as input')] function ReverseString(const aString: string; const aUpperCase: Boolean): string; [MVCDoc('Returns the next monday starting from aDate')] function GetNextMonday(const aDate: TDate): TDate; function PlayWithDatesAndTimes(const aJustAFloat: Double; const aTime: TTime; const aDate: TDate; const aDateAndTime: TDateTime): TDateTime; - function GetCustomers(aString: string): TDataset; + function GetCustomers(FilterString: string): TDataset; function GetMulti: TMultiDataset; function GetStringDictionary: TMVCStringDictionary; function GetUser(aUserName: string): TPerson; - function SavePerson(const aPerson: TJsonObject): Integer; + function SavePerson(const Person: TJsonObject): Integer; function FloatsTest(const aDouble: Double; const aExtended: Extended): Extended; procedure DoSomething; function SaveObjectWithJSON(const WithJSON: TJsonObject): TJsonObject; @@ -123,15 +123,15 @@ begin Result := aDouble + aExtended; end; -function TMyObject.GetCustomers(aString: string): TDataset; +function TMyObject.GetCustomers(FilterString: string): TDataset; var lMT: TFDMemTable; begin lMT := GetCustomersDataset; try - if not aString.IsEmpty then + if not FilterString.IsEmpty then begin - lMT.Filter := aString; + lMT.Filter := FilterString; lMT.Filtered := True; end; lMT.First; @@ -269,7 +269,7 @@ begin end; end; -function TMyObject.SavePerson(const aPerson: TJsonObject): Integer; +function TMyObject.SavePerson(const Person: TJsonObject): Integer; // var // lPerson: TPerson; begin @@ -284,9 +284,9 @@ begin Result := Random(1000); end; -function TMyObject.Subtract(aValue1, aValue2: Integer): Integer; +function TMyObject.Subtract(Value1, Value2: Integer): Integer; begin - Result := aValue1 - aValue2; + Result := Value1 - Value2; end; { TData } diff --git a/sources/MVCFramework.Commons.pas b/sources/MVCFramework.Commons.pas index 5b07913f..db107b49 100644 --- a/sources/MVCFramework.Commons.pas +++ b/sources/MVCFramework.Commons.pas @@ -120,7 +120,7 @@ type OneMiB = 1048576; OneKiB = 1024; DEFAULT_MAX_REQUEST_SIZE = OneMiB * 5; // 5 MiB - HATEOAS_PROP_NAME = '_links'; + HATEOAS_PROP_NAME = 'links'; X_HTTP_Method_Override = 'X-HTTP-Method-Override'; end; diff --git a/sources/MVCFramework.JSONRPC.pas b/sources/MVCFramework.JSONRPC.pas index f090f95a..fa8777b9 100644 --- a/sources/MVCFramework.JSONRPC.pas +++ b/sources/MVCFramework.JSONRPC.pas @@ -136,8 +136,13 @@ type function GetItem(const Index: Integer): TValue; function GetItemDataType(const Index: Integer): TJSONRPCParamDataType; protected - FParamsValue: TList; - FParamsType: TList; + fParamValues: TList; + fParamNames: TList; + fParamTypes: TList; + private + procedure CheckNotNames; + procedure CheckBalancedParams; + function GetItemName(const Index: Integer): string; public constructor Create; virtual; destructor Destroy; override; @@ -145,6 +150,7 @@ type function Count: Integer; property Items[const index: Integer]: TValue read GetItem; default; property ItemsType[const index: Integer]: TJSONRPCParamDataType read GetItemDataType; + property ItemsName[const index: Integer]: string read GetItemName; function ToArray: TArray; procedure Add(const Value: string); overload; procedure Add(const Value: Integer); overload; @@ -156,6 +162,16 @@ type procedure Add(const Value: TDateTime); overload; procedure Add(const Value: Double); overload; procedure Add(const Value: TValue; const ParamType: TJSONRPCParamDataType); overload; + procedure AddByName(const Name: string; const Value: string); overload; + procedure AddByName(const Name: string; const Value: Integer); overload; + procedure AddByName(const Name: string; const Value: TJDOJsonObject); overload; + procedure AddByName(const Name: string; const Value: TJDOJsonArray); overload; + procedure AddByName(const Name: string; const Value: Boolean); overload; + procedure AddByName(const Name: string; const Value: TDate); overload; + procedure AddByName(const Name: string; const Value: TTime); overload; + procedure AddByName(const Name: string; const Value: TDateTime); overload; + procedure AddByName(const Name: string; const Value: Double); overload; + procedure AddByName(const Name: string; const Value: TValue; const ParamType: TJSONRPCParamDataType); overload; end; IJSONRPCNotification = interface(IJSONRPCObject) @@ -498,6 +514,84 @@ begin end; end; +procedure AppendTValueToJsonObject(const Value: TValue; const Name: string; const ParamType: TJSONRPCParamDataType; + const JSONObj: TJDOJsonObject); +var + lSer: TMVCJsonDataObjectsSerializer; + lOrdinalValue: Int64; +begin + case ParamType of + pdtInteger: + begin + JSONObj.I[name] := Value.AsInteger; + end; + pdtFloat: + begin + JSONObj.F[name] := Value.AsExtended; + end; + pdtDateTime: + begin + JSONObj.S[name] := DateTimeToISOTimeStamp(FloatToDateTime(Value.AsExtended)); + end; + pdtDate: + begin + JSONObj.S[name] := DateToISODate(FloatToDateTime(Value.AsExtended)); + end; + pdtTime: + begin + JSONObj.S[name] := TimeToISOTime(FloatToDateTime(Value.AsExtended)); + end; + pdtString: + begin + JSONObj.S[name] := Value.AsString; + end; + pdtLongInteger: + begin + JSONObj.L[name] := Value.AsInt64; + end; + pdtBoolean: + begin + if not Value.TryAsOrdinal(lOrdinalValue) then + begin + raise EMVCException.Create('Invalid ordinal parameter'); + end; + JSONObj.B[name] := lOrdinalValue = 1; + end; + pdTJDOJsonObject: + begin + JSONObj.O[name] := (Value.AsObject as TJDOJsonObject).Clone as TJDOJsonObject; + end; + pdtJSONArray: + begin + JSONObj.A[name] := (Value.AsObject as TJDOJsonArray).Clone as TJDOJsonArray; + end; + pdtObject: + begin + if Value.AsObject is TDataSet then + begin + lSer := TMVCJsonDataObjectsSerializer.Create; + try + lSer.DataSetToJsonArray(TDataSet(Value.AsObject), JSONObj.A[name], TMVCNameCase.ncLowerCase, []); + finally + lSer.Free; + end + end + else + begin + lSer := TMVCJsonDataObjectsSerializer.Create; + try + JSONObj.O[name] := lSer.SerializeObjectToJSON(Value.AsObject, + TMVCSerializationType.stProperties, [], nil); + finally + lSer.Free; + end; + end; + end; + else + raise EMVCException.Create('Invalid type'); + end; +end; + function JSONDataValueToTValue(const JSONDataValue: TJsonDataValueHelper): TValue; overload; begin case JSONDataValue.Typ of @@ -1048,7 +1142,15 @@ begin ('Cannot call a function using a JSON-RPC notification. [HINT] Use requests for functions and notifications for procedures'); end; - lJSONRPCReq.FillParameters(lJSON, lRTTIMethod); + try + lJSONRPCReq.FillParameters(lJSON, lRTTIMethod); + except + on Ex: EMVCJSONRPCErrorResponse do + begin + raise EMVCJSONRPCInvalidParams.Create('Cannot map all parameters to remote method. ' + Ex.Message); + end; + end; + try TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_CALL, lJSON, 'JSONRequest'); LogD('[JSON-RPC][CALL][' + CALL_TYPE[lRTTIMethod.MethodKind] + '] ' + lRTTIMethod.Name); @@ -1058,6 +1160,10 @@ begin begin raise EMVCJSONRPCInvalidParams.Create('Check your input parameters types'); end; + on Ex: EMVCJSONRPCInvalidRequest do + begin + raise EMVCJSONRPCInvalidParams.Create(Ex.Message); + end; end; case lJSONRPCReq.RequestType of @@ -1365,23 +1471,29 @@ begin end; procedure TJSONRPCNotification.FillParameters( - const - JSON: - TJDOJsonObject; -const - RTTIMethod: - TRTTIMethod); + const JSON: TJDOJsonObject; +const RTTIMethod: TRTTIMethod); var lRTTIMethodParams: TArray; lRTTIMethodParam: TRttiParameter; lJSONParams: TJDOJsonArray; + lJSONNamedParams: TJDOJsonObject; I: Integer; + lUseNamedParams: Boolean; begin + lUseNamedParams := False; lJSONParams := nil; + lJSONNamedParams := nil; Params.Clear; if JSON.Types[JSONRPC_PARAMS] = jdtArray then begin lJSONParams := JSON.A[JSONRPC_PARAMS]; + lUseNamedParams := False; + end + else if JSON.Types[JSONRPC_PARAMS] = jdtObject then + begin + lJSONNamedParams := JSON.O[JSONRPC_PARAMS]; + lUseNamedParams := True; end else if JSON.Types[JSONRPC_PARAMS] <> jdtNone then @@ -1390,12 +1502,27 @@ begin end; lRTTIMethodParams := RTTIMethod.GetParameters; - if (Length(lRTTIMethodParams) > 0) and (not Assigned(lJSONParams)) then - raise EMVCJSONRPCInvalidParams.CreateFmt('Wrong parameters count. Expected %d got %d.', - [Length(lRTTIMethodParams), 0]); - if Assigned(lJSONParams) and (Length(lRTTIMethodParams) <> lJSONParams.Count) then - raise EMVCJSONRPCInvalidParams.CreateFmt('Wrong parameters count. Expected %d got %d.', - [Length(lRTTIMethodParams), lJSONParams.Count]); + if lUseNamedParams then + begin + if (Length(lRTTIMethodParams) > 0) and (not Assigned(lJSONNamedParams)) then + raise EMVCJSONRPCInvalidParams.CreateFmt('Wrong parameters count. Expected %d got %d.', + [Length(lRTTIMethodParams), 0]); + + if Assigned(lJSONNamedParams) and (Length(lRTTIMethodParams) <> lJSONNamedParams.Count) then + raise EMVCJSONRPCInvalidParams.CreateFmt('Wrong parameters count. Expected %d got %d.', + [Length(lRTTIMethodParams), lJSONNamedParams.Count]); + end + else + begin + if (Length(lRTTIMethodParams) > 0) and (not Assigned(lJSONParams)) then + raise EMVCJSONRPCInvalidParams.CreateFmt('Wrong parameters count. Expected %d got %d.', + [Length(lRTTIMethodParams), 0]); + + if Assigned(lJSONParams) and (Length(lRTTIMethodParams) <> lJSONParams.Count) then + raise EMVCJSONRPCInvalidParams.CreateFmt('Wrong parameters count. Expected %d got %d.', + [Length(lRTTIMethodParams), lJSONParams.Count]); + end; + for lRTTIMethodParam in lRTTIMethodParams do begin if lRTTIMethodParam.Flags * [pfVar, pfOut, pfArray, pfReference] <> [] then @@ -1407,10 +1534,19 @@ begin // scroll json params and rttimethod params and find the best match if Assigned(lJSONParams) then begin + // positional params for I := 0 to lJSONParams.Count - 1 do begin JSONDataValueToTValueParam(lJSONParams[I], lRTTIMethodParams[I], Params); end; + end + else if Assigned(lJSONNamedParams) then + begin + // named params + for I := 0 to lJSONNamedParams.Count - 1 do + begin + JSONDataValueToTValueParam(lJSONNamedParams.Values[lRTTIMethodParams[I].Name.ToLower], lRTTIMethodParams[I], Params); + end; end; end; @@ -1424,10 +1560,24 @@ begin Result.S[JSONRPC_METHOD] := FMethod; if FParams.Count > 0 then begin - for I := 0 to FParams.Count - 1 do - begin - AppendTValueToJsonArray(FParams.FParamsValue[I], FParams.FParamsType[I], - Result.A[JSONRPC_PARAMS]); + if FParams.fParamNames.Count = 0 then + begin // positional params + for I := 0 to FParams.Count - 1 do + begin + AppendTValueToJsonArray(FParams.fParamValues[I], FParams.fParamTypes[I], + Result.A[JSONRPC_PARAMS]); + end; + end + else + begin // named params + for I := 0 to FParams.Count - 1 do + begin + AppendTValueToJsonObject( + FParams.fParamValues[I], + FParams.fParamNames[I], + FParams.fParamTypes[I], + Result.O[JSONRPC_PARAMS]); + end; end; end; end; @@ -1560,10 +1710,7 @@ begin FID := Value; end; -procedure TJSONRPCResponse.SetJSON( - const - JSON: - TJDOJsonObject); +procedure TJSONRPCResponse.SetJSON(const JSON: TJDOJsonObject); begin if JSON.Types[JSONRPC_ID] = jdtString then RequestID := JSON.S[JSONRPC_ID] @@ -1762,117 +1909,106 @@ begin // do nothing end; -procedure TJSONRPCProxyGenerator.StartGeneration( - const - aClassName: - string); +procedure TJSONRPCProxyGenerator.StartGeneration(const aClassName: string); begin // do nothing end; { TJSONRPCRequestParams } -procedure TJSONRPCRequestParams.Add( - const - Value: - TJDOJsonArray); +procedure TJSONRPCRequestParams.Add(const Value: TJDOJsonArray); begin Add(Value, pdtJSONArray); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - TJDOJsonObject); +procedure TJSONRPCRequestParams.Add(const Value: TJDOJsonObject); begin Add(Value, pdTJDOJsonObject); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - Integer); +procedure TJSONRPCRequestParams.Add(const Value: Integer); begin Add(Value, pdtInteger); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - string); +procedure TJSONRPCRequestParams.Add(const Value: string); begin Add(Value, pdtString); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - Boolean); +procedure TJSONRPCRequestParams.Add(const Value: Boolean); begin Add(Value, pdtBoolean); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - Double); +procedure TJSONRPCRequestParams.Add(const Value: Double); begin Add(Value, pdtFloat); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - TDateTime); +procedure TJSONRPCRequestParams.Add(const Value: TDateTime); begin Add(Value, pdtDateTime); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - TTime); +procedure TJSONRPCRequestParams.Add(const Value: TTime); begin Add(Value, pdtTime); end; -procedure TJSONRPCRequestParams.Add( - const - Value: - TDate); +procedure TJSONRPCRequestParams.Add(const Value: TDate); begin Add(Value, pdtDate); end; +procedure TJSONRPCRequestParams.CheckBalancedParams; +begin + if fParamNames.Count <> fParamValues.Count then + begin + raise EMVCJSONRPCException.Create('Cannot mix positional with named parameters'); + end; +end; + +procedure TJSONRPCRequestParams.CheckNotNames; +begin + if fParamNames.Count > 0 then + begin + raise EMVCJSONRPCException.Create('Cannot mix positional with named parameters'); + end; +end; + procedure TJSONRPCRequestParams.Clear; begin - FParamsValue.Clear; - FParamsType.Clear; + fParamValues.Clear; + fParamTypes.Clear; + fParamNames.Clear; end; function TJSONRPCRequestParams.Count: Integer; begin - Result := FParamsValue.Count; + Result := fParamValues.Count; end; constructor TJSONRPCRequestParams.Create; begin inherited Create; - FParamsValue := TList.Create; - FParamsType := TList.Create; + fParamValues := TList.Create; + fParamTypes := TList.Create; + fParamNames := TList.Create; end; destructor TJSONRPCRequestParams.Destroy; var lValue: TValue; begin - for lValue in FParamsValue do + for lValue in fParamValues do begin if lValue.IsObject then lValue.AsObject.Free; end; - FParamsValue.Free; - FParamsType.Free; + fParamValues.Free; + fParamTypes.Free; + fParamNames.Free; inherited; end; @@ -1881,7 +2017,7 @@ function TJSONRPCRequestParams.GetItem( Index: Integer): TValue; begin - Result := FParamsValue[index]; + Result := fParamValues[index]; end; function TJSONRPCRequestParams.GetItemDataType( @@ -1889,24 +2025,86 @@ function TJSONRPCRequestParams.GetItemDataType( Index: Integer): TJSONRPCParamDataType; begin - Result := FParamsType[index]; + Result := fParamTypes[index]; +end; + +function TJSONRPCRequestParams.GetItemName(const Index: Integer): string; +begin + Result := fParamNames[index]; end; function TJSONRPCRequestParams.ToArray: TArray; begin - Result := FParamsValue.ToArray; + Result := fParamValues.ToArray; end; -procedure TJSONRPCRequestParams.Add( - const - Value: - TValue; -const - ParamType: - TJSONRPCParamDataType); +procedure TJSONRPCRequestParams.Add(const Value: TValue; const ParamType: TJSONRPCParamDataType); begin - FParamsValue.Add(Value); - FParamsType.Add(ParamType); + CheckNotNames; + fParamValues.Add(Value); + fParamTypes.Add(ParamType); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: Boolean); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtBoolean); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: TJDOJsonArray); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtJSONArray); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: TJDOJsonObject); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdTJDOJsonObject); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: Integer); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtInteger); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name, Value: string); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtString); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: TValue; const ParamType: TJSONRPCParamDataType); +begin + CheckBalancedParams; + fParamNames.Add(LowerCase(name)); + fParamValues.Add(Value); + fParamTypes.Add(ParamType); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: Double); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtFloat); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: TDateTime); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtDateTime); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: TTime); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtTime); +end; + +procedure TJSONRPCRequestParams.AddByName(const Name: string; +const Value: TDate); +begin + AddByName(name, Value, TJSONRPCParamDataType.pdtDate); end; { EMVCJSONRPCException } diff --git a/sources/MVCFramework.Middleware.StaticFiles.pas b/sources/MVCFramework.Middleware.StaticFiles.pas index db6a0f8d..5bd99c9b 100644 --- a/sources/MVCFramework.Middleware.StaticFiles.pas +++ b/sources/MVCFramework.Middleware.StaticFiles.pas @@ -40,7 +40,7 @@ type /// /// URL segment that represents the path to static files /// - STATIC_FILES_PATH = '/'; + STATIC_FILES_PATH = '/static'; /// /// Physical path of the root folder that contains the static files @@ -91,7 +91,8 @@ implementation uses System.SysUtils, - System.IOUtils; + System.NetEncoding, + System.IOUtils, System.Classes; { TMVCStaticFilesMiddleware } @@ -194,6 +195,13 @@ begin AHandled := SendStaticFileIfPresent(AContext, lFileName); end; + // if (not AHandled) and lPathInfo.EndsWith('favicon.ico') then + // begin + // AContext.Response.SetContentStream(TBytesStream.Create(TNetEncoding.Base64.DecodeStringToBytes(DMVC_FAVICON)), + // TMVCMediaType.IMAGE_X_ICON); + // AHandled := True; + // end; + if (not AHandled) and fSPAWebAppSupport and AContext.Request.ClientPreferHTML and (not fIndexDocument.IsEmpty) then begin lFileName := TPath.GetFullPath(TPath.Combine(fDocumentRoot, fIndexDocument)); diff --git a/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas b/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas index 43be0406..6bb9f7c6 100644 --- a/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas +++ b/sources/MVCFramework.Serializer.JsonDataObjects.CustomTypes.pas @@ -458,6 +458,11 @@ begin case lObj.DataSetSerializationType of dstSingleRecord: begin + if TDataSet(lObj.Data).Eof then + begin + raise EMVCSerializationException.Create(HTTP_STATUS.InternalServerError, + 'Cannot serialize a single record of an empty dataset'); + end; fCurrentSerializer.InternalSerializeDataSetRecord( TDataSet(lObj.Data), lOutObject.O[lName], diff --git a/unittests/general/Several/JSONRPCTestsU.pas b/unittests/general/Several/JSONRPCTestsU.pas index ace5677e..7f298f9b 100644 --- a/unittests/general/Several/JSONRPCTestsU.pas +++ b/unittests/general/Several/JSONRPCTestsU.pas @@ -37,6 +37,10 @@ type [Test] procedure TestRequestWithArrayParameters; [Test] + procedure TestRequestWithNamedParameters; + [Test] + procedure TestRequestWithMixedParamaters; + [Test] procedure TestRequestWithNoParameters; [Test] procedure TestRequestWithMalformedJSON; @@ -88,6 +92,44 @@ begin end, EMVCJSONRPCParseError); end; +procedure TTestJSONRPC.TestRequestWithMixedParamaters; +var + lReq: IJSONRPCRequest; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'subtract'; + Assert.WillRaise( + procedure + begin + lReq.Params.AddByName('par1', 42); + lReq.Params.Add(42); + end, EMVCJSONRPCException); + + lReq := TJSONRPCRequest.Create; + lReq.Method := 'subtract'; + Assert.WillRaise( + procedure + begin + lReq.Params.Add(42); + lReq.Params.AddByName('par1', 42); + end, EMVCJSONRPCException); +end; + +procedure TTestJSONRPC.TestRequestWithNamedParameters; +var + lReq: IJSONRPCRequest; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'subtract'; + lReq.Params.AddByName('par1', 42); + lReq.Params.AddByName('PAR2', 23); + lReq.RequestID := 1; + Assert.AreEqual(1, lReq.RequestID.AsInteger); + Assert.AreEqual('par1', lReq.Params.ItemsName[0]); + Assert.AreEqual('par2', lReq.Params.ItemsName[1]); + Assert.AreEqual('subtract', lReq.Method); +end; + procedure TTestJSONRPC.TestRequestWithNoParameters; var lReq: IJSONRPCRequest; diff --git a/unittests/general/Several/LiveServerTestU.pas b/unittests/general/Several/LiveServerTestU.pas index 5bbd87fe..0b53d37a 100644 --- a/unittests/general/Several/LiveServerTestU.pas +++ b/unittests/general/Several/LiveServerTestU.pas @@ -220,7 +220,10 @@ type // test web server [Test] procedure TestDirectoryTraversal1; - + [Test] + procedure TestDirectoryTraversal2; + [Test] + procedure TestSPASupport; // test server side views [Test] procedure TestViewDataViewDataSet; @@ -242,10 +245,18 @@ type [Test] procedure TestRequestWithParams_I_I_ret_I; [Test] + procedure TestRequestWithNamedParams_I_I_ret_I; + [Test] procedure TestRequestWithParams_I_I_I_ret_O; [Test] + procedure TestRequestWithNamedParams_I_I_I_ret_O; + [Test] + procedure TestRequestWithWrongNamedParams; + [Test] procedure TestRequest_S_I_ret_S; [Test] + procedure TestRequest_NamedParams_S_I_ret_S; + [Test] procedure TestRequestWithParams_I_I_ret_A; [Test] procedure TestRequestWithParams_DT_T_ret_DT; @@ -1408,6 +1419,53 @@ begin end; end; +procedure TServerTest.TestDirectoryTraversal2; +var + lRes: IRESTResponse; + I: Integer; + lUrl: string; +begin + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/index.html', []); + Assert.areEqual(200, lRes.ResponseCode, '/static/index.html'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static.html', []); + Assert.areEqual(200, lRes.ResponseCode, '/static.html'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/', []); + Assert.areEqual(200, lRes.ResponseCode, '/static/'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static', []); + Assert.areEqual(200, lRes.ResponseCode, '/static'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/..\..\donotdeleteme.txt', []); + Assert.areEqual(404, lRes.ResponseCode); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/../../donotdeleteme.txt', []); + Assert.areEqual(404, lRes.ResponseCode); + + lUrl := 'Windows\win.ini'; + for I := 1 to 30 do + begin + lUrl := '..\' + lUrl; + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/' + lUrl, []); + Assert.areEqual(404, lRes.ResponseCode, 'Fail with: ' + '/' + lUrl); + end; +end; + procedure TServerTest.TestSerializeAndDeserializeNullables; var lRes: IRESTResponse; @@ -1544,6 +1602,46 @@ begin DoLogout; end; +procedure TServerTest.TestSPASupport; +var + lRes: IRESTResponse; + I: Integer; + lUrl: string; +begin + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/index.html', []); + Assert.areEqual(200, lRes.ResponseCode); + Assert.Contains(lRes.BodyAsString, 'This is a TEXT file'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/', []); + Assert.areEqual(200, lRes.ResponseCode, '/static/'); + Assert.Contains(lRes.BodyAsString, 'This is a TEXT file'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/..\..\donotdeleteme.txt', []); + Assert.areEqual(404, lRes.ResponseCode, '/static/..\..\donotdeleteme.txt'); + + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/static/../../donotdeleteme.txt', []); + Assert.areEqual(404, lRes.ResponseCode, '/static/../../donotdeleteme.txt'); + Assert.Contains(lRes.Error.ExceptionMessage, 'Not Found', true); + + lUrl := 'Windows\win.ini'; + for I := 1 to 30 do + begin + lUrl := '..\' + lUrl; + lRes := RESTClient + .Accept(TMVCMediaType.TEXT_HTML) + .doGET('/' + lUrl, []); + Assert.areEqual(404, lRes.ResponseCode, 'Fail with: ' + '/' + lUrl); + end; +end; + procedure TServerTest.TestStringDictionary; var lRes: IRESTResponse; @@ -1853,6 +1951,65 @@ begin Assert.areEqual(2000, lYear); end; +procedure TJSONRPCServerTest.TestRequestWithWrongNamedParams; +var + lReq: IJSONRPCRequest; + lRPCResp: IJSONRPCResponse; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'add'; + lReq.Params.AddByName('wrongname1', 3); + lReq.Params.AddByName('wrongname2', 4); + lReq.Params.AddByName('wrongname3', 5); + lReq.RequestID := 1234; + + lRPCResp := FExecutor.ExecuteRequest(lReq); + Assert.isTrue(lRPCResp.IsError); + Assert.Contains(lRPCResp.Error.ErrMessage, 'cannot map all parameter', true); +end; + +procedure TJSONRPCServerTest.TestRequestWithNamedParams_I_I_I_ret_O; +var + lReq: IJSONRPCRequest; + lRPCResp: IJSONRPCResponse; + lS: string; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'add'; + lReq.Params.AddByName('value1', 3); + lReq.Params.AddByName('value2', 4); + lReq.Params.AddByName('value3', 5); + lReq.RequestID := 1234; + + lRPCResp := FExecutor.ExecuteRequest(lReq); + lS := (lRPCResp.Result.AsObject as TJDOJsonObject).ToJSON(); + Assert.areEqual(12, TJDOJsonObject(lRPCResp.Result.AsObject).I['res']); + + lRPCResp := FExecutor2.ExecuteRequest(lReq); + lS := (lRPCResp.Result.AsObject as TJDOJsonObject).ToJSON(); + Assert.areEqual(12, TJDOJsonObject(lRPCResp.Result.AsObject).I['res']); +end; + +procedure TJSONRPCServerTest.TestRequestWithNamedParams_I_I_ret_I; +var + lReq: IJSONRPCRequest; + lResp: IJSONRPCResponse; +begin + lReq := TJSONRPCRequest.Create; + lReq.RequestID := 1234; + lReq.Method := 'subtract'; + lReq.Params.AddByName('Value1', 18); + lReq.Params.AddByName('Value2', 8); + + lResp := FExecutor.ExecuteRequest(lReq); + Assert.areEqual(10, lResp.Result.AsInteger); + Assert.areEqual(1234, lResp.RequestID.AsInteger); + + lResp := FExecutor2.ExecuteRequest(lReq); + Assert.areEqual(10, lResp.Result.AsInteger); + Assert.areEqual(1234, lResp.RequestID.AsInteger); +end; + procedure TJSONRPCServerTest.TestRequestWithoutParams; var lReq: IJSONRPCNotification; @@ -1874,6 +2031,7 @@ begin lReq.Method := 'subtract'; lReq.Params.Add(18); lReq.Params.Add(8); + lResp := FExecutor.ExecuteRequest(lReq); Assert.areEqual(10, lResp.Result.AsInteger); Assert.areEqual(1234, lResp.RequestID.AsInteger); @@ -1939,6 +2097,24 @@ begin Assert.areEqual(12, TJDOJsonObject(lRPCResp.Result.AsObject).I['res']); end; +procedure TJSONRPCServerTest.TestRequest_NamedParams_S_I_ret_S; +var + lReq: IJSONRPCRequest; + lRPCResp: IJSONRPCResponse; +begin + lReq := TJSONRPCRequest.Create; + lReq.Method := 'MultiplyString'; + lReq.Params.AddByName('aString', 'Daniele'); + lReq.Params.AddByName('Multiplier', 4); + lReq.RequestID := 1234; + lRPCResp := FExecutor.ExecuteRequest(lReq); + Assert.isFalse(lRPCResp.IsError); + Assert.areEqual('DanieleDanieleDanieleDaniele', lRPCResp.Result.AsString); + + lRPCResp := FExecutor2.ExecuteRequest(lReq); + Assert.areEqual('DanieleDanieleDanieleDaniele', lRPCResp.Result.AsString); +end; + procedure TJSONRPCServerTest.TestRequest_S_I_ret_S; var lReq: IJSONRPCRequest; diff --git a/unittests/general/TestServer/TestServer.dproj b/unittests/general/TestServer/TestServer.dproj index 2dac8ade..ffb309c6 100644 --- a/unittests/general/TestServer/TestServer.dproj +++ b/unittests/general/TestServer/TestServer.dproj @@ -28,17 +28,6 @@ Base true - - true - Base - true - - - true - Cfg_1 - true - true - true Base @@ -117,17 +106,6 @@ TestServer_Icon.ico - - RELEASE;$(DCC_Define) - 0 - false - 0 - - - 1033 - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) - TestServer_Icon.ico - DEBUG;$(DCC_Define) false @@ -171,10 +149,6 @@ Base - - Cfg_1 - Base - Delphi.Personality.12 diff --git a/unittests/general/TestServer/TestServerControllerJSONRPCU.pas b/unittests/general/TestServer/TestServerControllerJSONRPCU.pas index 20e1c5db..677fa4dd 100644 --- a/unittests/general/TestServer/TestServerControllerJSONRPCU.pas +++ b/unittests/general/TestServer/TestServerControllerJSONRPCU.pas @@ -8,18 +8,18 @@ uses type TTestJSONRPCController = class(TMVCJSONRPCController) public - function Subtract(aValue1, aValue2: Int64): Integer; + function Subtract(Value1, Value2: Int64): Integer; procedure MyNotify; - function Add(aValue1, aValue2, aValue3: Int64): TJsonObject; + function Add(Value1, Value2, Value3: Int64): TJsonObject; function GetListFromTo(aFrom, aTo: Int64): TJsonArray; function MultiplyString(aString: string; Multiplier: Int64): string; end; TTestJSONRPCClass = class(TObject) public - function Subtract(aValue1, aValue2: Int64): Integer; + function Subtract(Value1, Value2: Int64): Integer; procedure MyNotify; - function Add(aValue1, aValue2, aValue3: Int64): TJsonObject; + function Add(Value1, Value2, Value3: Int64): TJsonObject; function GetListFromTo(aFrom, aTo: Int64): TJsonArray; function MultiplyString(aString: string; Multiplier: Int64): string; function AddTimeToDateTime(aDateTime: TDateTime; aTime: TTime): TDateTime; @@ -32,10 +32,10 @@ uses { TTestJSONRPCController } -function TTestJSONRPCController.Add(aValue1, aValue2, aValue3: Int64): TJsonObject; +function TTestJSONRPCController.Add(Value1, Value2, Value3: Int64): TJsonObject; begin Result := TJsonObject.Create; - Result.I['res'] := aValue1 + aValue2 + aValue3; + Result.I['res'] := Value1 + Value2 + Value3; end; function TTestJSONRPCController.GetListFromTo(aFrom, aTo: Int64): TJsonArray; @@ -65,17 +65,17 @@ begin Self.ClassName; end; -function TTestJSONRPCController.Subtract(aValue1, aValue2: Int64): Integer; +function TTestJSONRPCController.Subtract(Value1, Value2: Int64): Integer; begin - Result := aValue1 - aValue2; + Result := Value1 - Value2; end; { TTestJSONRPCClass } -function TTestJSONRPCClass.Add(aValue1, aValue2, aValue3: Int64): TJsonObject; +function TTestJSONRPCClass.Add(Value1, Value2, Value3: Int64): TJsonObject; begin Result := TJsonObject.Create; - Result.I['res'] := aValue1 + aValue2 + aValue3; + Result.I['res'] := Value1 + Value2 + Value3; end; function TTestJSONRPCClass.AddTimeToDateTime(aDateTime: TDateTime; @@ -111,9 +111,9 @@ begin Self.ClassName; end; -function TTestJSONRPCClass.Subtract(aValue1, aValue2: Int64): Integer; +function TTestJSONRPCClass.Subtract(Value1, Value2: Int64): Integer; begin - Result := aValue1 - aValue2; + Result := Value1 - Value2; end; end. diff --git a/unittests/general/TestServer/WebModuleUnit.pas b/unittests/general/TestServer/WebModuleUnit.pas index d975ea7d..b6a90d55 100644 --- a/unittests/general/TestServer/WebModuleUnit.pas +++ b/unittests/general/TestServer/WebModuleUnit.pas @@ -98,7 +98,9 @@ begin Result := TTestFault2Controller.Create; // this will raise an exception end) .AddMiddleware(TMVCSpeedMiddleware.Create) - .AddMiddleware(TMVCStaticFilesMiddleware.Create('/', '..\www')) + .AddMiddleware(TMVCStaticFilesMiddleware.Create('/', '..\www', 'index.html', False)) + .AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', '..\www', 'index.html', False)) + .AddMiddleware(TMVCStaticFilesMiddleware.Create('/spa', '..\www', 'index.html', True)) .AddMiddleware(TMVCBasicAuthenticationMiddleware.Create(TBasicAuthHandler.Create)) .AddMiddleware(TMVCCustomAuthenticationMiddleware.Create(TCustomAuthHandler.Create, '/system/users/logged')) .AddMiddleware(TMVCCompressionMiddleware.Create); diff --git a/unittests/general/TestServer/www/static.html b/unittests/general/TestServer/www/static.html new file mode 100644 index 00000000..46a6c945 --- /dev/null +++ b/unittests/general/TestServer/www/static.html @@ -0,0 +1 @@ +This is a TEXT file named STATIC.html \ No newline at end of file