diff --git a/LICENSE-THIRD-PARTY b/THIRD-PARTY-LICENSES similarity index 50% rename from LICENSE-THIRD-PARTY rename to THIRD-PARTY-LICENSES index ece76399..23f6acaf 100644 --- a/LICENSE-THIRD-PARTY +++ b/THIRD-PARTY-LICENSES @@ -202,4 +202,203 @@ Apache License 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. \ No newline at end of file + limitations under the License. + + +-------------------------------------------------------------------------------- + +danielaparker/JsonCons.Net +v1.1.0 + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2013-2018 Docker, Inc. + + 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 + + https://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. + + \ No newline at end of file diff --git a/docs/utilities/jmespath-functions.md b/docs/utilities/jmespath-functions.md new file mode 100644 index 00000000..411b7fec --- /dev/null +++ b/docs/utilities/jmespath-functions.md @@ -0,0 +1,197 @@ +--- +title: JMESPath Functions +description: Utility +--- + + + +???+ tip + JMESPath is a query language for JSON used by AWS CLI, AWS Python SDK, and Powertools for AWS Lambda. + +Built-in JMESPath functions to easily deserialize common encoded JSON payloads in Lambda functions. + +## Key features + +* Deserialize JSON from JSON strings, base64, and compressed data +* Use JMESPath to extract and combine data recursively +* Provides commonly used JMESPath expression with popular event sources + +## Getting started + +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/develop/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathExamples.cs){target="_blank"}. + +You might have events that contains encoded JSON payloads as string, base64, or even in compressed format. It is a common use case to decode and extract them partially or fully as part of your Lambda function invocation. + +???+ info "Terminology" + **Envelope** is the terminology we use for the **JMESPath expression** to extract your JSON object from your data input. We might use those two terms interchangeably. + +### Extracting data + +You can use the `JsonTransformer.Transform` function with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank" rel="nofollow"}. + +???+ tip + Another common use case is to fetch deeply nested data, filter, flatten, and more. + +=== "Transform" + ```csharp hl_lines="1 2" + var transformer = JsonTransformer.Parse("powertools_json(body).customerId"); + using var result = transformer.Transform(doc.RootElement); + + Logger.LogInformation(result.RootElement.GetRawText()); // "dd4649e6-2484-4993-acb8-0f9123103394" + ``` + +=== "Payload" + ```json + { + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] + } + ``` + +### Built-in envelopes + +We provide built-in envelopes for popular AWS Lambda event sources to easily decode and/or deserialize JSON objects. + +| Envelop | JMESPath expression | +|---------------------|-----------------------------------------------------------------------------| +| API_GATEWAY_HTTP | powertools_json(body) | +| API_GATEWAY_REST | powertools_json(body) | +| CLOUDWATCH_LOGS | awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*] | +| KINESIS_DATA_STREAM | Records[*].kinesis.powertools_json(powertools_base64(data)) | +| SNS | Records[*].Sns.Message | powertools_json(@) | +| SQS | Records[*].powertools_json(body) | + +???+ tip "Using SNS?" + If you don't require SNS metadata, enable [raw message delivery](https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html). It will reduce multiple payload layers and size, when using SNS in combination with other services (_e.g., SQS, S3, etc_). + +## Advanced + +### Built-in JMESPath functions + +You can use our built-in JMESPath functions within your envelope expression. They handle deserialization for common data formats found in AWS Lambda event sources such as JSON strings, base64, and uncompress gzip data. + +#### powertools_json function + +Use `powertools_json` function to decode any JSON string anywhere a JMESPath expression is allowed. + +> **Idempotency scenario** + +This sample will deserialize the JSON string within the `body` key before [Idempotency](./idempotency.md){target="_blank"} processes it. + +=== "Idempotency utility: WithEventKeyJmesPath" + + ```csharp hl_lines="4" + Idempotency.Configure(builder => + builder + .WithOptions(optionsBuilder => + optionsBuilder.WithEventKeyJmesPath("powertools_json(Body).[\"user_id\", \"product_id\"]")) + .UseDynamoDb("idempotency_table")); + ``` + +=== "Payload" + + ```json hl_lines="28" + { + "version": "2.0", + "routeKey": "ANY /createpayment", + "rawPath": "/createpayment", + "rawQueryString": "", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/createpayment", + "protocol": "HTTP/1.1", + "sourceIp": "ip", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "ANY /createpayment", + "stage": "$default", + "time": "10/Feb/2021:13:40:43 +0000", + "timeEpoch": 1612964443723 + }, + "body": "{\"user_id\":\"xyz\",\"product_id\":\"123456789\"}", + "isBase64Encoded": false + } + ``` + +#### powertools_base64 function + +Use `powertools_base64` function to decode any base64 data. + +This sample will decode the base64 value within the `data` key, and deserialize the JSON string before validation. + +=== "Function" + + ```csharp + var transformer = JsonTransformer.Parse("powertools_base64(body).customerId"); + using var result = transformer.Transform(doc.RootElement); + + Logger.LogInformation(result.RootElement.GetRawText()); // "dd4649e6-2484-4993-acb8-0f9123103394" + ``` + +=== "Payload" + + ```json + { + "body": "eyJjdXN0b21lcklkIjoiZGQ0NjQ5ZTYtMjQ4NC00OTkzLWFjYjgtMGY5MTIzMTAzMzk0In0=", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] + } + ``` + +#### powertools_base64_gzip function + +Use `powertools_base64_gzip` function to decompress and decode base64 data. + +This sample will decompress and decode base64 data from Cloudwatch Logs, then use JMESPath pipeline expression to pass the result for decoding its JSON string. + +=== "Function" + + ```csharp + var transformer = JsonTransformer.Parse("powertools_base64_gzip(body).customerId"); + using var result = transformer.Transform(doc.RootElement); + + Logger.LogInformation(result.RootElement.GetRawText()); // "dd4649e6-2484-4993-acb8-0f9123103394" + ``` + +=== "Payload" + + ```json + { + "body": "H4sIAAAAAAAAA6tWSi4tLsnPTS3yTFGyUkpJMTEzsUw10zUysTDRNbG0NNZNTE6y0DVIszQ0MjY0MDa2NFGqBQCMzDWgNQAAAA==", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] + } + ``` \ No newline at end of file diff --git a/examples/Idempotency/src/HelloWorld/HelloWorld.csproj b/examples/Idempotency/src/HelloWorld/HelloWorld.csproj index 9f776ce0..4a3f8ed3 100644 --- a/examples/Idempotency/src/HelloWorld/HelloWorld.csproj +++ b/examples/Idempotency/src/HelloWorld/HelloWorld.csproj @@ -8,7 +8,7 @@ - - + + diff --git a/examples/Logging/src/HelloWorld/HelloWorld.csproj b/examples/Logging/src/HelloWorld/HelloWorld.csproj index a970a2f0..be745788 100644 --- a/examples/Logging/src/HelloWorld/HelloWorld.csproj +++ b/examples/Logging/src/HelloWorld/HelloWorld.csproj @@ -8,7 +8,7 @@ - + diff --git a/examples/Metrics/src/HelloWorld/HelloWorld.csproj b/examples/Metrics/src/HelloWorld/HelloWorld.csproj index d6eee6b5..9116bbec 100644 --- a/examples/Metrics/src/HelloWorld/HelloWorld.csproj +++ b/examples/Metrics/src/HelloWorld/HelloWorld.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj index ce9c7cb4..100f2d2c 100644 --- a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj @@ -13,8 +13,8 @@ - - - + + + diff --git a/examples/Tracing/src/HelloWorld/HelloWorld.csproj b/examples/Tracing/src/HelloWorld/HelloWorld.csproj index e6bf4310..f63df00c 100644 --- a/examples/Tracing/src/HelloWorld/HelloWorld.csproj +++ b/examples/Tracing/src/HelloWorld/HelloWorld.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 62c1cec1..27ad89f2 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Param EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Parameters.Tests", "tests\AWS.Lambda.Powertools.Parameters.Tests\AWS.Lambda.Powertools.Parameters.Tests.csproj", "{386A9769-59BF-4BE3-99D4-A9603E300729}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.JMESPath", "src\AWS.Lambda.Powertools.JMESPath\AWS.Lambda.Powertools.JMESPath.csproj", "{4F5020DB-9856-4A6F-B2CB-2C213FD749BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.JMESPath.Tests", "tests\AWS.Lambda.Powertools.JMESPath.Tests\AWS.Lambda.Powertools.JMESPath.Tests.csproj", "{B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -216,6 +220,30 @@ Global {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x64.Build.0 = Release|Any CPU {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x86.ActiveCfg = Release|Any CPU {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x86.Build.0 = Release|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Debug|x64.Build.0 = Debug|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Debug|x86.Build.0 = Debug|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Release|Any CPU.Build.0 = Release|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Release|x64.ActiveCfg = Release|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Release|x64.Build.0 = Release|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Release|x86.ActiveCfg = Release|Any CPU + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC}.Release|x86.Build.0 = Release|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Debug|x64.Build.0 = Debug|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Debug|x86.Build.0 = Debug|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Release|x64.ActiveCfg = Release|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Release|x64.Build.0 = Release|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Release|x86.ActiveCfg = Release|Any CPU + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -233,5 +261,7 @@ Global {F8B4100F-4014-4A1E-8130-D281453B79ED} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} {12B940EF-A5D3-459D-BD36-A603834D1F7D} = {1CFF5568-8486-475F-81F6-06105C437528} {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3} = {1CFF5568-8486-475F-81F6-06105C437528} + {4F5020DB-9856-4A6F-B2CB-2C213FD749BC} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {B1A91FDB-A843-4CE5-A1AC-2ED48A158AA1} = {1CFF5568-8486-475F-81F6-06105C437528} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index d12fe902..50a99381 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -14,8 +14,8 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 27ae02f6..c71dc9d9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -21,8 +21,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; -using AWS.Lambda.Powertools.Idempotency.Serialization; -using DevLab.JmesPath; +using AWS.Lambda.Powertools.JMESPath; namespace AWS.Lambda.Powertools.Idempotency.Persistence; @@ -226,15 +225,13 @@ private DataRecord RetrieveFromCache(string idempotencyKey, DateTimeOffset now) { if (!_idempotencyOptions.UseLocalCache) return null; - - if (_cache.TryGet(idempotencyKey, out var record) && record!=null) + + if (!_cache.TryGet(idempotencyKey, out var record) || record == null) return null; + if (!record.IsExpired(now)) { - if (!record.IsExpired(now)) - { - return record; - } - DeleteFromCache(idempotencyKey); + return record; } + DeleteFromCache(idempotencyKey); return null; } @@ -262,15 +259,11 @@ private string GetHashedPayload(JsonDocument data) return ""; } - var jmes = new JmesPath(); - jmes.FunctionRepository.Register(); - var result = jmes.Transform(data.RootElement.ToString(), _idempotencyOptions.PayloadValidationJmesPath); - var node = JsonDocument.Parse(result); - return GenerateHash(node.RootElement); + var transformer = JsonTransformer.Parse(_idempotencyOptions.PayloadValidationJmesPath); + var result = transformer.Transform(data.RootElement); + return GenerateHash(result.RootElement); } - - /// /// Calculate unix timestamp of expiry date for idempotency record /// @@ -293,10 +286,9 @@ private string GetHashedIdempotencyKey(JsonDocument data) var eventKeyJmesPath = _idempotencyOptions.EventKeyJmesPath; if (eventKeyJmesPath != null) { - var jmes = new JmesPath(); - jmes.FunctionRepository.Register(); - var result = jmes.Transform(node.ToString(), eventKeyJmesPath); - node = JsonDocument.Parse(result).RootElement; + var transformer = JsonTransformer.Parse(eventKeyJmesPath); + var result = transformer.Transform(node); + node = result.RootElement; } if (IsMissingIdempotencyKey(node)) diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/AWS.Lambda.Powertools.JMESPath.csproj b/libraries/src/AWS.Lambda.Powertools.JMESPath/AWS.Lambda.Powertools.JMESPath.csproj new file mode 100644 index 00000000..f4ce628b --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/AWS.Lambda.Powertools.JMESPath.csproj @@ -0,0 +1,11 @@ + + + + + AWS.Lambda.Powertools.JMESPath + Powertools for AWS Lambda (.NET) - JMESPath package. + AWS.Lambda.Powertools.JMESPath + AWS.Lambda.Powertools.JMESPath + + + diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/BaseExpression.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/BaseExpression.cs new file mode 100644 index 00000000..843d6f8c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/BaseExpression.cs @@ -0,0 +1,55 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Base class for all expressions. +/// +internal abstract class BaseExpression : IExpression +{ + /// + public int PrecedenceLevel {get;} + + /// + public bool IsRightAssociative {get;} + + /// + public bool IsProjection {get;} + + private protected BaseExpression(Operator oper, bool isProjection) + { + PrecedenceLevel = OperatorTable.PrecedenceLevel(oper); + IsRightAssociative = OperatorTable.IsRightAssociative(oper); + IsProjection = isProjection; + } + + /// + public abstract bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value); + + /// + public virtual void AddExpression(IExpression expr) + { + } + + public override string ToString() + { + return "ToString not implemented"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/CurrentNode.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/CurrentNode.cs new file mode 100644 index 00000000..2566e00c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/CurrentNode.cs @@ -0,0 +1,43 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents the current node. +/// +internal sealed class CurrentNode : BaseExpression +{ + internal CurrentNode() + : base(Operator.Default, false) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + value = current; + return true; + } + + public override string ToString() + { + return "CurrentNode"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Expression.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Expression.cs new file mode 100644 index 00000000..7e0163dd --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Expression.cs @@ -0,0 +1,178 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions +{ + // BaseExpression + + /// + /// Represents a JMESPath expression. + /// + internal class Expression + { + /// + /// The tokens in the expression. + /// + private readonly Token[] _tokens; + + internal Expression(Token[] tokens) + { + _tokens = tokens; + } + + /// + /// Evaluates the expression against the given resources. + /// + public bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue result) + { + var stack = new Stack(); + IList argStack = new List(); + + var rootPtr = current; + + for (var i = _tokens.Length-1; i >= 0; --i) + { + var token = _tokens[i]; + switch (token.Type) + { + case TokenType.Literal: + { + stack.Push(token.GetValue()); + break; + } + case TokenType.BeginExpressionType: + { + Debug.Assert(i>0); + token = _tokens[--i]; + Debug.Assert(token.Type == TokenType.Expression); + Debug.Assert(stack.Count != 0); + stack.Pop(); + stack.Push(new ExpressionValue(token.GetExpression())); + break; + } + case TokenType.Pipe: + { + Debug.Assert(stack.Count != 0); + rootPtr = stack.Peek(); + break; + } + case TokenType.CurrentNode: + stack.Push(rootPtr); + break; + case TokenType.Expression: + { + Debug.Assert(stack.Count != 0); + var ptr = stack.Pop(); + if (!token.GetExpression().TryEvaluate(resources, ptr, out var val)) + { + result = JsonConstants.Null; + return false; + } + stack.Push(val); + break; + } + case TokenType.UnaryOperator: + { + Debug.Assert(stack.Count >= 1); + var rhs = stack.Pop(); + if (!token.GetUnaryOperator().TryEvaluate(rhs, out var val)) + { + result = JsonConstants.Null; + return false; + } + stack.Push(val); + break; + } + case TokenType.BinaryOperator: + { + Debug.Assert(stack.Count >= 2); + var rhs = stack.Pop(); + var lhs = stack.Pop(); + if (!token.GetBinaryOperator().TryEvaluate(lhs, rhs, out var val)) + { + result = JsonConstants.Null; + return false; + } + stack.Push(val); + break; + } + case TokenType.Argument: + { + Debug.Assert(stack.Count != 0); + argStack.Add(stack.Pop()); + break; + } + case TokenType.Function: + { + if (token.GetFunction().Arity != null && token.GetFunction().Arity != argStack.Count()) + { + // airty error should never happen here + result = JsonConstants.Null; + return false; + } + + if (!token.GetFunction().TryEvaluate(resources, argStack, out var val)) + { + result = JsonConstants.Null; + return false; + } + argStack.Clear(); + stack.Push(val); + break; + } + default: + break; + } + } + Debug.Assert(stack.Count == 1); + result = stack.Peek(); + return true; + } + + internal static bool IsFalse(IValue val) + { + switch (val.Type) + { + case JmesPathType.False: + return true; + case JmesPathType.Null: + return true; + case JmesPathType.Array: + return val.GetArrayLength() == 0; + case JmesPathType.Object: + return val.EnumerateObject().MoveNext() == false; + case JmesPathType.String: + return val.GetString().Length == 0; + case JmesPathType.Number: + return false; + default: + return false; + } + } + + internal static bool IsTrue(IValue val) + { + return !IsFalse(val); + } + } +} + diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FilterExpression.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FilterExpression.cs new file mode 100644 index 00000000..4389b588 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FilterExpression.cs @@ -0,0 +1,76 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a filter expression. +/// +internal sealed class FilterExpression : Projection +{ + /// + /// The expression to evaluate. + /// + private readonly Expression _expr; + + internal FilterExpression(Expression expr) + : base(Operator.Projection) + { + _expr = expr; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type != JmesPathType.Array) + { + value = JsonConstants.Null; + return true; + } + var result = new List(); + + foreach (var item in current.EnumerateArray()) + { + if (!_expr.TryEvaluate(resources, item, out var test)) + { + value = JsonConstants.Null; + return false; + } + + if (!Expression.IsTrue(test)) continue; + if (!TryApplyExpressions(resources, item, out var val)) + { + value = JsonConstants.Null; + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + value = new ArrayValue(result); + return true; + } + + public override string ToString() + { + return "FilterExpression"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FlattenProjection.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FlattenProjection.cs new file mode 100644 index 00000000..4c0746d1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FlattenProjection.cs @@ -0,0 +1,84 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents the flatten projection operator. +/// +internal sealed class FlattenProjection : Projection +{ + internal FlattenProjection() + : base(Operator.FlattenProjection) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type != JmesPathType.Array) + { + value = JsonConstants.Null; + return true; + } + + var result = new List(); + foreach (var item in current.EnumerateArray()) + { + if (item.Type == JmesPathType.Array) + { + foreach (var elem in item.EnumerateArray()) + { + if (elem.Type == JmesPathType.Null) continue; + if (!TryApplyExpressions(resources, elem, out var val)) + { + value = JsonConstants.Null; + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + } + else + { + if (item.Type == JmesPathType.Null) continue; + if (!TryApplyExpressions(resources, item, out var val)) + { + value = JsonConstants.Null; + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + } + + value = new ArrayValue(result); + return true; + } + + public override string ToString() + { + return "FlattenProjection"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FunctionExpression.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FunctionExpression.cs new file mode 100644 index 00000000..2627df88 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/FunctionExpression.cs @@ -0,0 +1,54 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a function expression. +/// +internal sealed class FunctionExpression : BaseExpression +{ + /// + /// The expression to evaluate. + /// + private readonly Expression _expr; + + internal FunctionExpression(Expression expr) + : base(Operator.Default, false) + { + _expr = expr; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (!_expr.TryEvaluate(resources, current, out var val)) + { + value = JsonConstants.Null; + return true; + } + value = val; + return true; + } + + public override string ToString() + { + return "FunctionExpression"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IExpression.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IExpression.cs new file mode 100644 index 00000000..011a6f4a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IExpression.cs @@ -0,0 +1,55 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a JMESPath expression. +/// +internal interface IExpression +{ + /// + /// Evaluates the expression against the provided resources. + /// + /// + /// + /// + /// + bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value); + + /// + /// The precedence level of the expression. + /// + int PrecedenceLevel {get;} + + /// + /// True if the expression is a projection, false otherwise. + /// + bool IsProjection {get;} + + /// + /// True if the expression is right-associative, false otherwise. + /// + bool IsRightAssociative {get;} + + /// + /// Adds an expression to the expression. + /// + void AddExpression(IExpression expr); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IdentifierSelector.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IdentifierSelector.cs new file mode 100644 index 00000000..4648c54d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IdentifierSelector.cs @@ -0,0 +1,54 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a JMESPath identifier expression. +/// +internal sealed class IdentifierSelector : BaseExpression +{ + /// + /// The identifier to select. + /// + private readonly string _identifier; + + internal IdentifierSelector(string name) + : base(Operator.Default, false) + { + _identifier = name; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type == JmesPathType.Object && current.TryGetProperty(_identifier, out value)) + { + return true; + } + + value = JsonConstants.Null; + return true; + } + + public override string ToString() + { + return $"IdentifierSelector {_identifier}"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IndexSelector.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IndexSelector.cs new file mode 100644 index 00000000..d6233ce9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/IndexSelector.cs @@ -0,0 +1,66 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a single index selector. +/// +internal sealed class IndexSelector : BaseExpression +{ + /// + /// The index of the selector. + /// + private readonly int _index; + internal IndexSelector(int index) + : base(Operator.Default, false) + { + _index = index; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type != JmesPathType.Array) + { + value = JsonConstants.Null; + return true; + } + var slen = current.GetArrayLength(); + if (_index >= 0 && _index < slen) + { + value = current[_index]; + } + else if ((slen + _index) >= 0 && (slen+_index) < slen) + { + var index = slen + _index; + value = current[index]; + } + else + { + value = JsonConstants.Null; + } + return true; + } + + public override string ToString() + { + return $"Index Selector {_index}"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/JsonConstants.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/JsonConstants.cs new file mode 100644 index 00000000..1d8e7006 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/JsonConstants.cs @@ -0,0 +1,35 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Constants used by the JMESPath parser. +/// +internal static class JsonConstants +{ + static JsonConstants() + { + True = new TrueValue(); + False = new FalseValue(); + Null = new NullValue(); + } + + internal static IValue True {get;} + internal static IValue False {get;} + internal static IValue Null {get;} +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/KeyExpressionPair.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/KeyExpressionPair.cs new file mode 100644 index 00000000..2cc65cdb --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/KeyExpressionPair.cs @@ -0,0 +1,31 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// A pair of a JMESPath key and an expression. +/// +internal struct KeyExpressionPair +{ + internal string Key {get;} + internal Expression Expression {get;} + + internal KeyExpressionPair(string key, Expression expression) + { + Key = key; + Expression = expression; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/ListProjection.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/ListProjection.cs new file mode 100644 index 00000000..a8f9bfc6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/ListProjection.cs @@ -0,0 +1,66 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a projection that returns a list of values. +/// +internal sealed class ListProjection : Projection +{ + internal ListProjection() + : base(Operator.Projection) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type != JmesPathType.Array) + { + value = JsonConstants.Null; + return true; + } + + var result = new List(); + foreach (var item in current.EnumerateArray()) + { + if (item.Type != JmesPathType.Null) + { + if (!TryApplyExpressions(resources, item, out var val)) + { + value = JsonConstants.Null; + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + } + value = new ArrayValue(result); + return true; + } + + public override string ToString() + { + return "ListProjection"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/MultiSelectHash.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/MultiSelectHash.cs new file mode 100644 index 00000000..454550c4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/MultiSelectHash.cs @@ -0,0 +1,66 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a multi-select hash expression. +/// +internal sealed class MultiSelectHash : BaseExpression +{ + /// + /// The list of key expression pairs. + /// + private readonly IList _keyExprPairs; + + internal MultiSelectHash(IList keyExprPairs) + : base(Operator.Default, false) + { + _keyExprPairs = keyExprPairs; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type == JmesPathType.Null) + { + value = JsonConstants.Null; + return true; + } + var result = new Dictionary(); + foreach (var item in _keyExprPairs) + { + if (!item.Expression.TryEvaluate(resources, current, out var val)) + { + value = JsonConstants.Null; + return false; + } + result.Add(item.Key, val); + } + + value = new ObjectValue(result); + return true; + } + + public override string ToString() + { + return "MultiSelectHash"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/MultiSelectList.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/MultiSelectList.cs new file mode 100644 index 00000000..4b5a577c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/MultiSelectList.cs @@ -0,0 +1,66 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a multi-select list expression. +/// +internal sealed class MultiSelectList : BaseExpression +{ + /// + /// The list of expressions to evaluate. + /// + private readonly IList _expressions; + + internal MultiSelectList(IList expressions) + : base(Operator.Default, false) + { + _expressions = expressions; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type == JmesPathType.Null) + { + value = JsonConstants.Null; + return true; + } + var result = new List(); + + foreach (var expr in _expressions) + { + if (!expr.TryEvaluate(resources, current, out var val)) + { + value = JsonConstants.Null; + return false; + } + result.Add(val); + } + value = new ArrayValue(result); + return true; + } + + public override string ToString() + { + return "MultiSelectList"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/ObjectProjection.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/ObjectProjection.cs new file mode 100644 index 00000000..64704b3a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/ObjectProjection.cs @@ -0,0 +1,63 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents the projection of an object. +/// +internal sealed class ObjectProjection : Projection +{ + internal ObjectProjection() + : base(Operator.Projection) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type != JmesPathType.Object) + { + value = JsonConstants.Null; + return true; + } + + var result = new List(); + value = new ArrayValue(result); + foreach (var item in current.EnumerateObject()) + { + if (item.Value.Type == JmesPathType.Null) continue; + if (!TryApplyExpressions(resources, item.Value, out var val)) + { + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + return true; + } + + public override string ToString() + { + return "ObjectProjection"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Projection.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Projection.cs new file mode 100644 index 00000000..d6a7e62c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Projection.cs @@ -0,0 +1,74 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Base class for projection expressions. +/// +internal abstract class Projection : BaseExpression +{ + /// + /// List of expressions to be applied to the current value. + /// + private readonly List _expressions; + + private protected Projection(Operator oper) + : base(oper, true) + { + _expressions = new List(); + } + + /// + /// Adds an expression to the list of expressions to be applied to the current value. + /// + /// + public override void AddExpression(IExpression expr) + { + if (_expressions.Count != 0 && _expressions[_expressions.Count-1].IsProjection && + (expr.PrecedenceLevel > _expressions[_expressions.Count-1].PrecedenceLevel || + (expr.PrecedenceLevel == _expressions[_expressions.Count-1].PrecedenceLevel && expr.IsRightAssociative))) + { + _expressions[_expressions.Count-1].AddExpression(expr); + } + else + { + _expressions.Add(expr); + } + } + + /// + /// Tries to apply the list of expressions to the current value. + /// + /// + /// + /// + /// + internal bool TryApplyExpressions(DynamicResources resources, IValue current, out IValue value) + { + value = current; + foreach (var expression in _expressions) + { + if (!expression.TryEvaluate(resources, value, out value)) + { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Slice.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Slice.cs new file mode 100644 index 00000000..c534c34c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/Slice.cs @@ -0,0 +1,70 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +namespace AWS.Lambda.Powertools.JMESPath.Expressions +{ + /// + /// A slice of a list or string. + /// + internal readonly struct Slice + { + /// + /// The start of the slice. + /// + private readonly int? _start; + /// + /// The stop of the slice. + /// + private readonly int? _stop; + + /// + /// The step of the slice. + /// + public int Step {get;} + + public Slice(int? start, int? stop, int step) + { + _start = start; + _stop = stop; + Step = step; + } + + /// + /// Gets the start of the slice. + /// + /// + /// + public int GetStart(int size) + { + // 2024-04-19: Powertools addition. + if (!_start.HasValue) return Step >= 0 ? 0 : size; + var len = _start.Value >= 0 ? _start.Value : size + _start.Value; + return len <= size ? len : size; + } + + /// + /// Gets the stop of the slice. + /// + /// + /// + public int GetStop(int size) + { + // 2024-04-19: Powertools addition. + if (!_stop.HasValue) return Step >= 0 ? size : -1; + var len = _stop.Value >= 0 ? _stop.Value : size + _stop.Value; + return len <= size ? len : size; + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/SliceProjection.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/SliceProjection.cs new file mode 100644 index 00000000..5250a2fd --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Expressions/SliceProjection.cs @@ -0,0 +1,114 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Expressions; + +/// +/// Represents a projection of a slice of an array. +/// +internal sealed class SliceProjection : Projection +{ + /// + /// The slice to project. + /// + private readonly Slice _slice; + + internal SliceProjection(Slice s) + : base(Operator.Projection) + { + _slice = s; + } + + /// + public override bool TryEvaluate(DynamicResources resources, + IValue current, + out IValue value) + { + if (current.Type != JmesPathType.Array) + { + value = JsonConstants.Null; + return true; + } + + var start = _slice.GetStart(current.GetArrayLength()); + var end = _slice.GetStop(current.GetArrayLength()); + var step = _slice.Step; + + if (step == 0) + { + value = JsonConstants.Null; + return false; + } + + var result = new List(); + if (step > 0) + { + if (start < 0) + { + start = 0; + } + if (end > current.GetArrayLength()) + { + end = current.GetArrayLength(); + } + for (var i = start; i < end; i += step) + { + if (!TryApplyExpressions(resources, current[i], out var val)) + { + value = JsonConstants.Null; + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + } + else + { + if (start >= current.GetArrayLength()) + { + start = current.GetArrayLength() - 1; + } + if (end < -1) + { + end = -1; + } + for (var i = start; i > end; i += step) + { + if (!TryApplyExpressions(resources, current[i], out var val)) + { + value = JsonConstants.Null; + return false; + } + if (val.Type != JmesPathType.Null) + { + result.Add(val); + } + } + } + + value = new ArrayValue(result); + return true; + } + + public override string ToString() + { + return "SliceProjection"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/AbsFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/AbsFunction.cs new file mode 100644 index 00000000..d1b9da51 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/AbsFunction.cs @@ -0,0 +1,60 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the absolute value of a number. +/// +internal sealed class AbsFunction : BaseFunction +{ + internal AbsFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg = args[0]; + + if (arg.TryGetDecimal(out var decVal)) + { + element = new DecimalValue(decVal >= 0 ? decVal : -decVal); + return true; + } + + if (arg.TryGetDouble(out var dblVal)) + { + element = new DecimalValue(dblVal >= 0 ? decVal : new decimal(-dblVal)); + return true; + } + + element = JsonConstants.Null; + return false; + } + + public override string ToString() + { + return "abs"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/AvgFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/AvgFunction.cs new file mode 100644 index 00000000..3820b089 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/AvgFunction.cs @@ -0,0 +1,71 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the average of the values +/// +internal sealed class AvgFunction : BaseFunction +{ + internal AvgFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Array || arg0.GetArrayLength() == 0) + { + element = JsonConstants.Null; + return false; + } + + if (!SumFunction.Instance.TryEvaluate(resources, args, out var sum)) + { + element = JsonConstants.Null; + return false; + } + + if (sum.TryGetDecimal(out var decVal)) + { + element = new DecimalValue(decVal / arg0.GetArrayLength()); + return true; + } + + if (sum.TryGetDouble(out var dblVal)) + { + element = new DoubleValue(dblVal / arg0.GetArrayLength()); + return true; + } + + element = JsonConstants.Null; + return false; + } + + public override string ToString() + { + return "avg"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/Base64Function.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/Base64Function.cs new file mode 100644 index 00000000..589600a4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/Base64Function.cs @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the Base64 encoded value of a string. powertools_base64 +/// +internal sealed class Base64Function : BaseFunction +{ + /// + public Base64Function() + : base(1) + { + } + + public override string ToString() + { + return "powertools_base64"; + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + var base64StringBytes = Convert.FromBase64String(args[0].GetString()); + var doc = JsonDocument.Parse(base64StringBytes); + element = new JsonElementValue(doc.RootElement); + return true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/Base64GzipFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/Base64GzipFunction.cs new file mode 100644 index 00000000..18617695 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/Base64GzipFunction.cs @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Decodes a base64-encoded, gzip-compressed string into a JSON document. +/// +internal sealed class Base64GzipFunction : BaseFunction +{ + /// + public Base64GzipFunction() + : base(1) + { + } + + public override string ToString() + { + return "powertools_base64_gzip"; + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var compressedBytes = Convert.FromBase64String(args[0].GetString()); + + using var compressedStream = new MemoryStream(compressedBytes); + using var decompressedStream = new MemoryStream(); + using (var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress)) + { + gzipStream.CopyTo(decompressedStream); + } + + var doc = JsonDocument.Parse(Encoding.UTF8.GetString(decompressedStream.ToArray())); + element = new JsonElementValue(doc.RootElement); + + return true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/BaseFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/BaseFunction.cs new file mode 100644 index 00000000..5168da76 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/BaseFunction.cs @@ -0,0 +1,36 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Base class for JMESPath functions. +/// +internal abstract class BaseFunction : IFunction +{ + private protected BaseFunction(int? argCount) + { + Arity = argCount; + } + + /// + public int? Arity { get; } + + /// + public abstract bool TryEvaluate(DynamicResources resources, IList args, out IValue element); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/BuiltInFunctions.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/BuiltInFunctions.cs new file mode 100644 index 00000000..c0035054 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/BuiltInFunctions.cs @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// A registry of built-in functions. +/// +internal sealed class BuiltInFunctions +{ + internal static BuiltInFunctions Instance { get; } = new(); + + private readonly Dictionary _functions = new(); + + private BuiltInFunctions() + { + _functions.Add("abs", new AbsFunction()); + _functions.Add("avg", new AvgFunction()); + _functions.Add("ceil", new CeilFunction()); + _functions.Add("contains", new ContainsFunction()); + _functions.Add("ends_with", new EndsWithFunction()); + _functions.Add("floor", new FloorFunction()); + _functions.Add("join", new JoinFunction()); + _functions.Add("keys", new KeysFunction()); + _functions.Add("length", new LengthFunction()); + _functions.Add("map", new MapFunction()); + _functions.Add("max", new MaxFunction()); + _functions.Add("max_by", new MaxByFunction()); + _functions.Add("merge", new MergeFunction()); + _functions.Add("min", new MinFunction()); + _functions.Add("min_by", new MinByFunction()); + _functions.Add("not_null", new NotNullFunction()); + _functions.Add("reverse", new ReverseFunction()); + _functions.Add("sort", new SortFunction()); + _functions.Add("sort_by", new SortByFunction()); + _functions.Add("starts_with", new StartsWithFunction()); + _functions.Add("sum", new SumFunction()); + _functions.Add("to_array", new ToArrayFunction()); + _functions.Add("to_number", new ToNumberFunction()); + _functions.Add("to_string", new ToStringFunction()); + _functions.Add("type", new TypeFunction()); + _functions.Add("values", new ValuesFunction()); + _functions.Add("powertools_json", new JsonFunction()); + _functions.Add("powertools_base64", new Base64Function()); + _functions.Add("powertools_base64_gzip", new Base64GzipFunction()); + } + + internal bool TryGetFunction(string name, out IFunction func) + { + return _functions.TryGetValue(name, out func); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/CeilFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/CeilFunction.cs new file mode 100644 index 00000000..5d3cbb3c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/CeilFunction.cs @@ -0,0 +1,67 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the smallest integer greater than or equal to the argument. +/// +internal sealed class CeilFunction : BaseFunction +{ + internal CeilFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var val = args[0]; + if (val.Type != JmesPathType.Number) + { + element = JsonConstants.Null; + return false; + } + + if (val.TryGetDecimal(out var decVal)) + { + element = new DecimalValue(decimal.Ceiling(decVal)); + return true; + } + + if (val.TryGetDouble(out var dblVal)) + { + element = new DoubleValue(Math.Ceiling(dblVal)); + return true; + } + + element = JsonConstants.Null; + return false; + } + + public override string ToString() + { + return "ceil"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ContainsFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ContainsFunction.cs new file mode 100644 index 00000000..94f41326 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ContainsFunction.cs @@ -0,0 +1,87 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns true if the first argument contains the second argument. +/// +internal sealed class ContainsFunction : BaseFunction +{ + internal ContainsFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + var arg1 = args[1]; + + var comparer = ValueEqualityComparer.Instance; + + switch (arg0.Type) + { + case JmesPathType.Array: + if (arg0.EnumerateArray().Any(item => comparer.Equals(item, arg1))) + { + element = JsonConstants.True; + return true; + } + + element = JsonConstants.False; + return true; + case JmesPathType.String: + { + if (arg1.Type != JmesPathType.String) + { + element = JsonConstants.Null; + return false; + } + + var s0 = arg0.GetString(); + var s1 = arg1.GetString(); + if (s0.Contains(s1)) + { + element = JsonConstants.True; + return true; + } + + element = JsonConstants.False; + return true; + } + default: + { + element = JsonConstants.Null; + return false; + } + } + } + + public override string ToString() + { + return "contains"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EndsWithFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EndsWithFunction.cs new file mode 100644 index 00000000..738a29f0 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EndsWithFunction.cs @@ -0,0 +1,46 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns true if the first argument ends with the second argument. +/// +internal sealed class EndsWithFunction : BaseFunction +{ + internal EndsWithFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + // 2024-04-19: Powertools addition. + return EvaluateStartEndWith.TryEvaluate(args, out element, s0 => s0.EndsWith); + } + + public override string ToString() + { + return "ends_with"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateMinMax.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateMinMax.cs new file mode 100644 index 00000000..a7fa04e5 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateMinMax.cs @@ -0,0 +1,83 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Evaluates the min and max functions. +/// +internal static class EvaluateMinMax +{ + /// + /// Evaluates the min and max functions. + /// + /// + /// + /// + /// + internal static bool TryEvaluate(IList args, IBinaryOperator binaryOperator, out IValue element) + { + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Array) + { + element = JsonConstants.Null; + return false; + } + + if (arg0.GetArrayLength() == 0) + { + element = JsonConstants.Null; + return false; + } + + var isNumber = arg0[0].Type == JmesPathType.Number; + var isString = arg0[0].Type == JmesPathType.String; + if (!isNumber && !isString) + { + element = JsonConstants.Null; + return false; + } + + var index = 0; + for (var i = 1; i < arg0.GetArrayLength(); ++i) + { + if (!(arg0[i].Type == JmesPathType.Number == isNumber && + arg0[i].Type == JmesPathType.String == isString)) + { + element = JsonConstants.Null; + return false; + } + + if (!binaryOperator.TryEvaluate(arg0[i], arg0[index], out var value)) + { + element = JsonConstants.Null; + return false; + } + + if (Expression.IsTrue(value)) + { + index = i; + } + } + + element = arg0[index]; + return true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateMinMaxBy.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateMinMaxBy.cs new file mode 100644 index 00000000..dc04356a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateMinMaxBy.cs @@ -0,0 +1,98 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Evaluates the min and max functions by resources. +/// +internal static class EvaluateMinMaxBy +{ + /// + /// Evaluates the min and max function by resources. + /// + /// + /// + /// + /// + /// + internal static bool TryEvaluate(DynamicResources resources, IList args, IBinaryOperator binaryOperator, out IValue element) + { + if (!(args[0].Type == JmesPathType.Array && args[1].Type == JmesPathType.Expression)) + { + element = JsonConstants.Null; + return false; + } + + var arg0 = args[0]; + if (arg0.GetArrayLength() == 0) + { + element = JsonConstants.Null; + return true; + } + + var expr = args[1].GetExpression(); + + if (!expr.TryEvaluate(resources, arg0[0], out var key1)) + { + element = JsonConstants.Null; + return false; + } + + var isNumber1 = key1.Type == JmesPathType.Number; + var isString1 = key1.Type == JmesPathType.String; + if (!(isNumber1 || isString1)) + { + element = JsonConstants.Null; + return false; + } + + var index = 0; + for (var i = 1; i < arg0.GetArrayLength(); ++i) + { + if (!expr.TryEvaluate(resources, arg0[i], out var key2)) + { + element = JsonConstants.Null; + return false; + } + + var isNumber2 = key2.Type == JmesPathType.Number; + var isString2 = key2.Type == JmesPathType.String; + if (!(isNumber2 == isNumber1 && isString2 == isString1)) + { + element = JsonConstants.Null; + return false; + } + + if (!binaryOperator.TryEvaluate(key2, key1, out var value)) + { + element = JsonConstants.Null; + return false; + } + + if (value.Type != JmesPathType.True) continue; + key1 = key2; + index = i; + } + + element = arg0[index]; + return true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateStartEndWith.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateStartEndWith.cs new file mode 100644 index 00000000..c2a2d50c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/EvaluateStartEndWith.cs @@ -0,0 +1,54 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Evaluates true if the first string starts with the second string. +/// Evaluates true if the first string ends with the second string. +/// +internal static class EvaluateStartEndWith +{ + /// + /// Evaluates true if the first string starts with the second string. + /// Evaluates true if the first string ends with the second string. + /// + /// + /// + /// + /// + internal static bool TryEvaluate(IList args, out IValue element, Func> method) + { + var arg0 = args[0]; + var arg1 = args[1]; + if (arg0.Type != JmesPathType.String + || arg1.Type != JmesPathType.String) + { + element = JsonConstants.Null; + return false; + } + + var s0 = arg0.GetString(); + var s1 = arg1.GetString(); + element = method(s0)(s1) ? JsonConstants.True : JsonConstants.False; + + return true; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/FloorFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/FloorFunction.cs new file mode 100644 index 00000000..22c59813 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/FloorFunction.cs @@ -0,0 +1,67 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the largest integer less than or equal to the argument. +/// +internal sealed class FloorFunction : BaseFunction +{ + internal FloorFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var val = args[0]; + if (val.Type != JmesPathType.Number) + { + element = JsonConstants.Null; + return false; + } + + if (val.TryGetDecimal(out var decVal)) + { + element = new DecimalValue(decimal.Floor(decVal)); + return true; + } + + if (val.TryGetDouble(out var dblVal)) + { + element = new DoubleValue(Math.Floor(dblVal)); + return true; + } + + element = JsonConstants.Null; + return false; + } + + public override string ToString() + { + return "floor"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/IFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/IFunction.cs new file mode 100644 index 00000000..16e1b852 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/IFunction.cs @@ -0,0 +1,35 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Represents a JMESPath function. +/// +internal interface IFunction +{ + /// + /// The number of arguments the function takes. + /// + int? Arity { get; } + + /// + /// Evaluates the function. + /// + bool TryEvaluate(DynamicResources resources, IList args, out IValue element); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/JoinFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/JoinFunction.cs new file mode 100644 index 00000000..a30671db --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/JoinFunction.cs @@ -0,0 +1,75 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns a string consisting of a list of strings joined by a separator. +/// +internal sealed class JoinFunction : BaseFunction +{ + internal JoinFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + var arg1 = args[1]; + + if (!(arg0.Type == JmesPathType.String && args[1].Type == JmesPathType.Array)) + { + element = JsonConstants.Null; + return false; + } + + var sep = arg0.GetString(); + var buf = new StringBuilder(); + foreach (var j in arg1.EnumerateArray()) + { + if (j.Type != JmesPathType.String) + { + element = JsonConstants.Null; + return false; + } + + if (buf.Length != 0) + { + buf.Append(sep); + } + + var sv = j.GetString(); + buf.Append(sv); + } + + element = new StringValue(buf.ToString()); + return true; + } + + public override string ToString() + { + return "join"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/JsonFunction.cs similarity index 55% rename from libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs rename to libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/JsonFunction.cs index 94ed3f25..a1381cc6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/JsonFunction.cs @@ -13,30 +13,33 @@ * permissions and limitations under the License. */ +using System.Collections.Generic; using System.Diagnostics; -using DevLab.JmesPath.Functions; -using Newtonsoft.Json.Linq; +using AWS.Lambda.Powertools.JMESPath.Values; -namespace AWS.Lambda.Powertools.Idempotency.Serialization; +namespace AWS.Lambda.Powertools.JMESPath.Functions; /// -/// Creates JMESPath function powertools_json() to treat the payload as a JSON object rather than a string. +/// Returns the JSON representation of a value. /// -public class JsonFunction : JmesPathFunction +internal sealed class JsonFunction : BaseFunction { /// public JsonFunction() - : base("powertools_json", 1) + : base(1) { } + public override string ToString() + { + return "powertools_json"; + } + /// - public override JToken Execute(params JmesPathFunctionArgument[] args) + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) { - Debug.Assert(args.Length == 1); - Debug.Assert(args[0].IsToken); - var argument = args[0]; - var token = argument.Token; - return JToken.Parse(token.ToString()); + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + element = args[0]; + return true; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/KeysFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/KeysFunction.cs new file mode 100644 index 00000000..f3136ef6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/KeysFunction.cs @@ -0,0 +1,61 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the keys of the object as an array. +/// +internal sealed class KeysFunction : BaseFunction +{ + internal KeysFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Object) + { + element = JsonConstants.Null; + return false; + } + + var values = new List(); + + foreach (var property in arg0.EnumerateObject()) + { + values.Add(new StringValue(property.Name)); + } + + element = new ArrayValue(values); + return true; + } + + public override string ToString() + { + return "keys"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/LengthFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/LengthFunction.cs new file mode 100644 index 00000000..fb6ef5e4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/LengthFunction.cs @@ -0,0 +1,76 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the number of elements in a value. +/// +internal sealed class LengthFunction : BaseFunction +{ + internal LengthFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + + switch (arg0.Type) + { + case JmesPathType.Object: + { + var count = 0; + foreach (var unused in arg0.EnumerateObject()) + { + ++count; + } + + element = new DecimalValue(new decimal(count)); + return true; + } + case JmesPathType.Array: + element = new DecimalValue(new decimal(arg0.GetArrayLength())); + return true; + case JmesPathType.String: + { + var bytes = Encoding.UTF32.GetBytes(arg0.GetString().ToCharArray()); + element = new DecimalValue(new decimal(bytes.Length / 4)); + return true; + } + default: + { + element = JsonConstants.Null; + return false; + } + } + } + + public override string ToString() + { + return "length"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MapFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MapFunction.cs new file mode 100644 index 00000000..13c6ce67 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MapFunction.cs @@ -0,0 +1,68 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns a new array with the results of calling a provided function on every element in the calling array. +/// +internal sealed class MapFunction : BaseFunction +{ + internal MapFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + if (!(args[0].Type == JmesPathType.Expression && args[1].Type == JmesPathType.Array)) + { + element = JsonConstants.Null; + return false; + } + + var expr = args[0].GetExpression(); + var arg0 = args[1]; + + var list = new List(); + + foreach (var item in arg0.EnumerateArray()) + { + if (!expr.TryEvaluate(resources, item, out var val)) + { + element = JsonConstants.Null; + return false; + } + + list.Add(val); + } + + element = new ArrayValue(list); + return true; + } + + public override string ToString() + { + return "map"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MaxByFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MaxByFunction.cs new file mode 100644 index 00000000..7cd67189 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MaxByFunction.cs @@ -0,0 +1,46 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the maximum value of the expression by resource. +/// +internal sealed class MaxByFunction : BaseFunction +{ + internal MaxByFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + // 2024-04-19: Powertools addition. + return EvaluateMinMaxBy.TryEvaluate(resources, args, GtOperator.Instance, out element); + } + + public override string ToString() + { + return "max_by"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MaxFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MaxFunction.cs new file mode 100644 index 00000000..a5325d97 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MaxFunction.cs @@ -0,0 +1,47 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the maximum value in a list. +/// +internal sealed class MaxFunction : BaseFunction +{ + internal MaxFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + // 2024-04-19: Powertools addition. + return EvaluateMinMax.TryEvaluate(args, GtOperator.Instance, out element); + } + + public override string ToString() + { + return "max"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MergeFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MergeFunction.cs new file mode 100644 index 00000000..29afd5d6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MergeFunction.cs @@ -0,0 +1,80 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Linq; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Merges multiple objects into a single object. +/// +internal sealed class MergeFunction : BaseFunction +{ + internal MergeFunction() + : base(null) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + if (!args.Any()) + { + element = JsonConstants.Null; + return false; + } + + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Object) + { + element = JsonConstants.Null; + return false; + } + + if (args.Count == 1) + { + element = arg0; + return true; + } + + var dict = new Dictionary(); + foreach (var argi in args) + { + if (argi.Type != JmesPathType.Object) + { + element = JsonConstants.Null; + return false; + } + + foreach (var item in argi.EnumerateObject()) + { + if (dict.TryAdd(item.Name, item.Value)) continue; + dict.Remove(item.Name); + dict.Add(item.Name, item.Value); + } + } + + element = new ObjectValue(dict); + return true; + } + + public override string ToString() + { + return "merge"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MinByFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MinByFunction.cs new file mode 100644 index 00000000..f98def98 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MinByFunction.cs @@ -0,0 +1,46 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the element with the minimum value by resource +/// +internal sealed class MinByFunction : BaseFunction +{ + internal MinByFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + // 2024-04-19: Powertools addition. + return EvaluateMinMaxBy.TryEvaluate(resources, args, LtOperator.Instance, out element); + } + + public override string ToString() + { + return "min_by"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MinFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MinFunction.cs new file mode 100644 index 00000000..e686fb5a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/MinFunction.cs @@ -0,0 +1,47 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the minimum value in a element. +/// +internal sealed class MinFunction : BaseFunction +{ + internal MinFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + // 2024-04-19: Powertools addition. + return EvaluateMinMax.TryEvaluate(args, LtOperator.Instance, out element); + } + + public override string ToString() + { + return "min"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/NotNullFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/NotNullFunction.cs new file mode 100644 index 00000000..bc8b86cf --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/NotNullFunction.cs @@ -0,0 +1,50 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the first non-null argument. +/// +internal sealed class NotNullFunction : BaseFunction +{ + internal NotNullFunction() + : base(null) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + foreach (var arg in args) + { + if (arg.Type == JmesPathType.Null) continue; + element = arg; + return true; + } + + element = JsonConstants.Null; + return true; + } + + public override string ToString() + { + return "not_null"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ReverseFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ReverseFunction.cs new file mode 100644 index 00000000..70a33ff8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ReverseFunction.cs @@ -0,0 +1,78 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the elements of the input array in reverse order. +/// +internal sealed class ReverseFunction : BaseFunction +{ + internal ReverseFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + switch (arg0.Type) + { + case JmesPathType.String: + { + element = new StringValue(string.Join("", GraphemeClusters(arg0.GetString()).Reverse().ToArray())); + return true; + } + case JmesPathType.Array: + { + var list = new List(); + for (var i = arg0.GetArrayLength() - 1; i >= 0; --i) + { + list.Add(arg0[i]); + } + + element = new ArrayValue(list); + return true; + } + default: + element = JsonConstants.Null; + return false; + } + } + + private static IEnumerable GraphemeClusters(string s) + { + var enumerator = StringInfo.GetTextElementEnumerator(s); + while (enumerator.MoveNext()) + { + yield return (string)enumerator.Current; + } + } + + public override string ToString() + { + return "reverse"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortByComparer.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortByComparer.cs new file mode 100644 index 00000000..a307ae06 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortByComparer.cs @@ -0,0 +1,80 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions +{ + /// + /// Implements the sort_by function. + /// + internal sealed class SortByComparer : IComparer, System.Collections.IComparer + { + private readonly DynamicResources _resources; + private readonly IExpression _expr; + + internal bool IsValid { get; private set; } = true; + + internal SortByComparer(DynamicResources resources, + IExpression expr) + { + _resources = resources; + _expr = expr; + } + + public int Compare(IValue lhs, IValue rhs) + { + var comparer = ValueComparer.Instance; + + if (!IsValid) + { + return 0; + } + + if (!_expr.TryEvaluate(_resources, lhs, out var key1)) + { + IsValid = false; + return 0; + } + + var isNumber1 = key1.Type == JmesPathType.Number; + var isString1 = key1.Type == JmesPathType.String; + if (!(isNumber1 || isString1)) + { + IsValid = false; + return 0; + } + + if (!_expr.TryEvaluate(_resources, rhs, out var key2)) + { + IsValid = false; + return 0; + } + + var isNumber2 = key2.Type == JmesPathType.Number; + var isString2 = key2.Type == JmesPathType.String; + if (isNumber2 == isNumber1 && isString2 == isString1) return comparer.Compare(key1, key2); + IsValid = false; + return 0; + } + + int System.Collections.IComparer.Compare(object x, object y) + { + return Compare((IValue)x, (IValue)y); + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortByFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortByFunction.cs new file mode 100644 index 00000000..5ca2e64a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortByFunction.cs @@ -0,0 +1,75 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the input array sorted by the value of the expression by resources. +/// +internal sealed class SortByFunction : BaseFunction +{ + internal SortByFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + if (!(args[0].Type == JmesPathType.Array && args[1].Type == JmesPathType.Expression)) + { + element = JsonConstants.Null; + return false; + } + + var arg0 = args[0]; + if (arg0.GetArrayLength() <= 1) + { + element = arg0; + return true; + } + + var expr = args[1].GetExpression(); + + var list = new List(); + foreach (var item in arg0.EnumerateArray()) + { + list.Add(item); + } + + var comparer = new SortByComparer(resources, expr); + list.Sort(comparer); + if (comparer.IsValid) + { + element = new ArrayValue(list); + return true; + } + + element = JsonConstants.Null; + return false; + } + + public override string ToString() + { + return "sort_by"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortFunction.cs new file mode 100644 index 00000000..5c489eac --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SortFunction.cs @@ -0,0 +1,84 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the sorted elements of the input array. +/// +internal sealed class SortFunction : BaseFunction +{ + internal SortFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Array) + { + element = JsonConstants.Null; + return false; + } + + if (arg0.GetArrayLength() <= 1) + { + element = arg0; + return true; + } + + var isNumber1 = arg0[0].Type == JmesPathType.Number; + var isString1 = arg0[0].Type == JmesPathType.String; + if (!isNumber1 && !isString1) + { + element = JsonConstants.Null; + return false; + } + + var comparer = ValueComparer.Instance; + + var list = new List(); + foreach (var item in arg0.EnumerateArray()) + { + var isNumber2 = item.Type == JmesPathType.Number; + var isString2 = item.Type == JmesPathType.String; + if (!(isNumber2 == isNumber1 && isString2 == isString1)) + { + element = JsonConstants.Null; + return false; + } + + list.Add(item); + } + + list.Sort(comparer); + element = new ArrayValue(list); + return true; + } + + public override string ToString() + { + return "sort"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/StartsWithFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/StartsWithFunction.cs new file mode 100644 index 00000000..d2b15033 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/StartsWithFunction.cs @@ -0,0 +1,46 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns true if the first argument starts with the second argument. +/// +internal sealed class StartsWithFunction : BaseFunction +{ + internal StartsWithFunction() + : base(2) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + // 2024-04-19: Powertools addition. + return EvaluateStartEndWith.TryEvaluate(args, out element, s0 => s0.StartsWith); + } + + public override string ToString() + { + return "starts_with"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SumFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SumFunction.cs new file mode 100644 index 00000000..f88327ba --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/SumFunction.cs @@ -0,0 +1,94 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the sum of the values in a list. +/// +internal sealed class SumFunction : BaseFunction +{ + internal static SumFunction Instance { get; } = new(); + + internal SumFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Array) + { + element = JsonConstants.Null; + return false; + } + + foreach (var item in arg0.EnumerateArray()) + { + if (item.Type == JmesPathType.Number) continue; + element = JsonConstants.Null; + return false; + } + + var success = true; + decimal decSum = 0; + foreach (var item in arg0.EnumerateArray()) + { + if (!item.TryGetDecimal(out var dec)) + { + success = false; + break; + } + + decSum += dec; + } + + if (success) + { + element = new DecimalValue(decSum); + return true; + } + + double dblSum = 0; + foreach (var item in arg0.EnumerateArray()) + { + if (!item.TryGetDouble(out var dbl)) + { + element = JsonConstants.Null; + return false; + } + + dblSum += dbl; + } + + element = new DoubleValue(dblSum); + return true; + } + + public override string ToString() + { + return "sum"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToArrayFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToArrayFunction.cs new file mode 100644 index 00000000..5832a208 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToArrayFunction.cs @@ -0,0 +1,53 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the input as an array if it is not already an array. +/// +internal sealed class ToArrayFunction : BaseFunction +{ + internal ToArrayFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + if (arg0.Type == JmesPathType.Array) + { + element = arg0; + return true; + } + + var list = new List { arg0 }; + element = new ArrayValue(list); + return true; + } + + public override string ToString() + { + return "to_array"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToNumberFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToNumberFunction.cs new file mode 100644 index 00000000..b5389cf4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToNumberFunction.cs @@ -0,0 +1,74 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Converts a string to a number. +/// +internal sealed class ToNumberFunction : BaseFunction +{ + internal ToNumberFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, + out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + switch (arg0.Type) + { + case JmesPathType.Number: + element = arg0; + return true; + case JmesPathType.String: + { + var s = arg0.GetString(); + if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var dec)) + { + element = new DecimalValue(dec); + return true; + } + + if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var dbl)) + { + element = new DoubleValue(dbl); + return true; + } + + element = JsonConstants.Null; + return false; + } + default: + element = JsonConstants.Null; + return false; + } + } + + public override string ToString() + { + return "to_number"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToStringFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToStringFunction.cs new file mode 100644 index 00000000..bd179c3d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ToStringFunction.cs @@ -0,0 +1,63 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the string representation of a value. +/// +internal sealed class ToStringFunction : BaseFunction +{ + internal ToStringFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + if (args[0].Type == JmesPathType.Expression) + { + element = JsonConstants.Null; + return false; + } + + var arg0 = args[0]; + switch (arg0.Type) + { + case JmesPathType.String: + element = arg0; + return true; + case JmesPathType.Expression: + element = JsonConstants.Null; + return false; + default: + element = new StringValue(arg0.ToString()); + return true; + } + } + + public override string ToString() + { + return "to_string"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/TypeFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/TypeFunction.cs new file mode 100644 index 00000000..fbf3f399 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/TypeFunction.cs @@ -0,0 +1,71 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the type of the value as a string. +/// +internal sealed class TypeFunction : BaseFunction +{ + internal TypeFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + + switch (arg0.Type) + { + case JmesPathType.Number: + element = new StringValue("number"); + return true; + case JmesPathType.True: + case JmesPathType.False: + element = new StringValue("boolean"); + return true; + case JmesPathType.String: + element = new StringValue("string"); + return true; + case JmesPathType.Object: + element = new StringValue("object"); + return true; + case JmesPathType.Array: + element = new StringValue("array"); + return true; + case JmesPathType.Null: + element = new StringValue("null"); + return true; + default: + element = JsonConstants.Null; + return false; + } + } + + public override string ToString() + { + return "type"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ValuesFunction.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ValuesFunction.cs new file mode 100644 index 00000000..263201f6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Functions/ValuesFunction.cs @@ -0,0 +1,60 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Functions; + +/// +/// Returns the values of an object. +/// +internal sealed class ValuesFunction : BaseFunction +{ + internal ValuesFunction() + : base(1) + { + } + + /// + public override bool TryEvaluate(DynamicResources resources, IList args, out IValue element) + { + Debug.Assert(Arity.HasValue && args.Count == Arity!.Value); + + var arg0 = args[0]; + if (arg0.Type != JmesPathType.Object) + { + element = JsonConstants.Null; + return false; + } + + var list = new List(); + + foreach (var item in arg0.EnumerateObject()) + { + list.Add(item.Value); + } + + element = new ArrayValue(list); + return true; + } + + public override string ToString() + { + return "values"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/InternalsVisibleTo.cs new file mode 100644 index 00000000..89436184 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/InternalsVisibleTo.cs @@ -0,0 +1,18 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.JMESPath.Tests")] \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathParseException.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathParseException.cs new file mode 100644 index 00000000..1cd7bfa4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathParseException.cs @@ -0,0 +1,51 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; + +namespace AWS.Lambda.Powertools.JMESPath; + +/// +/// Defines a custom exception object that is thrown when JMESPath parsing fails. +/// + +public sealed class JmesPathParseException : Exception +{ + /// + /// The line in the JMESPath string where a parse error was detected. + /// + private int LineNumber {get;} + + /// + /// The column in the JMESPath string where a parse error was detected. + /// + private int ColumnNumber {get;} + + internal JmesPathParseException(string message, int line, int column) + : base(message) + { + LineNumber = line; + ColumnNumber = column; + } + + /// + /// Returns an error message that describes the current exception. + /// + /// A string representation of the current exception. + public override string ToString () + { + return $"{Message} at line {LineNumber} and column {ColumnNumber}"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathParser.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathParser.cs new file mode 100644 index 00000000..dd6e004f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathParser.cs @@ -0,0 +1,1679 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Functions; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath +{ + /// + /// Parses a JMESPath expression and returns a . + /// + internal ref struct JmesPathParser + { + private readonly ReadOnlySpan _span; + private int _index; + private int _column; + private int _line; + private readonly Stack _stateStack; + private readonly Stack_outputStack; + private readonly Stack_operatorStack; + + internal JmesPathParser(string input) + { + _span = input.AsSpan(); + _index = 0; + _column = 1; + _line = 1; + _stateStack = new Stack(); + _outputStack = new Stack(); + _operatorStack = new Stack(); + } + + /// + /// Parses a JMESPath expression and returns a . + /// + internal JsonTransformer Parse() + { + _stateStack.Clear(); + _outputStack.Clear(); + _operatorStack.Clear(); + _index = 0; + _line = 1; + _column = 1; + + var buffer = new StringBuilder(); + int? sliceStart = null; + int? sliceStop = null; + var sliceStep = 1; + uint cp = 0; + uint cp2 = 0; + + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Push(JmesPathState.Start); + + // 2024-04-19: Powertools addition. + var syntaxErrorMsg = "Syntax error"; + while (_index < _span.Length) + { + // 2024-04-19: Powertools addition. + var expectedRightBracket = "Expected right bracket"; + switch (_stateStack.Peek()) + { + case JmesPathState.Start: + { + _stateStack.Pop(); + _stateStack.Push(JmesPathState.RhsExpression); + _stateStack.Push(JmesPathState.LhsExpression); + break; + } + case JmesPathState.RhsExpression: + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '.': + ++_index; + ++_column; + _stateStack.Push(JmesPathState.SubExpression); + break; + case '|': + ++_index; + ++_column; + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.ExpectPipeOrOr); + break; + case '&': + ++_index; + ++_column; + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.ExpectAnd); + break; + case '<': + case '>': + case '=': + { + _stateStack.Push(JmesPathState.ComparatorExpression); + break; + } + case '!': + { + ++_index; + ++_column; + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.CmpNe); + break; + } + case ')': + { + _stateStack.Pop(); + break; + } + case '[': + _stateStack.Push(JmesPathState.BracketSpecifier); + ++_index; + ++_column; + break; + default: + if (_stateStack.Count > 1) + { + _stateStack.Pop(); + } + else + { + throw new JmesPathParseException(syntaxErrorMsg, _line, _column); + } + break; + } + break; + case JmesPathState.ComparatorExpression: + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '<': + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.CmpLtOrLte); + break; + case '>': + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.CmpGtOrGte); + break; + case '=': + { + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.CmpEq); + break; + } + default: + if (_stateStack.Count > 1) + { + _stateStack.Pop(); + } + else + { + throw new JmesPathParseException(syntaxErrorMsg, _line, _column); + } + break; + } + break; + case JmesPathState.LhsExpression: + { + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '\"': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ValExpr); + _stateStack.Push(JmesPathState.QuotedString); + ++_index; + ++_column; + break; + case '\'': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.RawString); + ++_index; + ++_column; + break; + case '`': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.Literal); + ++_index; + ++_column; + break; + case '{': + PushToken(new Token(TokenType.BeginMultiSelectHash)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.MultiSelectHash); + ++_index; + ++_column; + break; + case '*': // wildcard + PushToken(new Token(new ObjectProjection())); + _stateStack.Pop(); + ++_index; + ++_column; + break; + case '(': + { + ++_index; + ++_column; + PushToken(new Token(TokenType.LeftParen)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectRightParen); + _stateStack.Push(JmesPathState.RhsExpression); + _stateStack.Push(JmesPathState.LhsExpression); + break; + } + case '!': + { + ++_index; + ++_column; + PushToken(new Token(NotOperator.Instance)); + break; + } + case '@': + ++_index; + ++_column; + PushToken(new Token(new CurrentNode())); + _stateStack.Pop(); + break; + case '[': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.BracketSpecifierOrMultiSelectList); + ++_index; + ++_column; + break; + default: + if ((_span[_index] >= 'A' && _span[_index] <= 'Z') || (_span[_index] >= 'a' && _span[_index] <= 'z') || (_span[_index] == '_')) + { + _stateStack.Pop(); + _stateStack.Push(JmesPathState.IdentifierOrFunctionExpr); + _stateStack.Push(JmesPathState.UnquotedString); + buffer.Append(_span[_index]); + ++_index; + ++_column; + } + else + { + throw new JmesPathParseException("Expected identifier", _line, _column); + } + break; + } + break; + } + + case JmesPathState.SubExpression: + { + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '\"': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ValExpr); + _stateStack.Push(JmesPathState.QuotedString); + ++_index; + ++_column; + break; + case '{': + PushToken(new Token(TokenType.BeginMultiSelectHash)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.MultiSelectHash); + ++_index; + ++_column; + break; + case '*': + PushToken(new Token(new ObjectProjection())); + _stateStack.Pop(); + ++_index; + ++_column; + break; + case '[': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectMultiSelectList); + ++_index; + ++_column; + break; + default: + if ((_span[_index] >= 'A' && _span[_index] <= 'Z') || (_span[_index] >= 'a' && _span[_index] <= 'z') || (_span[_index] == '_')) + { + _stateStack.Pop(); + _stateStack.Push(JmesPathState.IdentifierOrFunctionExpr); + _stateStack.Push(JmesPathState.UnquotedString); + buffer.Append(_span[_index]); + ++_index; + ++_column; + } + else + { + throw new JmesPathParseException("Expected identifier", _line, _column); + } + break; + } + break; + } + case JmesPathState.KeyExpr: + PushToken(new Token(TokenType.Key, buffer.ToString())); + buffer.Clear(); + _stateStack.Pop(); + break; + case JmesPathState.ValExpr: + PushToken(new Token(new IdentifierSelector(buffer.ToString()))); + buffer.Clear(); + _stateStack.Pop(); + break; + case JmesPathState.ExpressionOrExpressionType: + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '&': + PushToken(new Token(TokenType.BeginExpressionType)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpressionType); + _stateStack.Push(JmesPathState.RhsExpression); + _stateStack.Push(JmesPathState.LhsExpression); + ++_index; + ++_column; + break; + default: + _stateStack.Pop(); + _stateStack.Push(JmesPathState.Argument); + _stateStack.Push(JmesPathState.RhsExpression); + _stateStack.Push(JmesPathState.LhsExpression); + break; + } + break; + + case JmesPathState.IdentifierOrFunctionExpr: + switch(_span[_index]) + { + case '(': + { + var functionName = buffer.ToString(); + if (!BuiltInFunctions.Instance.TryGetFunction(functionName, out var func)) + { + throw new JmesPathParseException($"Function '{functionName}' not found", _line, _column); + } + buffer.Clear(); + PushToken(new Token(func)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.FunctionExpression); + _stateStack.Push(JmesPathState.ExpressionOrExpressionType); + ++_index; + ++_column; + break; + } + default: + { + PushToken(new Token(new IdentifierSelector(buffer.ToString()))); + buffer.Clear(); + _stateStack.Pop(); + break; + } + } + break; + + case JmesPathState.FunctionExpression: + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case ',': + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Push(JmesPathState.ExpressionOrExpressionType); + ++_index; + ++_column; + break; + case ')': + { + PushToken(new Token(TokenType.EndArguments)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + } + } + break; + + case JmesPathState.Argument: + PushToken(new Token(TokenType.Argument)); + _stateStack.Pop(); + break; + + case JmesPathState.ExpressionType: + PushToken(new Token(TokenType.EndExpressionType)); + PushToken(new Token(TokenType.Argument)); + _stateStack.Pop(); + break; + + case JmesPathState.QuotedString: + switch (_span[_index]) + { + case '\"': + _stateStack.Pop(); // quotedString + ++_index; + ++_column; + break; + case '\\': + _stateStack.Push(JmesPathState.QuotedStringEscapeChar); + ++_index; + ++_column; + break; + default: + buffer.Append(_span[_index]); + ++_index; + ++_column; + break; + } + break; + + case JmesPathState.UnquotedString: + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + _stateStack.Pop(); // unquotedString + SkipWhiteSpace(); + break; + default: + if ((_span[_index] >= '0' && _span[_index] <= '9') || (_span[_index] >= 'A' && _span[_index] <= 'Z') || (_span[_index] >= 'a' && _span[_index] <= 'z') || (_span[_index] == '_')) + { + buffer.Append(_span[_index]); + ++_index; + ++_column; + } + else + { + _stateStack.Pop(); // unquotedString + } + break; + } + break; + + case JmesPathState.RawStringEscapeChar: + switch (_span[_index]) + { + case '\'': + buffer.Append(_span[_index]); + _stateStack.Pop(); + ++_index; + ++_column; + break; + default: + buffer.Append('\\'); + buffer.Append(_span[_index]); + _stateStack.Pop(); + ++_index; + ++_column; + break; + } + break; + + case JmesPathState.QuotedStringEscapeChar: + switch (_span[_index]) + { + case '\"': + buffer.Append('\"'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case '\\': + buffer.Append('\\'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case '/': + buffer.Append('/'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case 'b': + buffer.Append('\b'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case 'f': + buffer.Append('\f'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case 'n': + buffer.Append('\n'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case 'r': + buffer.Append('\r'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case 't': + buffer.Append('\t'); + ++_index; + ++_column; + _stateStack.Pop(); + break; + case 'u': + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU1); + break; + default: + throw new JmesPathParseException("Illegal escaped character", _line, _column); + } + break; + + case JmesPathState.EscapeU1: + cp = AppendToCodepoint(0, _span[_index]); + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU2); + break; + case JmesPathState.EscapeU2: + cp = AppendToCodepoint(cp, _span[_index]); + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU3); + break; + case JmesPathState.EscapeU3: + cp = AppendToCodepoint(cp, _span[_index]); + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU4); + break; + case JmesPathState.EscapeU4: + cp = AppendToCodepoint(cp, _span[_index]); + if (char.IsHighSurrogate((char)cp)) + { + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeExpectSurrogatePair1); + } + else + { + buffer.Append(char.ConvertFromUtf32((int)cp)); + ++_index; + ++_column; + _stateStack.Pop(); + } + break; + case JmesPathState.EscapeExpectSurrogatePair1: + switch (_span[_index]) + { + case '\\': + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeExpectSurrogatePair2); + break; + default: + throw new JmesPathParseException("Invalid codepoint", _line, _column); + } + break; + case JmesPathState.EscapeExpectSurrogatePair2: + switch (_span[_index]) + { + case 'u': + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU5); + break; + default: + throw new JmesPathParseException("Invalid codepoint", _line, _column); + } + break; + case JmesPathState.EscapeU5: + cp2 = AppendToCodepoint(0, _span[_index]); + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU6); + break; + case JmesPathState.EscapeU6: + cp2 = AppendToCodepoint(cp2, _span[_index]); + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU7); + break; + case JmesPathState.EscapeU7: + cp2 = AppendToCodepoint(cp2, _span[_index]); + ++_index; + ++_column; + _stateStack.Pop(); + _stateStack.Push(JmesPathState.EscapeU8); + break; + case JmesPathState.EscapeU8: + { + cp2 = AppendToCodepoint(cp2, _span[_index]); + var codepoint = 0x10000 + ((cp & 0x3FF) << 10) + (cp2 & 0x3FF); + buffer.Append(char.ConvertFromUtf32((int)codepoint)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + } + + case JmesPathState.RawString: + switch (_span[_index]) + { + case '\'': + { + PushToken(new Token(new StringValue(buffer.ToString()))); + buffer.Clear(); + _stateStack.Pop(); // rawString + ++_index; + ++_column; + break; + } + case '\\': + _stateStack.Push(JmesPathState.RawStringEscapeChar); + ++_index; + ++_column; + break; + default: + buffer.Append(_span[_index]); + ++_index; + ++_column; + break; + } + break; + + case JmesPathState.Literal: + switch (_span[_index]) + { + case '`': + { + try + { + using (var doc = JsonDocument.Parse(buffer.ToString())) + { + PushToken(new Token(new JsonElementValue(doc.RootElement.Clone()))); + buffer.Clear(); + _stateStack.Pop(); + ++_index; + } + } + catch (JsonException) + { + throw new JmesPathParseException("Invalid JSON literal", _line, _column); + } + break; + } + case '\\': + if (_index + 1 < _span.Length) + { + ++_index; + ++_column; + if (_span[_index] != '`') + { + buffer.Append('\\'); + } + buffer.Append(_span[_index]); + } + else + { + throw new JmesPathParseException("Unexpected end of input", _line, _column); + } + ++_index; + ++_column; + break; + default: + buffer.Append(_span[_index]); + ++_index; + ++_column; + break; + } + break; + + case JmesPathState.Number: + switch(_span[_index]) + { + case '-': + buffer.Append(_span[_index]); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.Digit); + ++_index; + ++_column; + break; + default: + _stateStack.Pop(); + _stateStack.Push(JmesPathState.Digit); + break; + } + break; + case JmesPathState.Digit: + switch(_span[_index]) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': + buffer.Append(_span[_index]); + ++_index; + ++_column; + break; + default: + _stateStack.Pop(); // digit + break; + } + break; + + case JmesPathState.BracketSpecifier: + switch(_span[_index]) + { + case '*': + PushToken(new Token(new ListProjection())); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectRightBracket); + ++_index; + ++_column; + break; + case ']': // [] + PushToken(new Token(new FlattenProjection())); + _stateStack.Pop(); // bracketSpecifier + ++_index; + ++_column; + break; + case '?': + PushToken(new Token(TokenType.BeginFilter)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.Filter); + _stateStack.Push(JmesPathState.RhsExpression); + _stateStack.Push(JmesPathState.LhsExpression); + ++_index; + ++_column; + break; + case ':': // sliceExpression + _stateStack.Pop(); + _stateStack.Push(JmesPathState.RhsSliceExpressionStop); + _stateStack.Push(JmesPathState.Number); + ++_index; + ++_column; + break; + // number + case '-':case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.IndexOrSliceExpression); + _stateStack.Push(JmesPathState.Number); + break; + default: + throw new JmesPathParseException("Expected index expression", _line, _column); + } + break; + case JmesPathState.BracketSpecifierOrMultiSelectList: + switch(_span[_index]) + { + case '*': + if (_index+1 >= _span.Length) + { + throw new JmesPathParseException("Unexpected end of input", _line, _column); + } + if (_span[_index+1] == ']') + { + _stateStack.Pop(); + _stateStack.Push(JmesPathState.BracketSpecifier); + } + else + { + PushToken(new Token(TokenType.BeginMultiSelectList)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.MultiSelectList); + _stateStack.Push(JmesPathState.LhsExpression); + } + break; + case ']': // [] + case '?': + case ':': // sliceExpression + case '-':case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.BracketSpecifier); + break; + default: + PushToken(new Token(TokenType.BeginMultiSelectList)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.MultiSelectList); + _stateStack.Push(JmesPathState.LhsExpression); + break; + } + break; + + case JmesPathState.ExpectMultiSelectList: + switch(_span[_index]) + { + case ']': + case '?': + case ':': + case '-':case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': + throw new JmesPathParseException("Expected MultiSelectList", _line, _column); + case '*': + PushToken(new Token(new ListProjection())); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectRightBracket); + ++_index; + ++_column; + break; + default: + PushToken(new Token(TokenType.BeginMultiSelectList)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.MultiSelectList); + _stateStack.Push(JmesPathState.LhsExpression); + break; + } + break; + + case JmesPathState.MultiSelectHash: + switch(_span[_index]) + { + case '*': + case ']': + case '?': + case ':': + case '-':case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': + break; + default: + _stateStack.Pop(); + _stateStack.Push(JmesPathState.KeyValExpr); + break; + } + break; + + case JmesPathState.IndexOrSliceExpression: + switch(_span[_index]) + { + case ']': + { + if (buffer.Length == 0) + { + PushToken(new Token(new FlattenProjection())); + } + else + { + if (!int.TryParse(buffer.ToString(), out var n)) + { + throw new JmesPathParseException("Invalid number", _line, _column); + } + PushToken(new Token(new IndexSelector(n))); + buffer.Clear(); + } + _stateStack.Pop(); // bracketSpecifier + ++_index; + ++_column; + break; + } + case ':': + { + var s = buffer.ToString(); + if (!int.TryParse(s, out var n)) + { + n = s.StartsWith('-') ? int.MinValue : int.MaxValue; + } + sliceStart = n; + buffer.Clear(); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.RhsSliceExpressionStop); + _stateStack.Push(JmesPathState.Number); + ++_index; + ++_column; + break; + } + default: + throw new JmesPathParseException(expectedRightBracket, _line, _column); + } + break; + case JmesPathState.RhsSliceExpressionStop : + { + if (buffer.Length != 0) + { + var s = buffer.ToString(); + if (!int.TryParse(s, out var n)) + { + n = s.StartsWith('-') ? int.MinValue : int.MaxValue; + } + sliceStop = n; + buffer.Clear(); + } + switch(_span[_index]) + { + case ']': + PushToken(new Token(new SliceProjection(new Slice(sliceStart,sliceStop,sliceStep)))); + sliceStart = null; + sliceStop = null; + sliceStep = 1; + _stateStack.Pop(); // bracketSpecifier2 + ++_index; + ++_column; + break; + case ':': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.RhsSliceExpressionStep); + _stateStack.Push(JmesPathState.Number); + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException(expectedRightBracket, _line, _column); + } + break; + } + case JmesPathState.RhsSliceExpressionStep: + { + if (buffer.Length != 0) + { + if (!int.TryParse(buffer.ToString(), out var n)) + { + throw new JmesPathParseException("Invalid slice stop", _line, _column); + } + buffer.Clear(); + if (n == 0) + { + throw new JmesPathParseException("Slice step cannot be zero", _line, _column); + } + sliceStep = n; + buffer.Clear(); + } + switch(_span[_index]) + { + case ']': + PushToken(new Token(new SliceProjection(new Slice(sliceStart,sliceStop,sliceStep)))); + sliceStart = null; + sliceStop = null; + sliceStep = 1; + buffer.Clear(); + _stateStack.Pop(); // rhsSliceExpressionStep + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException(expectedRightBracket, _line, _column); + } + break; + } + case JmesPathState.ExpectRightBracket: + { + switch(_span[_index]) + { + case ']': + _stateStack.Pop(); // expectRightBracket + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException(expectedRightBracket, _line, _column); + } + break; + } + case JmesPathState.ExpectRightParen: + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case ')': + ++_index; + ++_column; + PushToken(new Token(TokenType.RightParen)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.RhsExpression); + break; + default: + throw new JmesPathParseException("Expected right parenthesis", _line, _column); + } + break; + case JmesPathState.KeyValExpr: + { + switch (_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '\"': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectColon); + _stateStack.Push(JmesPathState.KeyExpr); + _stateStack.Push(JmesPathState.QuotedString); + ++_index; + ++_column; + break; + case '\'': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectColon); + _stateStack.Push(JmesPathState.RawString); + ++_index; + ++_column; + break; + default: + if ((_span[_index] >= 'A' && _span[_index] <= 'Z') || (_span[_index] >= 'a' && _span[_index] <= 'z') || (_span[_index] == '_')) + { + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectColon); + _stateStack.Push(JmesPathState.KeyExpr); + _stateStack.Push(JmesPathState.UnquotedString); + buffer.Append(_span[_index]); + ++_index; + ++_column; + } + else + { + throw new JmesPathParseException("Expected key", _line, _column); + } + break; + } + break; + } + case JmesPathState.CmpLtOrLte: + { + switch(_span[_index]) + { + case '=': + PushToken(new Token(LteOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + default: + PushToken(new Token(LtOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + break; + } + break; + } + case JmesPathState.CmpGtOrGte: + { + switch(_span[_index]) + { + case '=': + PushToken(new Token(GteOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + default: + PushToken(new Token(GtOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + break; + } + break; + } + case JmesPathState.CmpEq: + { + switch(_span[_index]) + { + case '=': + PushToken(new Token(EqOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException("Expected comparator", _line, _column); + } + break; + } + case JmesPathState.CmpNe: + { + switch(_span[_index]) + { + case '=': + PushToken(new Token(NeOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException("Expected comparator", _line, _column); + } + break; + } + case JmesPathState.ExpectDot: + { + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case '.': + _stateStack.Pop(); // expect_dot + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException("Expected dot", _line, _column); + } + break; + } + case JmesPathState.ExpectPipeOrOr: + { + switch(_span[_index]) + { + case '|': + PushToken(new Token(OrOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + default: + PushToken(new Token(TokenType.Pipe)); + _stateStack.Pop(); + break; + } + break; + } + case JmesPathState.ExpectAnd: + { + switch(_span[_index]) + { + case '&': + PushToken(new Token(AndOperator.Instance)); + PushToken(new Token(TokenType.CurrentNode)); + _stateStack.Pop(); // expectAnd + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException("Expected &&", _line, _column); + } + break; + } + case JmesPathState.MultiSelectList: + { + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case ',': + PushToken(new Token(TokenType.Separator)); + _stateStack.Push(JmesPathState.LhsExpression); + ++_index; + ++_column; + break; + case '[': + _stateStack.Push(JmesPathState.LhsExpression); + break; + case '.': + _stateStack.Push(JmesPathState.SubExpression); + ++_index; + ++_column; + break; + case '|': + { + ++_index; + ++_column; + _stateStack.Push(JmesPathState.LhsExpression); + _stateStack.Push(JmesPathState.ExpectPipeOrOr); + break; + } + case ']': + { + PushToken(new Token(TokenType.EndMultiSelectList)); + _stateStack.Pop(); + + ++_index; + ++_column; + break; + } + default: + throw new JmesPathParseException(expectedRightBracket, _line, _column); + } + break; + } + case JmesPathState.Filter: + { + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case ']': + { + PushToken(new Token(TokenType.EndFilter)); + _stateStack.Pop(); + ++_index; + ++_column; + break; + } + default: + throw new JmesPathParseException(expectedRightBracket, _line, _column); + } + break; + } + case JmesPathState.ExpectRightBrace: + { + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case ',': + PushToken(new Token(TokenType.Separator)); + _stateStack.Pop(); + _stateStack.Push(JmesPathState.KeyValExpr); + ++_index; + ++_column; + break; + case '[': + case '{': + _stateStack.Push(JmesPathState.LhsExpression); + break; + case '.': + _stateStack.Push(JmesPathState.SubExpression); + ++_index; + ++_column; + break; + case '}': + { + _stateStack.Pop(); + PushToken(new Token(TokenType.EndMultiSelectHash)); + ++_index; + ++_column; + break; + } + default: + throw new JmesPathParseException("Expected right brace", _line, _column); + } + break; + } + case JmesPathState.ExpectColon: + { + switch(_span[_index]) + { + case ' ':case '\t':case '\r':case '\n': + SkipWhiteSpace(); + break; + case ':': + _stateStack.Pop(); + _stateStack.Push(JmesPathState.ExpectRightBrace); + _stateStack.Push(JmesPathState.LhsExpression); + ++_index; + ++_column; + break; + default: + throw new JmesPathParseException("Expected colon", _line, _column); + } + break; + } + } + } + + if (_stateStack.Count == 0) + { + throw new JmesPathParseException(syntaxErrorMsg, _line, _column); + } + while (_stateStack.Count > 1) + { + switch (_stateStack.Peek()) + { + case JmesPathState.RhsExpression: + if (_stateStack.Count > 1) + { + _stateStack.Pop(); + } + else + { + throw new JmesPathParseException(syntaxErrorMsg, _line, _column); + } + break; + case JmesPathState.ValExpr: + case JmesPathState.IdentifierOrFunctionExpr: + PushToken(new Token(new IdentifierSelector(buffer.ToString()))); + _stateStack.Pop(); + break; + case JmesPathState.UnquotedString: + _stateStack.Pop(); + break; + default: + throw new JmesPathParseException(syntaxErrorMsg, _line, _column); + } + } + + if (!(_stateStack.Count == 1 && _stateStack.Peek() == JmesPathState.RhsExpression)) + { + throw new JmesPathParseException("Unexpected end of input", _line, _column); + } + + _stateStack.Pop(); + + PushToken(new Token(TokenType.EndOfExpression)); + + var a = _outputStack.ToArray(); + + return new JsonTransformer(new Expression(a)); + } + + private void SkipWhiteSpace() + { + switch (_span[_index]) + { + case ' ':case '\t': + ++_index; + ++_column; + break; + case '\r': + if (_index+1 < _span.Length && _span[_index+1] == '\n') + ++_index; + ++_line; + _column = 1; + ++_index; + break; + case '\n': + ++_line; + _column = 1; + ++_index; + break; + } + } + + private void UnwindRightParen() + { + while (_operatorStack.Count > 1 && _operatorStack.Peek().Type != TokenType.LeftParen) + { + _outputStack.Push(_operatorStack.Pop()); + } + if (_operatorStack.Count == 0) + { + throw new JmesPathParseException("Unbalanced parentheses", _line, _column); + } + _operatorStack.Pop(); // TokenType.LeftParen + } + + /// + /// Pushes a token onto the output stack. + /// + /// + /// + private void PushToken(Token token) + { + switch (token.Type) + { + case TokenType.EndFilter: + { + UnwindRightParen(); + var tokens = new List(); + while (_outputStack.Count > 1 && _outputStack.Peek().Type != TokenType.BeginFilter) + { + tokens.Add(_outputStack.Pop()); + } + if (_outputStack.Count == 0) + { + throw new JmesPathParseException("Unbalanced parentheses", _line, _column); + } + if (tokens[tokens.Count-1].Type != TokenType.Literal) + { + tokens.Add(new Token(TokenType.CurrentNode)); + } + _outputStack.Pop(); + + if (_outputStack.Count != 0 && _outputStack.Peek().IsProjection && + (token.PrecedenceLevel > _outputStack.Peek().PrecedenceLevel || + (token.PrecedenceLevel == _outputStack.Peek().PrecedenceLevel && token.IsRightAssociative))) + { + _outputStack.Peek().GetExpression().AddExpression(new FilterExpression(new Expression(tokens.ToArray()))); + } + else + { + _outputStack.Push(new Token(new FilterExpression(new Expression(tokens.ToArray())))); + } + break; + } + case TokenType.EndMultiSelectList: + { + UnwindRightParen(); + var expressions = new List(); + while (_outputStack.Count > 0 && _outputStack.Peek().Type != TokenType.BeginMultiSelectList) + { + var tokens = new List(); + do + { + tokens.Add(_outputStack.Pop()); + } + while (_outputStack.Count > 0 && _outputStack.Peek().Type != TokenType.BeginMultiSelectList && _outputStack.Peek().Type != TokenType.Separator); + if (_outputStack.Peek().Type == TokenType.Separator) + { + _outputStack.Pop(); + } + if (tokens[tokens.Count-1].Type != TokenType.Literal) + { + tokens.Add(new Token(TokenType.CurrentNode)); + } + expressions.Add(new Expression(tokens.ToArray())); + } + if (_outputStack.Count == 0) + { + throw new JmesPathParseException("Unbalanced braces", _line, _column); + } + _outputStack.Pop(); // TokenType.BeginMultiSelectList + expressions.Reverse(); + + if (_outputStack.Count != 0 && _outputStack.Peek().IsProjection && + (token.PrecedenceLevel > _outputStack.Peek().PrecedenceLevel || + (token.PrecedenceLevel == _outputStack.Peek().PrecedenceLevel && token.IsRightAssociative))) + { + _outputStack.Peek().GetExpression().AddExpression(new MultiSelectList(expressions)); + } + else + { + _outputStack.Push(new Token(new MultiSelectList(expressions))); + } + break; + } + + case TokenType.EndMultiSelectHash: + { + UnwindRightParen(); + var keyExprPairs = new List(); + while (_outputStack.Count > 1 && _outputStack.Peek().Type != TokenType.BeginMultiSelectHash) + { + var tokens = new List(); + do + { + tokens.Add(_outputStack.Pop()); + } + while (_outputStack.Peek().Type != TokenType.Key); + if (_outputStack.Peek().Type != TokenType.Key) + { + throw new JmesPathParseException("Syntax error", _line, _column); + } + var key = _outputStack.Pop().GetKey(); + if (_outputStack.Peek().Type == TokenType.Separator) + { + _outputStack.Pop(); + } + if (tokens[tokens.Count-1].Type != TokenType.Literal) + { + tokens.Add(new Token(TokenType.CurrentNode)); + } + keyExprPairs.Add(new KeyExpressionPair(key, new Expression(tokens.ToArray()))); + } + if (_outputStack.Count == 0) + { + throw new JmesPathParseException("Syntax error", _line, _column); + } + keyExprPairs.Reverse(); + _outputStack.Pop(); // TokenType.BeginMultiSelectHash + + if (_outputStack.Count != 0 && _outputStack.Peek().IsProjection && + (token.PrecedenceLevel > _outputStack.Peek().PrecedenceLevel || + (token.PrecedenceLevel == _outputStack.Peek().PrecedenceLevel && token.IsRightAssociative))) + { + _outputStack.Peek().GetExpression().AddExpression(new MultiSelectHash(keyExprPairs)); + } + else + { + _outputStack.Push(new Token(new MultiSelectHash(keyExprPairs))); + } + break; + } + case TokenType.EndExpressionType: + { + var tokens = new List(); + while (_outputStack.Count > 1 && _outputStack.Peek().Type != TokenType.BeginExpressionType) + { + tokens.Add(_outputStack.Pop()); + } + if (_outputStack.Count == 0) + { + throw new JmesPathParseException("Unbalanced braces", _line, _column); + } + if (tokens[tokens.Count-1].Type != TokenType.Literal) + { + tokens.Add(new Token(TokenType.CurrentNode)); + } + _outputStack.Push(new Token(new FunctionExpression(new Expression(tokens.ToArray())))); + break; + } + case TokenType.Literal: + if (_outputStack.Count != 0 && _outputStack.Peek().Type == TokenType.CurrentNode) + { + _outputStack.Pop(); + _outputStack.Push(token); + } + else + { + _outputStack.Push(token); + } + break; + case TokenType.Expression: + if (_outputStack.Count != 0 && _outputStack.Peek().IsProjection && + (token.PrecedenceLevel > _outputStack.Peek().PrecedenceLevel || + (token.PrecedenceLevel == _outputStack.Peek().PrecedenceLevel && token.IsRightAssociative))) + { + _outputStack.Peek().GetExpression().AddExpression(token.GetExpression()); + } + else + { + _outputStack.Push(token); + } + break; + case TokenType.RightParen: + { + UnwindRightParen(); + break; + } + case TokenType.EndArguments: + { + UnwindRightParen(); + var argCount = 0; + var tokens = new List(); + Debug.Assert(_operatorStack.Count > 0 && _operatorStack.Peek().Type == TokenType.Function); + tokens.Add(_operatorStack.Pop()); // Function + while (_outputStack.Count > 1 && _outputStack.Peek().Type != TokenType.BeginArguments) + { + if (_outputStack.Peek().Type == TokenType.Argument) + { + ++argCount; + } + tokens.Add(_outputStack.Pop()); + } + if (_outputStack.Count == 0) + { + throw new JmesPathParseException("Expected parentheses", _line, _column); + } + _outputStack.Pop(); // TokenType.BeginArguments + if (tokens[tokens.Count-1].Type != TokenType.Literal) + { + tokens.Add(new Token(TokenType.CurrentNode)); + } + if (tokens[0].GetFunction().Arity != null && argCount != tokens[0].GetFunction().Arity) + { + throw new JmesPathParseException($"Invalid arity (The number of arguments or operands a function or operation takes) calling function '{tokens[0].GetFunction()}', expected {tokens[0].GetFunction().Arity}, found {argCount}", _line, _column); + } + + if (_outputStack.Count != 0 && _outputStack.Peek().IsProjection && + (token.PrecedenceLevel > _outputStack.Peek().PrecedenceLevel || + (token.PrecedenceLevel == _outputStack.Peek().PrecedenceLevel && token.IsRightAssociative))) + { + _outputStack.Peek().GetExpression().AddExpression(new FunctionExpression(new Expression(tokens.ToArray()))); + } + else + { + _outputStack.Push(new Token(new FunctionExpression(new Expression(tokens.ToArray())))); + } + break; + } + case TokenType.EndOfExpression: + { + while (_operatorStack.Count != 0) + { + _outputStack.Push(_operatorStack.Pop()); + } + break; + } + case TokenType.UnaryOperator: + case TokenType.BinaryOperator: + { + if (_operatorStack.Count == 0 || _operatorStack.Peek().Type == TokenType.LeftParen) + { + _operatorStack.Push(token); + } + else if (token.PrecedenceLevel > _operatorStack.Peek().PrecedenceLevel + || (token.PrecedenceLevel == _operatorStack.Peek().PrecedenceLevel && token.IsRightAssociative)) + { + _operatorStack.Push(token); + } + else + { + while (_operatorStack.Count > 0 && _operatorStack.Peek().IsOperator + && (_operatorStack.Peek().PrecedenceLevel > token.PrecedenceLevel + || (token.PrecedenceLevel == _operatorStack.Peek().PrecedenceLevel && token.IsRightAssociative))) + { + _outputStack.Push(_operatorStack.Pop()); + } + + _operatorStack.Push(token); + } + break; + } + case TokenType.Separator: + { + UnwindRightParen(); + _outputStack.Push(token); + _operatorStack.Push(new Token(TokenType.LeftParen)); + break; + } + case TokenType.BeginMultiSelectHash: + case TokenType.BeginMultiSelectList: + case TokenType.BeginFilter: + _outputStack.Push(token); + _operatorStack.Push(new Token(TokenType.LeftParen)); + break; + case TokenType.Function: + _outputStack.Push(new Token(TokenType.BeginArguments)); + _operatorStack.Push(token); + _operatorStack.Push(new Token(TokenType.LeftParen)); + break; + case TokenType.CurrentNode: + case TokenType.Key: + case TokenType.Pipe: + case TokenType.Argument: + case TokenType.BeginExpressionType: + _outputStack.Push(token); + break; + case TokenType.LeftParen: + _operatorStack.Push(token); + break; + } + } + + /// + /// Appends the given codepoint to the current codepoint. + /// + /// + /// + /// + /// + private uint AppendToCodepoint(uint cp, uint c) + { + // 2024-04-19: Powertools addition. + cp *= 16; + switch (c) + { + case >= '0' and <= '9': + cp += c - '0'; + break; + case >= 'a' and <= 'f': + cp += c - 'a' + 10; + break; + case >= 'A' and <= 'F': + cp += c - 'A' + 10; + break; + default: + throw new JmesPathParseException("Invalid codepoint", _line, _column); + } + return cp; + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathState.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathState.cs new file mode 100644 index 00000000..eb595e93 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/JmesPathState.cs @@ -0,0 +1,74 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +namespace AWS.Lambda.Powertools.JMESPath; + +/// +/// The state of the JMESPath parser. +/// +internal enum JmesPathState +{ + Start, + LhsExpression, + RhsExpression, + SubExpression, + ExpressionType, + ComparatorExpression, + FunctionExpression, + Argument, + ExpressionOrExpressionType, + QuotedString, + RawString, + RawStringEscapeChar, + QuotedStringEscapeChar, + EscapeU1, + EscapeU2, + EscapeU3, + EscapeU4, + EscapeExpectSurrogatePair1, + EscapeExpectSurrogatePair2, + EscapeU5, + EscapeU6, + EscapeU7, + EscapeU8, + Literal, + KeyExpr, + ValExpr, + IdentifierOrFunctionExpr, + UnquotedString, + KeyValExpr, + Number, + Digit, + IndexOrSliceExpression, + BracketSpecifier, + BracketSpecifierOrMultiSelectList, + Filter, + MultiSelectList, + MultiSelectHash, + RhsSliceExpressionStop, + RhsSliceExpressionStep, + ExpectRightBracket, + ExpectRightParen, + ExpectDot, + ExpectRightBrace, + ExpectColon, + ExpectMultiSelectList, + CmpLtOrLte, + CmpEq, + CmpGtOrGte, + CmpNe, + ExpectPipeOrOr, + ExpectAnd +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/JsonTransformer.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/JsonTransformer.cs new file mode 100644 index 00000000..46059aea --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/JsonTransformer.cs @@ -0,0 +1,159 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath +{ + // 2024-04-19: Powertools addition. + internal sealed class DynamicResources; + /// + /// Provides functionality for applying a JMESPath expression to transform a JSON document into + /// another JSON document + /// + /// + /// The following example shows how to apply a JMESPath expression to transform a JSON document into + /// another JSON document. + /// + /// using System; + /// using System.Text.Json; + /// + /// public class Example + /// { + /// public static void Main() + /// { + /// string jsonString = @" + /// { + /// ""people"": [ + /// { + /// ""age"": 20, + /// ""other"": ""foo"", + /// ""name"": ""Bob"" + /// }, + /// { + /// ""age"": 25, + /// ""other"": ""bar"", + /// ""name"": ""Fred"" + /// }, + /// { + /// ""age"": 30, + /// ""other"": ""baz"", + /// ""name"": ""George"" + /// } + /// ] + /// } + /// "; + /// + /// using JsonDocument doc = JsonDocument.Parse(jsonString); + /// + /// var transformer = JsonTransformer.Parse("people[?age > `20`].[name, age]"); + /// + /// using JsonDocument result = transformer.Transform(doc.RootElement); + /// + /// var serializerOptions = new JsonSerializerOptions() {WriteIndented = true}; + /// Console.WriteLine(JsonSerializer.Serialize(result.RootElement, serializerOptions)); + /// } + /// + /// Output: + /// + /// + /// [ + /// [ + /// "Fred", + /// 25 + /// ], + /// [ + /// "George", + /// 30 + /// ] + /// ] + /// + /// + + public sealed class JsonTransformer + { + /// + /// Parses a JMESPath string into a , for "parse once, use many times". + /// A instance is thread safe and has no mutable state. + /// + /// A JMESPath string. + /// A . + /// + /// The parameter is not a valid JMESPath expression. + /// + /// + /// The is . + /// + public static JsonTransformer Parse(string jmesPath) + { + ArgumentNullException.ThrowIfNull(jmesPath); + + var compiler = new JmesPathParser(jmesPath); + return compiler.Parse(); + } + + private readonly Expression _expr; + + internal JsonTransformer(Expression expr) + { + _expr = expr; + } + + /// + /// Applies a JMESPath expression to a JSON document to transform it + /// into another Json document. + /// + /// + /// It is the users responsibilty to properly Dispose the returned value + /// + /// The provided JSON document. + /// The transformed JSON document. If a type error is detected in a function call, + /// a JSON null value is returned. + public JsonDocument Transform(JsonElement doc) + { + // 2024-04-19: Powertools addition. + var resources = new DynamicResources(); + _expr.TryEvaluate(resources, new JsonElementValue(doc), out var temp); + return JsonDocument.Parse(temp.ToString() ?? string.Empty); + } + + /// + /// Applies a JMESPath expression to a JSON document to transform it + /// into another Json document. + /// This method parses and applies the expression in one operation. + /// + /// + /// It is the users responsibilty to properly Dispose the returned value + /// + /// The provided JSON document. + /// A JMESPath string. + /// The transformed JSON document. + /// + /// The parameter is not a valid JMESPath expression. + /// + /// + /// The is . + /// + public static JsonDocument Transform(JsonElement doc, string jmesPath) + { + // 2024-04-19: Powertools addition. + var searcher = Parse(jmesPath); + return searcher.Transform(doc); + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operator.cs new file mode 100644 index 00000000..1e24bbf0 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operator.cs @@ -0,0 +1,89 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +namespace AWS.Lambda.Powertools.JMESPath +{ + internal enum Operator + { + Default, // Identifier, CurrentNode, Index, MultiSelectList, MultiSelectHash, FunctionExpression + Projection, + FlattenProjection, // FlattenProjection + Or, + And, + Eq, + Ne, + Lt, + Lte, + Gt, + Gte, + Not + } + + internal static class OperatorTable + { + internal static int PrecedenceLevel(Operator oper) + { + switch (oper) + { + case Operator.Projection: + return 1; + case Operator.FlattenProjection: + return 1; + case Operator.Or: + return 2; + case Operator.And: + return 3; + case Operator.Eq: + case Operator.Ne: + return 4; + case Operator.Lt: + case Operator.Lte: + case Operator.Gt: + case Operator.Gte: + return 5; + case Operator.Not: + return 6; + default: + return 6; + } + } + + internal static bool IsRightAssociative(Operator oper) + { + switch (oper) + { + case Operator.Not: + return true; + case Operator.Projection: + return true; + case Operator.FlattenProjection: + return false; + case Operator.Or: + case Operator.And: + case Operator.Eq: + case Operator.Ne: + case Operator.Lt: + case Operator.Lte: + case Operator.Gt: + case Operator.Gte: + return false; + default: + return false; + } + } + } + +} + diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/AndOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/AndOperator.cs new file mode 100644 index 00000000..901aece8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/AndOperator.cs @@ -0,0 +1,47 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are and. +/// +internal sealed class AndOperator : BinaryOperator +{ + /// + /// Singleton instance of the class. + /// + internal static AndOperator Instance { get; } = new(); + + private AndOperator() + : base(Operator.And) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + result = Expression.IsTrue(lhs) ? rhs : lhs; + return true; + } + + public override string ToString() + { + return "AndOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/BinaryOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/BinaryOperator.cs new file mode 100644 index 00000000..f7aa3f07 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/BinaryOperator.cs @@ -0,0 +1,40 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators +{ + /// + /// Base class for all binary operators. + /// + internal abstract class BinaryOperator : IBinaryOperator + { + private protected BinaryOperator(Operator oper) + { + PrecedenceLevel = OperatorTable.PrecedenceLevel(oper); + } + + /// + public int PrecedenceLevel {get;} + + /// + public bool IsRightAssociative => false; + + /// + public abstract bool TryEvaluate(IValue lhs, IValue rhs, out IValue result); + } +} + diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/EqOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/EqOperator.cs new file mode 100644 index 00000000..d1fc3d4b --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/EqOperator.cs @@ -0,0 +1,48 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are equality +/// +internal sealed class EqOperator : BinaryOperator +{ + /// + /// Singleton instance of the + /// + internal static EqOperator Instance { get; } = new(); + + private EqOperator() + : base(Operator.Eq) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + var comparer = ValueEqualityComparer.Instance; + result = comparer.Equals(lhs, rhs) ? JsonConstants.True : JsonConstants.False; + return true; + } + + public override string ToString() + { + return "EqOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/GtOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/GtOperator.cs new file mode 100644 index 00000000..07b8ff0a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/GtOperator.cs @@ -0,0 +1,73 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are comparison +/// +internal sealed class GtOperator : BinaryOperator +{ + /// + /// Singleton instance of the class + /// + internal static GtOperator Instance { get; } = new(); + + private GtOperator() + : base(Operator.Gt) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + switch (lhs.Type) + { + case JmesPathType.Number when rhs.Type == JmesPathType.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + result = dec1 > dec2 ? JsonConstants.True : JsonConstants.False; + } + else if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + result = val1 > val2 ? JsonConstants.True : JsonConstants.False; + } + else + { + result = JsonConstants.Null; + } + + break; + } + case JmesPathType.String when rhs.Type == JmesPathType.String: + result = string.CompareOrdinal(lhs.GetString(), rhs.GetString()) > 0 ? JsonConstants.True : JsonConstants.False; + break; + default: + result = JsonConstants.Null; + break; + } + + return true; + } + + public override string ToString() + { + return "GtOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/GteOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/GteOperator.cs new file mode 100644 index 00000000..7c25c6eb --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/GteOperator.cs @@ -0,0 +1,73 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are comparison +/// +internal sealed class GteOperator : BinaryOperator +{ + /// + /// Singleton instance of the + /// + internal static GteOperator Instance { get; } = new(); + + private GteOperator() + : base(Operator.Gte) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + switch (lhs.Type) + { + case JmesPathType.Number when rhs.Type == JmesPathType.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + result = dec1 >= dec2 ? JsonConstants.True : JsonConstants.False; + } + else if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + result = val1 >= val2 ? JsonConstants.True : JsonConstants.False; + } + else + { + result = JsonConstants.Null; + } + + break; + } + case JmesPathType.String when rhs.Type == JmesPathType.String: + result = string.CompareOrdinal(lhs.GetString(), rhs.GetString()) >= 0 ? JsonConstants.True : JsonConstants.False; + break; + default: + result = JsonConstants.Null; + break; + } + + return true; + } + + public override string ToString() + { + return "GteOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/IBinaryOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/IBinaryOperator.cs new file mode 100644 index 00000000..cff04a8e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/IBinaryOperator.cs @@ -0,0 +1,39 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Interface for all binary operators. +/// +internal interface IBinaryOperator +{ + /// + /// The precedence level of the operator. + /// + int PrecedenceLevel {get;} + + /// + /// Whether the operator is right-associative. + /// + bool IsRightAssociative {get;} + + /// + /// Evaluates the expression. + /// + bool TryEvaluate(IValue lhs, IValue rhs, out IValue result); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/IUnaryOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/IUnaryOperator.cs new file mode 100644 index 00000000..a44b1392 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/IUnaryOperator.cs @@ -0,0 +1,42 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Interface for unary operators. +/// +internal interface IUnaryOperator +{ + /// + /// The precedence level of the operator. + /// + int PrecedenceLevel {get;} + + /// + /// Whether the operator is right-associative or not. + /// + bool IsRightAssociative {get;} + + /// + /// Evaluates the expression. + /// + /// + /// + /// + bool TryEvaluate(IValue elem, out IValue result); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/LtOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/LtOperator.cs new file mode 100644 index 00000000..6405e593 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/LtOperator.cs @@ -0,0 +1,73 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are comparison +/// +internal sealed class LtOperator : BinaryOperator +{ + /// + /// Singleton instance of the + /// + internal static LtOperator Instance { get; } = new(); + + private LtOperator() + : base(Operator.Lt) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + switch (lhs.Type) + { + case JmesPathType.Number when rhs.Type == JmesPathType.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + result = dec1 < dec2 ? JsonConstants.True : JsonConstants.False; + } + else if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + result = val1 < val2 ? JsonConstants.True : JsonConstants.False; + } + else + { + result = JsonConstants.Null; + } + + break; + } + case JmesPathType.String when rhs.Type == JmesPathType.String: + result = string.CompareOrdinal(lhs.GetString(), rhs.GetString()) < 0 ? JsonConstants.True : JsonConstants.False; + break; + default: + result = JsonConstants.Null; + break; + } + + return true; + } + + public override string ToString() + { + return "LtOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/LteOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/LteOperator.cs new file mode 100644 index 00000000..f0f3ba33 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/LteOperator.cs @@ -0,0 +1,74 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are comparison +/// +internal sealed class LteOperator : BinaryOperator +{ + /// + /// Singleton instance of the class + /// + internal static LteOperator Instance { get; } = new(); + + private LteOperator() + : base(Operator.Lte) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + switch (lhs.Type) + { + case JmesPathType.Number when rhs.Type == JmesPathType.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + result = dec1 <= dec2 ? JsonConstants.True : JsonConstants.False; + } + else if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + result = val1 <= val2 ? JsonConstants.True : JsonConstants.False; + } + else + { + result = JsonConstants.Null; + } + + break; + } + case JmesPathType.String when rhs.Type == JmesPathType.String: + result = string.CompareOrdinal(lhs.GetString(), rhs.GetString()) <= 0 ? JsonConstants.True : JsonConstants.False; + break; + default: + result = JsonConstants.Null; + break; + } + + return true; + } + + + public override string ToString() + { + return "LteOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/NeOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/NeOperator.cs new file mode 100644 index 00000000..410f3b66 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/NeOperator.cs @@ -0,0 +1,53 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are inequality +/// +internal sealed class NeOperator : BinaryOperator +{ + /// + /// Singleton instance of the class + /// + internal static NeOperator Instance { get; } = new(); + + private NeOperator() + : base(Operator.Ne) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + if (!EqOperator.Instance.TryEvaluate(lhs, rhs, out var value)) + { + result = JsonConstants.Null; + return false; + } + + result = Expression.IsFalse(value) ? JsonConstants.True : JsonConstants.False; + return true; + } + + public override string ToString() + { + return "NeOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/NotOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/NotOperator.cs new file mode 100644 index 00000000..429b1a5d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/NotOperator.cs @@ -0,0 +1,46 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Represents the not operator. +/// +internal sealed class NotOperator : UnaryOperator +{ + /// + /// The singleton instance of the class. + /// + internal static NotOperator Instance { get; } = new(); + + private NotOperator() + : base(Operator.Not) + {} + + /// + public override bool TryEvaluate(IValue elem, out IValue result) + { + result = Expression.IsFalse(elem) ? JsonConstants.True : JsonConstants.False; + return true; + } + + public override string ToString() + { + return "Not"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/OrOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/OrOperator.cs new file mode 100644 index 00000000..3db2145f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/OrOperator.cs @@ -0,0 +1,52 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators; + +/// +/// Base class for all binary operators that are left associative. +/// +internal sealed class OrOperator : BinaryOperator +{ + /// + /// Singleton instance of the class. + /// + internal static OrOperator Instance { get; } = new(); + + private OrOperator() + : base(Operator.Or) + { + } + + /// + public override bool TryEvaluate(IValue lhs, IValue rhs, out IValue result) + { + if (lhs.Type == JmesPathType.Null && rhs.Type == JmesPathType.Null) + { + result = lhs; + return true; + } + result = Expression.IsTrue(lhs) ? lhs : rhs; + return true; + } + + public override string ToString() + { + return "OrOperator"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/UnaryOperator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/UnaryOperator.cs new file mode 100644 index 00000000..2a156989 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Operators/UnaryOperator.cs @@ -0,0 +1,41 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath.Operators +{ + /// + /// Base class for unary operators. + /// + internal abstract class UnaryOperator : IUnaryOperator + { + private protected UnaryOperator(Operator oper) + { + PrecedenceLevel = OperatorTable.PrecedenceLevel(oper); + IsRightAssociative = OperatorTable.IsRightAssociative(oper); + } + + /// + public int PrecedenceLevel {get;} + + /// + public bool IsRightAssociative {get;} + + /// + public abstract bool TryEvaluate(IValue elem, out IValue result); + } +} + diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/README.md b/libraries/src/AWS.Lambda.Powertools.JMESPath/README.md new file mode 100644 index 00000000..2ec00c16 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/README.md @@ -0,0 +1,80 @@ +# Powertools JMESPath support + +JMESPath is a query language for JSON used by AWS CLI, AWS Python SDK, and Powertools for AWS Lambda. + +With built-in JMESPath functions to easily deserialize common encoded JSON payloads in Lambda functions. + +## Key features + +- Deserialize JSON from JSON strings, base64, and compressed data +- Use JMESPath to extract and combine data recursively +- Provides commonly used JMESPath expression with popular event sources + +JMESPath allows you to transform a JsonDocument into another JsonDocument. + +For example, consider the JSON data + +```csharp + +string jsonString = """ +{ + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] +} +"""; + +using JsonDocument doc = JsonDocument.Parse(jsonString); + +string expr = "powertools_json(body).customerId"; +//also works for fetching and flattening deeply nested data +// string expr = "deeply_nested[*].some_data[]"; + +JsonDocument result = JsonTransformer.Transform(doc.RootElement, expr); + + + +``` + +It produces the result +```json +"dd4649e6-2484-4993-acb8-0f9123103394" +``` + You can find more examples [here](../../tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathExamples.cs) + +## Built-in envelopes + +We provide built-in envelopes for popular AWS Lambda event sources to easily decode and/or deserialize JSON objects. + +| Envelop | JMESPath expression | +|---------------------|-----------------------------------------------------------------------------| +| API_GATEWAY_HTTP | powertools_json(body) | +| API_GATEWAY_REST | powertools_json(body) | +| CLOUDWATCH_LOGS | awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*] | +| KINESIS_DATA_STREAM | Records[*].kinesis.powertools_json(powertools_base64(data)) | +| SNS | Records[*].Sns.Message | powertools_json(@) | +| SQS | Records[*].powertools_json(body) | + +More examples of events can be found [here](../../tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files) + +## Built-in JMESPath functions +You can use our built-in JMESPath functions within your envelope expression. They handle deserialization for common data formats found in AWS Lambda event sources such as JSON strings, base64, and uncompress gzip data. + +### powertools_json function +Use powertools_json function to decode any JSON string anywhere a JMESPath expression is allowed. + +### powertools_base64 function +Use powertools_base64 function to decode any base64 data. + +### powertools_base64_gzip function +Use powertools_base64_gzip function to decompress and decode base64 data. + +## Credit +We took heavy inspiration in the https://github.com/danielaparker/JsonCons.Net repository. \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Token.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Token.cs new file mode 100644 index 00000000..46465f84 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Token.cs @@ -0,0 +1,307 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Diagnostics; +using AWS.Lambda.Powertools.JMESPath.Expressions; +using AWS.Lambda.Powertools.JMESPath.Functions; +using AWS.Lambda.Powertools.JMESPath.Operators; +using AWS.Lambda.Powertools.JMESPath.Values; + +namespace AWS.Lambda.Powertools.JMESPath +{ + internal enum TokenType + { + CurrentNode, + LeftParen, + RightParen, + BeginMultiSelectHash, + EndMultiSelectHash, + BeginMultiSelectList, + EndMultiSelectList, + BeginFilter, + EndFilter, + Pipe, + Separator, + Key, + Literal, + Expression, + BinaryOperator, + UnaryOperator, + Function, + BeginArguments, + EndArguments, + Argument, + BeginExpressionType, + EndExpressionType, + EndOfExpression + } + + /// + /// Represents a token in the JMESPath expression. + /// + internal readonly struct Token : IEquatable + { + /// + /// The expression associated with this token. + /// + private readonly object _expr; + + internal Token(TokenType type) + { + Type = type; + _expr = null; + } + + internal Token(TokenType type, string s) + { + Type = type; + _expr = s; + } + + internal Token(IExpression expr) + { + Type = TokenType.Expression; + _expr = expr; + } + + internal Token(IUnaryOperator expr) + { + Type = TokenType.UnaryOperator; + _expr = expr; + } + + internal Token(IBinaryOperator expr) + { + Type = TokenType.BinaryOperator; + _expr = expr; + } + + internal Token(IFunction expr) + { + Type = TokenType.Function; + _expr = expr; + } + + internal Token(IValue expr) + { + Type = TokenType.Literal; + _expr = expr; + } + + internal TokenType Type{get;} + + /// + /// Return if it is an Operator + /// + internal bool IsOperator + { + get + { + switch(Type) + { + case TokenType.UnaryOperator: + return true; + case TokenType.BinaryOperator: + return true; + default: + return false; + } + } + } + + /// + /// True if the expression is a projection, false otherwise. + /// + internal bool IsProjection + { + get + { + switch(Type) + { + case TokenType.Expression: + return GetExpression().IsProjection; + default: + return false; + } + } + } + + /// + /// True if the expression is right-associative, false otherwise. + /// + internal bool IsRightAssociative + { + get + { + switch(Type) + { + case TokenType.Expression: + return GetExpression().IsRightAssociative; + case TokenType.UnaryOperator: + return GetUnaryOperator().IsRightAssociative; + case TokenType.BinaryOperator: + return GetBinaryOperator().IsRightAssociative; + default: + return false; + } + } + } + + /// + /// The precedence level of the operator. + /// + internal int PrecedenceLevel + { + get + { + switch(Type) + { + case TokenType.Expression: + return GetExpression().PrecedenceLevel; + case TokenType.UnaryOperator: + return GetUnaryOperator().PrecedenceLevel; + case TokenType.BinaryOperator: + return GetBinaryOperator().PrecedenceLevel; + default: + return 100; + } + } + } + + /// + /// Returns the token expression key if Type is Key + /// + /// + /// + internal string GetKey() + { + Debug.Assert(Type == TokenType.Key); + return _expr as string ?? throw new InvalidOperationException("Key cannot be null"); + } + + /// + /// Returns the token expression key if Type is UnaryOperator + /// + /// + /// + internal IUnaryOperator GetUnaryOperator() + { + Debug.Assert(Type == TokenType.UnaryOperator); + return _expr as IUnaryOperator ?? throw new InvalidOperationException("Unary operator cannot be null"); + } + + /// + /// Returns the token expression key if Type is BinaryOperator + /// + /// + /// + internal IBinaryOperator GetBinaryOperator() + { + Debug.Assert(Type == TokenType.BinaryOperator); + return _expr as IBinaryOperator ?? throw new InvalidOperationException("Binary operator cannot be null"); + } + + /// + /// Returns the token expression key if Type is Literal + /// + /// + /// + internal IValue GetValue() + { + Debug.Assert(Type == TokenType.Literal); + return _expr as IValue ?? throw new InvalidOperationException("Value cannot be null"); + } + + /// + /// Returns the token expression key if Type is Function + /// + /// + /// + internal IFunction GetFunction() + { + Debug.Assert(Type == TokenType.Function); + return _expr as IFunction ?? throw new InvalidOperationException("Function cannot be null"); + } + + /// + /// Returns the token expression key if Type is Expression + /// + internal IExpression GetExpression() + { + Debug.Assert(Type == TokenType.Expression); + return _expr as IExpression ?? throw new InvalidOperationException("Expression cannot be null"); + } + public bool Equals(Token other) + { + return Type == other.Type; + } + + public override string ToString() + { + switch(Type) + { + case TokenType.BeginArguments: + return "BeginArguments"; + case TokenType.CurrentNode: + return "CurrentNode"; + case TokenType.LeftParen: + return "LeftParen"; + case TokenType.RightParen: + return "RightParen"; + case TokenType.BeginMultiSelectHash: + return "BeginMultiSelectHash"; + case TokenType.EndMultiSelectHash: + return "EndMultiSelectHash"; + case TokenType.BeginMultiSelectList: + return "BeginMultiSelectList"; + case TokenType.EndMultiSelectList: + return "EndMultiSelectList"; + case TokenType.BeginFilter: + return "BeginFilter"; + case TokenType.EndFilter: + return "EndFilter"; + case TokenType.Pipe: + return $"Pipe"; + case TokenType.Separator: + return "Separator"; + case TokenType.Key: + return $"Key {_expr}"; + case TokenType.Literal: + return $"Literal {_expr}"; + case TokenType.Expression: + return "Expression"; + case TokenType.BinaryOperator: + return $"BinaryOperator {_expr}"; + case TokenType.UnaryOperator: + return $"UnaryOperator {_expr}"; + case TokenType.Function: + return $"Function {_expr}"; + case TokenType.EndArguments: + return "EndArguments"; + case TokenType.Argument: + return "Argument"; + case TokenType.BeginExpressionType: + return "BeginExpressionType"; + case TokenType.EndExpressionType: + return "EndExpressionType"; + case TokenType.EndOfExpression: + return "EndOfExpression"; + default: + return "Other"; + } + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Utilities/JsonElementEqualityComparer.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Utilities/JsonElementEqualityComparer.cs new file mode 100644 index 00000000..d96c61f2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Utilities/JsonElementEqualityComparer.cs @@ -0,0 +1,195 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace AWS.Lambda.Powertools.JMESPath.Utilities +{ + /// + /// Compares two instances for equality by using value-based comparison. + /// + + public sealed class JsonElementEqualityComparer : IEqualityComparer + { + /// Gets a singleton instance of . This property is read-only. + public static JsonElementEqualityComparer Instance { get; } = new(); + + private static int MaxHashDepth => 64; + + private JsonElementEqualityComparer() {} + + /// + /// Determines whether the provided objects are equal. + /// + /// If the two instances have different data types, they are different. + /// + /// If both instances are null, true, or false, they are equal. + /// + /// If both are strings, they are compared with the String.Equals method. + /// + /// If both are numbers, and both can be represented by a , + /// they are compared with the Decimal.Equals method, otherwise they are + /// compared as doubles. + /// + /// If both are objects, they are compared according to the following rules: + /// + ///
    + ///
  • If the two objects have a different number of properties, they are different.
  • + ///
  • Otherwise, order each object's properties by name and compare sequentially. + /// The properties are compared first by name with the String.Equals method, then by value with
  • + ///
  • A mismatching property means the two instance are different.
  • + ///
+ /// + /// If both are arrays, and both have the same length and compare equal element wise with , + /// they are equal, otherwise they are different. + ///
+ /// The first object of type cref="JsonElement"/> to compare. + /// The second object of type cref="JsonElement"/> to compare. + /// + /// + /// Unable to compare numbers as either or double (shouldn't happen.) + /// + public bool Equals(JsonElement lhs, JsonElement rhs) + { + if (lhs.ValueKind != rhs.ValueKind) + return false; + + switch (lhs.ValueKind) + { + case JsonValueKind.Null: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Undefined: + return true; + + case JsonValueKind.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + return dec1.Equals(dec2); + } + + if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + return Math.Abs(val1 - val2) < 0.000000001; + } + + return false; + } + + case JsonValueKind.String: + { + var str = lhs.GetString() ?? throw new InvalidOperationException("string cannot be null"); + return str.Equals(rhs.GetString()); + } + + case JsonValueKind.Array: + return lhs.EnumerateArray().SequenceEqual(rhs.EnumerateArray(), this); + + case JsonValueKind.Object: + { + // OrderBy performs a stable sort (Note that supports duplicate property names) + var baseEnumerator1 = lhs.EnumerateObject(); + var baseEnumerator2 = rhs.EnumerateObject(); + if (baseEnumerator1.Count() != baseEnumerator2.Count()) + { + return false; + } + + using var enumerator1 = baseEnumerator1.OrderBy(p => p.Name, StringComparer.Ordinal).GetEnumerator(); + using var enumerator2 = baseEnumerator2.OrderBy(p => p.Name, StringComparer.Ordinal).GetEnumerator(); + + var result1 = enumerator1.MoveNext(); + var result2 = enumerator2.MoveNext(); + while (result1 && result2) + { + if (enumerator1.Current.Name != enumerator2.Current.Name) + { + return false; + } + if (!(Equals(enumerator1.Current.Value,enumerator2.Current.Value))) + { + return false; + } + result1 = enumerator1.MoveNext(); + result2 = enumerator2.MoveNext(); + } + + return result1 == false && result2 == false; + } + + default: + throw new InvalidOperationException($"Unknown JsonValueKind {lhs.ValueKind}"); + } + } + + /// + /// Returns a hash code for the specified value. + /// + /// + /// An Int32 value representing the hash code of the value. + public int GetHashCode(JsonElement value) + { + return ComputeHashCode(value, 0); + } + + private int ComputeHashCode(JsonElement element, int depth) + { + var hashCode = element.ValueKind.GetHashCode(); + + switch (element.ValueKind) + { + case JsonValueKind.Null: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Undefined: + break; + + case JsonValueKind.Number: + hashCode += 17*element.GetDouble().GetHashCode(); + break; + + case JsonValueKind.String: + { + var str = element.GetString() ?? throw new InvalidOperationException("string cannot be null"); + hashCode += 17 * str.GetHashCode(); + break; + } + + case JsonValueKind.Array: + if (depth < MaxHashDepth) + foreach (var item in element.EnumerateArray()) + hashCode += 17*ComputeHashCode(item, depth+1); + break; + + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) + { + hashCode += 17*property.Name.GetHashCode(); + if (depth < MaxHashDepth) + hashCode += 17*ComputeHashCode(property.Value, depth+1); + } + break; + + default: + throw new InvalidOperationException($"Unknown JsonValueKind {element.ValueKind}"); + } + return hashCode; + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ArrayValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ArrayValue.cs new file mode 100644 index 00000000..e07d8b8d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ArrayValue.cs @@ -0,0 +1,179 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a JMESPath array value. +/// +internal readonly struct ArrayValue : IValue +{ + /// + /// An enumerator for a JMESPath array value. + /// + private sealed class ArrayEnumerator : IArrayValueEnumerator + { + /// + /// The list of values in the array. + /// + private readonly IList _value; + /// + /// The enumerator for the list of values in the array. + /// + private readonly System.Collections.IEnumerator _enumerator; + + public ArrayEnumerator(IList value) + { + _value = value; + _enumerator = value.GetEnumerator(); + } + + /// + /// Gets the current value in the array. + /// + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + /// + /// Resets the enumerator to the beginning of the array. + /// + public void Reset() { _enumerator.Reset(); } + + void IDisposable.Dispose() {} + + /// + /// Gets the current value in the array. + /// + /// + public IValue Current => _enumerator.Current as IValue ?? throw new InvalidOperationException("Current cannot be null"); + + /// + /// Gets the current value in the array. + /// + object System.Collections.IEnumerator.Current => Current; + + /// + /// Gets an enumerator for the array. + /// + /// + public IEnumerator GetEnumerator() + { + return _value.GetEnumerator(); + } + + /// + /// Gets an enumerator for the array. + /// + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + /// + /// The list of values in the array. + /// + private readonly IList _value; + + /// + /// Creates a new array value. + /// + internal ArrayValue(IList value) + { + _value = value; + } + + /// + /// Gets the type of the value. + /// + public JmesPathType Type => JmesPathType.Array; + + /// + /// Gets the value at the specified index. + /// + public IValue this[int index] => _value[index]; + + /// + /// Gets the length of the array. + /// + /// + public int GetArrayLength() { return _value.Count; } + + public string GetString() + { + throw new InvalidOperationException(); + } + + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + public IArrayValueEnumerator EnumerateArray() + { + return new ArrayEnumerator(_value); + } + + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + var buffer = new StringBuilder(); + buffer.Append('['); + var first = true; + foreach (var item in _value) + { + if (!first) + { + buffer.Append(','); + } + else + { + first = false; + } + + buffer.Append(item); + } + + buffer.Append(']'); + return buffer.ToString(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/DecimalValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/DecimalValue.cs new file mode 100644 index 00000000..b03ff5db --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/DecimalValue.cs @@ -0,0 +1,104 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a JMESPath number value. +/// +internal readonly struct DecimalValue : IValue +{ + /// + /// The value of the JMESPath number. + /// + private readonly decimal _value; + + internal DecimalValue(decimal value) + { + _value = value; + } + + /// + /// The type of the JMESPath value. + /// + public JmesPathType Type => JmesPathType.Number; + + /// + /// Gets the value at the specified index. + /// + /// + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + value = _value; + return true; + } + + /// + public bool TryGetDouble(out double value) + { + value = (double)_value; + return true; + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + var s = JsonSerializer.Serialize(_value); + return s; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/DoubleValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/DoubleValue.cs new file mode 100644 index 00000000..2962f28d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/DoubleValue.cs @@ -0,0 +1,109 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a double value. +/// +internal readonly struct DoubleValue : IValue +{ + /// + /// The value of this . + /// + private readonly double _value; + + internal DoubleValue(double value) + { + _value = value; + } + + /// + public JmesPathType Type => JmesPathType.Number; + + /// + /// Gets the value at the specified index. + /// + /// + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + if (!(double.IsNaN(_value) || double.IsInfinity(_value)) && + _value is >= (double)decimal.MinValue and <= (double)decimal.MaxValue) + { + value = decimal.MinValue; + return false; + } + + value = new decimal(_value); + return true; + } + + /// + public bool TryGetDouble(out double value) + { + value = _value; + return true; + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + var s = JsonSerializer.Serialize(_value); + return s; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ExpressionValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ExpressionValue.cs new file mode 100644 index 00000000..53dc3a7c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ExpressionValue.cs @@ -0,0 +1,94 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a JMESPath expression. +/// +internal readonly struct ExpressionValue : IValue +{ + /// + /// The expression to evaluate. + /// + private readonly IExpression _expr; + + internal ExpressionValue(IExpression expr) + { + _expr = expr; + } + + /// + public JmesPathType Type => JmesPathType.Expression; + + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + return _expr; + } + + public override string ToString() + { + return "expression"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/FalseValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/FalseValue.cs new file mode 100644 index 00000000..5a06320c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/FalseValue.cs @@ -0,0 +1,84 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a JMESPath false value. +/// +internal readonly struct FalseValue : IValue +{ + /// + public JmesPathType Type => JmesPathType.False; + + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + return "false"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IArrayValueEnumerator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IArrayValueEnumerator.cs new file mode 100644 index 00000000..778e3403 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IArrayValueEnumerator.cs @@ -0,0 +1,25 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents an enumerator for an array value. +/// +internal interface IArrayValueEnumerator : IEnumerator, IEnumerable +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IObjectValueEnumerator.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IObjectValueEnumerator.cs new file mode 100644 index 00000000..407f04d0 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IObjectValueEnumerator.cs @@ -0,0 +1,25 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Collections.Generic; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Interface for object value enumerator. +/// +internal interface IObjectValueEnumerator : IEnumerator, IEnumerable +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IValue.cs new file mode 100644 index 00000000..49950ed6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/IValue.cs @@ -0,0 +1,84 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +internal interface IValue +{ + /// + /// The type of the JMESPath value + /// + JmesPathType Type { get; } + + /// + /// The value of the JMESPath value + /// + /// + IValue this[int index] { get; } + + /// + /// The length of the array + /// + /// + int GetArrayLength(); + + /// + /// Get the value as a string + /// + /// + string GetString(); + + /// + /// Try to get the value as a decimal + /// + /// + /// + bool TryGetDecimal(out decimal value); + + /// + /// Try to get the value as a double + /// + /// + /// + bool TryGetDouble(out double value); + + /// + /// Try to get the property value + /// + /// + /// + /// + bool TryGetProperty(string propertyName, out IValue property); + + /// + /// Enumerate the array values + /// + /// + IArrayValueEnumerator EnumerateArray(); + + + /// + /// Enumerate the object values + /// + IObjectValueEnumerator EnumerateObject(); + + + /// + /// Get the expression for this value + /// + IExpression GetExpression(); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/JmesPathType.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/JmesPathType.cs new file mode 100644 index 00000000..86a71209 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/JmesPathType.cs @@ -0,0 +1,31 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents the type of a JMESPath value. +/// +internal enum JmesPathType +{ + Null, + Array, + False, + Number, + Object, + String, + True, + Expression +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/JsonElementValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/JsonElementValue.cs new file mode 100644 index 00000000..b215d339 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/JsonElementValue.cs @@ -0,0 +1,250 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a value. +/// +internal readonly struct JsonElementValue : IValue +{ + /// + /// The underlying value. + /// + private class ArrayEnumerator : IArrayValueEnumerator + { + /// + /// The underlying value. + /// + private JsonElement.ArrayEnumerator _enumerator; + + public ArrayEnumerator(JsonElement.ArrayEnumerator enumerator) + { + _enumerator = enumerator; + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + _enumerator.Reset(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Cleanup + if (disposing) + { + _enumerator.Dispose(); + } + } + + /// + /// The current in the . + /// + public IValue Current => new JsonElementValue(_enumerator.Current); + + /// + /// The current in the . + /// + object System.Collections.IEnumerator.Current => Current; + + public IEnumerator GetEnumerator() + { + return new ArrayEnumerator(_enumerator.GetEnumerator()); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private class ObjectEnumerator : IObjectValueEnumerator + { + /// + /// The underlying value. + /// + private JsonElement.ObjectEnumerator _enumerator; + + public ObjectEnumerator(JsonElement.ObjectEnumerator enumerator) + { + _enumerator = enumerator; + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + _enumerator.Reset(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Cleanup + if (disposing) + { + _enumerator.Dispose(); + } + } + + /// + /// The current in the . + /// + public NameValuePair Current => + new(_enumerator.Current.Name, new JsonElementValue(_enumerator.Current.Value)); + + /// + /// The current in the . + /// + object System.Collections.IEnumerator.Current => Current; + + public IEnumerator GetEnumerator() + { + return new ObjectEnumerator(_enumerator); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private readonly JsonElement _element; + + internal JsonElementValue(JsonElement element) + { + _element = element; + } + + public JmesPathType Type + { + get + { + switch (_element.ValueKind) + { + case JsonValueKind.Array: + return JmesPathType.Array; + case JsonValueKind.False: + return JmesPathType.False; + case JsonValueKind.Number: + return JmesPathType.Number; + case JsonValueKind.Object: + return JmesPathType.Object; + case JsonValueKind.String: + return JmesPathType.String; + case JsonValueKind.True: + return JmesPathType.True; + default: + return JmesPathType.Null; + } + } + } + + /// + public IValue this[int index] => new JsonElementValue(_element[index]); + + public int GetArrayLength() + { + return _element.GetArrayLength(); + } + + public string GetString() + { + return _element.GetString() ?? throw new InvalidOperationException("String cannot be null"); + } + + public bool TryGetDecimal(out decimal value) + { + return _element.TryGetDecimal(out value); + } + + public bool TryGetDouble(out double value) + { + return _element.TryGetDouble(out value); + } + + public bool TryGetProperty(string propertyName, out IValue property) + { + var r = _element.TryGetProperty(propertyName, out var prop); + + property = prop.ValueKind == JsonValueKind.String && IsJsonValid(prop.GetString()) + ? new JsonElementValue(JsonNode.Parse(prop.GetString() ?? string.Empty).Deserialize()) + : new JsonElementValue(prop); + + return r; + } + + private static bool IsJsonValid(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return false; + + try + { + using var jsonDoc = JsonDocument.Parse(json); + return true; + } + catch (JsonException) + { + return false; + } + } + + public IArrayValueEnumerator EnumerateArray() + { + return new ArrayEnumerator(_element.EnumerateArray()); + } + + public IObjectValueEnumerator EnumerateObject() + { + return new ObjectEnumerator(_element.EnumerateObject()); + } + + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + var s = JsonSerializer.Serialize(_element); + return s; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/NameValuePair.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/NameValuePair.cs new file mode 100644 index 00000000..e0666fe2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/NameValuePair.cs @@ -0,0 +1,32 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +namespace AWS.Lambda.Powertools.JMESPath.Values +{ + /// + /// Represents a name-value pair. + /// + internal readonly struct NameValuePair + { + public string Name { get; } + public IValue Value { get; } + + public NameValuePair(string name, IValue value) + { + Name = name; + Value = value; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/NullValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/NullValue.cs new file mode 100644 index 00000000..458bf892 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/NullValue.cs @@ -0,0 +1,84 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a null value. +/// +internal readonly struct NullValue : IValue +{ + /// + public JmesPathType Type => JmesPathType.Null; + + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + return "null"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ObjectValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ObjectValue.cs new file mode 100644 index 00000000..86153435 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ObjectValue.cs @@ -0,0 +1,173 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents an object value. +/// +internal readonly struct ObjectValue : IValue +{ + /// + /// An that can be used to iterate over the + /// + private sealed class ObjectEnumerator : IObjectValueEnumerator + { + /// + /// The underlying that is being enumerated. + /// + private readonly IDictionary _value; + + /// + /// The underlying that is being enumerated. + /// + private readonly System.Collections.IEnumerator _enumerator; + + public ObjectEnumerator(IDictionary value) + { + _value = value; + _enumerator = value.GetEnumerator(); + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + _enumerator.Reset(); + } + + void IDisposable.Dispose() + { + } + + /// + public NameValuePair Current + { + get + { + var pair = (KeyValuePair)_enumerator.Current!; + return new NameValuePair(pair.Key, pair.Value); + } + } + + /// + object System.Collections.IEnumerator.Current => Current; + + public IEnumerator GetEnumerator() + { + return new ObjectEnumerator(_value); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private readonly IDictionary _value; + + internal ObjectValue(IDictionary value) + { + _value = value; + } + + /// + public JmesPathType Type => JmesPathType.Object; + + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + return _value.TryGetValue(propertyName, out property); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + return new ObjectEnumerator(_value); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + var buffer = new StringBuilder(); + buffer.Append('{'); + var first = true; + foreach (var property in _value) + { + if (!first) + { + buffer.Append(','); + } + else + { + first = false; + } + + buffer.Append(JsonSerializer.Serialize(property.Key)); + buffer.Append(':'); + buffer.Append(property.Value); + } + + buffer.Append('}'); + return buffer.ToString(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/StringValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/StringValue.cs new file mode 100644 index 00000000..02fb2890 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/StringValue.cs @@ -0,0 +1,96 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// Represents a string value. +/// +internal readonly struct StringValue : IValue +{ + /// + /// The string value. + /// + private readonly string _value; + + internal StringValue(string value) + { + _value = value; + } + + /// + public JmesPathType Type => JmesPathType.String; + + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + return _value; + } + + /// + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + var s = JsonSerializer.Serialize(_value); + return s; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/TrueValue.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/TrueValue.cs new file mode 100644 index 00000000..457012ee --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/TrueValue.cs @@ -0,0 +1,84 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using AWS.Lambda.Powertools.JMESPath.Expressions; + +namespace AWS.Lambda.Powertools.JMESPath.Values; + +/// +/// True value +/// +internal readonly struct TrueValue : IValue +{ + /// + public JmesPathType Type => JmesPathType.True; + + /// + public IValue this[int index] => throw new InvalidOperationException(); + + /// + public int GetArrayLength() + { + throw new InvalidOperationException(); + } + + /// + public string GetString() + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDecimal(out decimal value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetDouble(out double value) + { + throw new InvalidOperationException(); + } + + /// + public bool TryGetProperty(string propertyName, out IValue property) + { + throw new InvalidOperationException(); + } + + /// + public IArrayValueEnumerator EnumerateArray() + { + throw new InvalidOperationException(); + } + + /// + public IObjectValueEnumerator EnumerateObject() + { + throw new InvalidOperationException(); + } + + /// + public IExpression GetExpression() + { + throw new InvalidOperationException("Not an expression"); + } + + public override string ToString() + { + return "true"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ValueComparer.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ValueComparer.cs new file mode 100644 index 00000000..0996d6c7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ValueComparer.cs @@ -0,0 +1,180 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AWS.Lambda.Powertools.JMESPath.Values +{ + /// + /// Compares two instances. + /// + internal sealed class ValueComparer : IComparer, System.Collections.IComparer + { + /// Gets a singleton instance of . This property is read-only. + public static ValueComparer Instance { get; } = new(); + + /// + /// Constructs a + /// + private ValueComparer() {} + + /// + /// Compares two instances. + /// + /// If the two instances have different data types, they are + /// compared according to their Type property, which gives this ordering: + /// + /// Undefined + /// Object + /// Array + /// String + /// Number + /// True + /// False + /// Null + /// + /// + /// If both instances are null, true, or false, they are equal. + /// + /// If both are strings, they are compared with the String.CompareTo method. + /// + /// If both are numbers, and both can be represented by a , + /// they are compared with the Decimal.CompareTo method, otherwise they are + /// compared as doubles. + /// + /// If both are objects, they are compared accoring to the following rules: + /// + ///
    + ///
  • Order each object's properties by name and compare sequentially. + /// The properties are compared first by name with the String.CompareTo method, then by value with
  • + ///
  • The first mismatching property defines which instance is less or greater than the other.
  • + ///
  • If the two sequences have no mismatching properties until one of them ends, and the other is longer, the shorter sequence is less than the other.
  • + ///
  • If the two sequences have no mismatching properties and have the same length, they are equal.
  • + ///
+ /// + /// If both are arrays, they are compared element wise with . + /// The first mismatching element defines which instance is less or greater than the other. + /// If the two arrays have no mismatching elements until one of them ends, and the other is longer, the shorter array is less than the other. + /// If the two arrays have no mismatching elements and have the same length, they are equal. + /// + ///
+ /// The first object of type cref="IValue"/> to compare. + /// The second object of type cref="IValue"/> to compare. + /// + /// + /// Unable to compare numbers as either or double (shouldn't happen.) + /// + public int Compare(IValue lhs, IValue rhs) + { + if (lhs.Type != rhs.Type) + return (int)lhs.Type - (int)rhs.Type; + + switch (lhs.Type) + { + case JmesPathType.Null: + case JmesPathType.True: + case JmesPathType.False: + return 0; + + case JmesPathType.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + return dec1.CompareTo(dec2); + } + + if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + return val1.CompareTo(val2); + } + + throw new InvalidOperationException("Unable to compare numbers"); + } + + case JmesPathType.String: + return string.Compare(lhs.GetString(), rhs.GetString(), StringComparison.Ordinal); + + case JmesPathType.Array: + { + // 2024-04-19: Powertools addition. + return ArrayComparer(lhs, rhs); + } + + case JmesPathType.Object: + { + // 2024-04-19: Powertools addition. + return ObjectComparer(lhs, rhs); + } + + default: + throw new InvalidOperationException($"Unknown JmesPathType {lhs.Type}"); + } + } + + // 2024-04-19: Powertools addition. + private int ArrayComparer(IValue lhs, IValue rhs) + { + var enumerator1 = lhs.EnumerateArray(); + var enumerator2 = rhs.EnumerateArray(); + var result1 = enumerator1.MoveNext(); + var result2 = enumerator2.MoveNext(); + while (result1 && result2) + { + var diff = Compare(enumerator1.Current, enumerator2.Current); + if (diff != 0) + { + return diff; + } + result1 = enumerator1.MoveNext(); + result2 = enumerator2.MoveNext(); + } + return result1 ? 1 : result2 ? -1 : 0; + } + + // 2024-04-19: Powertools addition. + private int ObjectComparer(IValue lhs, IValue rhs) + { + // OrderBy performs a stable sort (Note that supports duplicate property names) + using var enumerator1 = lhs.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal).GetEnumerator(); + using var enumerator2 = rhs.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal).GetEnumerator(); + + var result1 = enumerator1.MoveNext(); + var result2 = enumerator2.MoveNext(); + while (result1 && result2) + { + if (enumerator1.Current.Name != enumerator2.Current.Name) + { + return string.Compare(enumerator1.Current.Name, enumerator2.Current.Name, StringComparison.Ordinal); + } + var diff = Compare(enumerator1.Current.Value, enumerator2.Current.Value); + if (diff != 0) + { + return diff; + } + result1 = enumerator1.MoveNext(); + result2 = enumerator2.MoveNext(); + } + + return result1 ? 1 : result2 ? -1 : 0; + } + + int System.Collections.IComparer.Compare(object x, object y) + { + return Compare((IValue)x, (IValue)y); + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ValueEqualityComparer.cs b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ValueEqualityComparer.cs new file mode 100644 index 00000000..73363874 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.JMESPath/Values/ValueEqualityComparer.cs @@ -0,0 +1,157 @@ +/* + * Copyright JsonCons.Net authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AWS.Lambda.Powertools.JMESPath.Values +{ + /// + /// Compares two instances for equality. + /// + internal sealed class ValueEqualityComparer : IEqualityComparer + { + /// + /// Singleton instance of . + /// + internal static ValueEqualityComparer Instance { get; } = new(); + + /// + /// Max Hash depth + /// + private readonly int _maxHashDepth = 100; + + private ValueEqualityComparer() {} + + /// + public bool Equals(IValue lhs, IValue rhs) + { + if (lhs != null && rhs != null && lhs.Type != rhs.Type) + return false; + + if (rhs == null || lhs == null) return false; + + switch (lhs.Type) + { + case JmesPathType.Null: + case JmesPathType.True: + case JmesPathType.False: + return true; + + case JmesPathType.Number: + { + if (lhs.TryGetDecimal(out var dec1) && rhs.TryGetDecimal(out var dec2)) + { + return dec1 == dec2; + } + + if (lhs.TryGetDouble(out var val1) && rhs.TryGetDouble(out var val2)) + { + return Math.Abs(val1 - val2) < 0.000000001; + } + + return false; + } + + case JmesPathType.String: + return lhs.GetString().Equals(rhs.GetString()); + + case JmesPathType.Array: + return lhs.EnumerateArray().SequenceEqual(rhs.EnumerateArray(), this); + + case JmesPathType.Object: + { + // OrderBy performs a stable sort (Note that IValue supports duplicate property names) + using var enumerator1 = lhs.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal) + .GetEnumerator(); + using var enumerator2 = rhs.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal) + .GetEnumerator(); + + var result1 = enumerator1.MoveNext(); + var result2 = enumerator2.MoveNext(); + while (result1 && result2) + { + if (enumerator1.Current.Name != enumerator2.Current.Name) + { + return false; + } + + if (!(Equals(enumerator1.Current.Value, enumerator2.Current.Value))) + { + return false; + } + + result1 = enumerator1.MoveNext(); + result2 = enumerator2.MoveNext(); + } + + return result1 == false && result2 == false; + } + + default: + throw new InvalidOperationException($"Unknown JmesPathType {lhs.Type}"); + } + } + + public int GetHashCode(IValue obj) + { + return ComputeHashCode(obj, 0); + } + + private int ComputeHashCode(IValue element, int depth) + { + var hashCode = element.Type.GetHashCode(); + + switch (element.Type) + { + case JmesPathType.Null: + case JmesPathType.True: + case JmesPathType.False: + break; + + case JmesPathType.Number: + { + element.TryGetDouble(out var dbl); + hashCode += 17 * dbl.GetHashCode(); + break; + } + + case JmesPathType.String: + hashCode += 17 * element.GetString().GetHashCode(); + break; + + case JmesPathType.Array: + if (depth < _maxHashDepth) + foreach (var item in element.EnumerateArray()) + hashCode += 17*ComputeHashCode(item, depth+1); + break; + + case JmesPathType.Object: + foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) + { + hashCode += 17*property.Name.GetHashCode(); + if (depth < _maxHashDepth) + hashCode += 17*ComputeHashCode(property.Value, depth+1); + } + break; + + default: + throw new InvalidOperationException($"Unknown JmesPathType {element.Type}"); + } + return hashCode; + } + } +} diff --git a/libraries/src/Directory.Build.targets b/libraries/src/Directory.Build.targets index 20593976..5844e458 100644 --- a/libraries/src/Directory.Build.targets +++ b/libraries/src/Directory.Build.targets @@ -1,6 +1,6 @@ - + diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index e73f3288..d9c404bd 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -10,7 +10,6 @@ - diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 0b0ff4ad..366aac77 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -545,7 +545,15 @@ private static APIGatewayProxyRequest LoadApiGatewayProxyRequest() }; var eventJson = File.ReadAllText("./resources/apigw_event.json"); - var request = JsonSerializer.Deserialize(eventJson, options); - return request!; + try + { + var request = JsonSerializer.Deserialize(eventJson, options); + return request!; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/AWS.Lambda.Powertools.JMESPath.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/AWS.Lambda.Powertools.JMESPath.Tests.csproj new file mode 100644 index 00000000..bbea95c1 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/AWS.Lambda.Powertools.JMESPath.Tests.csproj @@ -0,0 +1,106 @@ + + + + default + AWS.Lambda.Powertools.JMESpath.Tests + AWS.Lambda.Powertools.JMESpath.Tests + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/GlobalUsings.cs b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/GlobalUsings.cs new file mode 100644 index 00000000..2cdb71da --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/GlobalUsings.cs @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +global using Xunit; \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathExamples.cs b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathExamples.cs new file mode 100644 index 00000000..a1386ea6 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathExamples.cs @@ -0,0 +1,489 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Text.Json; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.JMESPath.Tests; + +public class JmesPathExamples +{ + private readonly ITestOutputHelper _output; + private readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = false }; + + public JmesPathExamples(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Select_With_Powertools_Json_Function() + { + var jsonString = """ + { + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] + } + """; + + using var doc = JsonDocument.Parse(jsonString); + + var transformer = JsonTransformer.Parse("powertools_json(body).customerId"); + using var result = transformer.Transform(doc.RootElement); + + _output.WriteLine(result.RootElement.GetRawText()); + + Assert.Equal("dd4649e6-2484-4993-acb8-0f9123103394", result.RootElement.GetString()); + } + + [Fact] + public void Select_With_Powertools_Base64_Function() + { + var jsonString = """ + { + "body": "eyJjdXN0b21lcklkIjoiZGQ0NjQ5ZTYtMjQ4NC00OTkzLWFjYjgtMGY5MTIzMTAzMzk0In0=", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] + } + """; + + using var doc = JsonDocument.Parse(jsonString); + + var transformer = JsonTransformer.Parse("powertools_base64(body).customerId"); + using var result = transformer.Transform(doc.RootElement); + + _output.WriteLine(result.RootElement.GetRawText()); + + Assert.Equal("dd4649e6-2484-4993-acb8-0f9123103394", result.RootElement.GetString()); + } + + [Fact] + public void Select_With_Powertools_Base64_Gzip_Function() + { + var jsonString = """ + { + "body": "H4sIAAAAAAAAA6tWSi4tLsnPTS3yTFGyUkpJMTEzsUw10zUysTDRNbG0NNZNTE6y0DVIszQ0MjY0MDa2NFGqBQCMzDWgNQAAAA==", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] + } + """; + + using var doc = JsonDocument.Parse(jsonString); + + var transformer = JsonTransformer.Parse("powertools_base64_gzip(body).customerId"); + using var result = transformer.Transform(doc.RootElement); + + _output.WriteLine(result.RootElement.GetRawText()); + + Assert.Equal("dd4649e6-2484-4993-acb8-0f9123103394", result.RootElement.GetString()); + } + + [Fact] + public void FiltersAndMultiselectLists() + { + //Arrange + + var jsonString = """ + { + "people": [ + { + "age": 20, + "other": "foo", + "name": "Bob" + }, + { + "age": 25, + "other": "bar", + "name": "Fred" + }, + { + "age": 30, + "other": "baz", + "name": "George" + } + ] + } + """; + + using var doc = JsonDocument.Parse(jsonString); + + var expectedJson = """[["Fred",25],["George",30]]"""; + + //Act + + var transformer = JsonTransformer.Parse("people[?age > `20`].[name, age]"); + + using var result = transformer.Transform(doc.RootElement); + + var actualJson = JsonSerializer.Serialize(result.RootElement, _serializerOptions); + + //Assert + + _output.WriteLine(actualJson); + Assert.Equal(expectedJson, actualJson); + } + + // Source: https://jmespath.org/examples.html#filters-and-multiselect-hashes + [Fact] + public void FiltersAndMultiselectHashes() + { + //Arrange + + var jsonString = """ + + { + "people": [ + { + "age": 20, + "other": "foo", + "name": "Bob" + }, + { + "age": 25, + "other": "bar", + "name": "Fred" + }, + { + "age": 30, + "other": "baz", + "name": "George" + } + ] + } + + """; + + using var doc = JsonDocument.Parse(jsonString); + var expectedJson = """[{"name":"Fred","age":25},{"name":"George","age":30}]"""; + + // Act + + var transformer = JsonTransformer.Parse("people[?age > `20`].{name: name, age: age}"); + + using var result = transformer.Transform(doc.RootElement); + var actualJson = JsonSerializer.Serialize(result.RootElement, _serializerOptions); + + //Assert + + _output.WriteLine(actualJson); + Assert.Equal(expectedJson, actualJson); + } + + // Source: https://jmespath.org/examples.html#working-with-nested-data + [Fact] + public void WorkingWithNestedData() + { + // Arrange + + var jsonString = """ + + { + "reservations": [ + { + "instances": [ + {"type": "small", + "state": {"name": "running"}, + "tags": [{"Key": "Name", + "Values": ["Web"]}, + {"Key": "version", + "Values": ["1"]}]}, + {"type": "large", + "state": {"name": "stopped"}, + "tags": [{"Key": "Name", + "Values": ["Web"]}, + {"Key": "version", + "Values": ["1"]}]} + ] + }, { + "instances": [ + {"type": "medium", + "state": {"name": "terminated"}, + "tags": [{"Key": "Name", + "Values": ["Web"]}, + {"Key": "version", + "Values": ["1"]}]}, + {"type": "xlarge", + "state": {"name": "running"}, + "tags": [{"Key": "Name", + "Values": ["DB"]}, + {"Key": "version", + "Values": ["1"]}]} + ] + } + ] + } + + """; + + using var doc = JsonDocument.Parse(jsonString); + var expectedJson = """[["Web","small","running"],["Web","large","stopped"],["Web","medium","terminated"],["DB","xlarge","running"]]"""; + + // Act + + var transformer = + JsonTransformer.Parse("reservations[].instances[].[tags[?Key=='Name'].Values[] | [0], type, state.name]"); + + using var result = transformer.Transform(doc.RootElement); + var actualJson = JsonSerializer.Serialize(result.RootElement, _serializerOptions); + + //Assert + + _output.WriteLine(actualJson); + Assert.Equal(expectedJson, actualJson); + } + + // Source: https://jmespath.org/examples.html#filtering-and-selecting-nested-data + [Fact] + public void FilteringAndSelectingNestedData() + { + //Arrange + + var jsonString = """ + + { + "people": [ + { + "general": { + "id": 100, + "age": 20, + "other": "foo", + "name": "Bob" + }, + "history": { + "first_login": "2014-01-01", + "last_login": "2014-01-02" + } + }, + { + "general": { + "id": 101, + "age": 30, + "other": "bar", + "name": "Bill" + }, + "history": { + "first_login": "2014-05-01", + "last_login": "2014-05-02" + } + } + ] + } + + """; + + using var doc = JsonDocument.Parse(jsonString); + var expectedJson = """{"id":100,"age":20,"other":"foo","name":"Bob"}"""; + + // Act + + var transformer = JsonTransformer.Parse("people[?general.id==`100`].general | [0]"); + using var result = transformer.Transform(doc.RootElement); + var actualJson = JsonSerializer.Serialize(result.RootElement, _serializerOptions); + + //Assert + + _output.WriteLine(actualJson); + Assert.Equal(expectedJson, actualJson); + } + + // Source: https://jmespath.org/examples.html#using-functions + [Fact] + public void UsingFunctions() + { + // Arrange + + var jsonString = """ + + { + "Contents": [ + { + "Date": "2014-12-21T05:18:08.000Z", + "Key": "logs/bb", + "Size": 303 + }, + { + "Date": "2014-12-20T05:19:10.000Z", + "Key": "logs/aa", + "Size": 308 + }, + { + "Date": "2014-12-20T05:19:12.000Z", + "Key": "logs/qux", + "Size": 297 + }, + { + "Date": "2014-11-20T05:22:23.000Z", + "Key": "logs/baz", + "Size": 329 + }, + { + "Date": "2014-12-20T05:25:24.000Z", + "Key": "logs/bar", + "Size": 604 + }, + { + "Date": "2014-12-20T05:27:12.000Z", + "Key": "logs/foo", + "Size": 647 + } + ] + } + + """; + + using var doc = JsonDocument.Parse(jsonString); + var expectedJson = """[{"Key":"logs/baz","Size":329},{"Key":"logs/aa","Size":308},{"Key":"logs/qux","Size":297},{"Key":"logs/bar","Size":604},{"Key":"logs/foo","Size":647},{"Key":"logs/bb","Size":303}]"""; + + // Act + + var transformer = JsonTransformer.Parse("sort_by(Contents, &Date)[*].{Key: Key, Size: Size}"); + using var result = transformer.Transform(doc.RootElement); + var actualJson = JsonSerializer.Serialize(result.RootElement, _serializerOptions); + + //Assert + + _output.WriteLine(actualJson); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void SortBySize() + { + // Arrange + + var jsonString = """ + + { + "Contents": [ + { + "Date": "2014-12-21T05:18:08.000Z", + "Key": "logs/bb", + "Size": 303 + }, + { + "Date": "2014-12-20T05:19:10.000Z", + "Key": "logs/aa", + "Size": 308 + }, + { + "Date": "2014-12-20T05:19:12.000Z", + "Key": "logs/qux", + "Size": 297 + }, + { + "Date": "2014-11-20T05:22:23.000Z", + "Key": "logs/baz", + "Size": 329 + }, + { + "Date": "2014-12-20T05:25:24.000Z", + "Key": "logs/bar", + "Size": 604 + }, + { + "Date": "2014-12-20T05:27:12.000Z", + "Key": "logs/foo", + "Size": 647 + } + ] + } + + """; + + using var doc = JsonDocument.Parse(jsonString); + var expectedJson = """[{"Size":297},{"Size":303},{"Size":308},{"Size":329},{"Size":604},{"Size":647}]"""; + + // Act + + var transformer = JsonTransformer.Parse("sort_by(Contents, &Size)[*].{Size: Size}"); + using var result = transformer.Transform(doc.RootElement); + var actualJson = JsonSerializer.Serialize(result.RootElement, _serializerOptions); + + //Assert + + _output.WriteLine(actualJson); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void KeyOfInterest() + { + var jsonString = """ + + { + "Data":[ + { + "KeyOfInterest":true, + "AnotherKey":true + }, + { + "KeyOfInterest":false, + "AnotherKey":true + }, + { + "KeyOfInterest":true, + "AnotherKey":true + } + ] + } + + """; + + using var doc = JsonDocument.Parse(jsonString); + + var expectedJson1 = "[true,false,true]"; + var expectedJson2 = """[{"Key of Interest":true,"Another Key":true},{"Key of Interest":false,"Another Key":true},{"Key of Interest":true,"Another Key":true}]"""; + + // Act + + var result1 = JsonTransformer.Transform(doc.RootElement, + "Data[*].KeyOfInterest"); + var result2 = JsonTransformer.Transform(doc.RootElement, + "Data[*].{\"Key of Interest\" : KeyOfInterest, \"Another Key\": AnotherKey}"); + + var actualJson1 = JsonSerializer.Serialize(result1); + var actualJson2 = JsonSerializer.Serialize(result2, _serializerOptions); + + // Assert + + _output.WriteLine(JsonSerializer.Serialize(result1)); + _output.WriteLine(JsonSerializer.Serialize(result2, _serializerOptions)); + + Assert.Equal(expectedJson1, actualJson1); + Assert.Equal(expectedJson2, actualJson2); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathTests.cs b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathTests.cs new file mode 100644 index 00000000..7a82c697 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/JmesPathTests.cs @@ -0,0 +1,130 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +using System.Text.Json; +using AWS.Lambda.Powertools.JMESPath.Utilities; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.JMESPath.Tests; + +public class JmesPathTests +{ + private readonly ITestOutputHelper _output; + + public JmesPathTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [InlineData("test_files/basic.json")] + [InlineData("test_files/benchmarks.json")] + [InlineData("test_files/boolean.json")] + [InlineData("test_files/current.json")] + [InlineData("test_files/escape.json")] + [InlineData("test_files/filters.json")] + [InlineData("test_files/identifiers.json")] + [InlineData("test_files/indices.json")] + [InlineData("test_files/literal.json")] + [InlineData("test_files/multiselect.json")] + [InlineData("test_files/pipe.json")] + [InlineData("test_files/slice.json")] + [InlineData("test_files/unicode.json")] + [InlineData("test_files/syntax.json")] + [InlineData("test_files/wildcard.json")] + [InlineData("test_files/example.json")] + [InlineData("test_files/functions.json")] + [InlineData("test_files/test.json")] + [InlineData("test_files/apigw_event.json")] + [InlineData("test_files/apigw_event_2.json")] + [InlineData("test_files/sns.json")] + [InlineData("test_files/sqs.json")] + [InlineData("test_files/cloud_watch_logs.json")] + [InlineData("test_files/kinesis_data_stream.json")] + public void RunJmesPathTests(string path) + { + _output.WriteLine($"Test {path}"); + + var text = File.ReadAllText(path); + var jsonOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip + }; + using var doc = JsonDocument.Parse(text, jsonOptions); + + var testsEnumerable = doc.RootElement.EnumerateArray(); + var comparer = JsonElementEqualityComparer.Instance; + + foreach (var testGroup in testsEnumerable) + { + var given = testGroup.GetProperty("given"); + var testCases = testGroup.GetProperty("cases"); + var testCasesEnumerable = testCases.EnumerateArray(); + foreach (var testCase in testCasesEnumerable) + { + var exprElement = testCase.GetProperty("expression"); + + try + { + if (testCase.TryGetProperty("error", out var expected)) + { + var msg = expected.GetString(); + //Debug.WriteLine($"message: {msg}"); + if (msg != null && (msg.Equals("syntax") || msg.Equals("invalid-arity") || msg.Equals("unknown-function") || msg.Equals("invalid-value"))) + { + Assert.Throws(() => JsonTransformer.Parse(exprElement.ToString())); + } + else + { + var expr = JsonTransformer.Parse(exprElement.ToString()); + try + { + var result = expr.Transform(given); + using var nullValue = JsonDocument.Parse("null"); + var success = comparer.Equals(result.RootElement, nullValue.RootElement); + Assert.True(success); + } + catch (InvalidOperationException) + { } + } + } + else if (testCase.TryGetProperty("result", out expected)) + { + var expr = JsonTransformer.Parse(exprElement.ToString()); + var result = expr.Transform(given); + + _output.WriteLine("File: {0}", path); + + // _output.WriteLine($"Document: {given}"); + _output.WriteLine($"Path: {exprElement}"); + _output.WriteLine($"Expected: {JsonSerializer.Serialize(expected)}"); + _output.WriteLine($"Result: {JsonSerializer.Serialize(result)}"); + + Assert.True(comparer.Equals(result.RootElement,expected)); + + } + } + catch (Exception e) + { + _output.WriteLine("File: {0}", path); + _output.WriteLine($"Document: {given}"); + _output.WriteLine($"Path: {exprElement}"); + _output.WriteLine("Error: {0}", e.Message); + throw; + } + } + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/apigw_event.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/apigw_event.json new file mode 100644 index 00000000..159bc270 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/apigw_event.json @@ -0,0 +1,76 @@ +[ + { + "given": { + "body": "{\"message\": \"Lambda rocks\", \"id\": 43876123454654}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + }, + "cases": [ + { + "expression": "body.id", + "result": 43876123454654 + }, + { + "expression": "powertools_json(body).id", + "result": 43876123454654 + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/apigw_event_2.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/apigw_event_2.json new file mode 100644 index 00000000..432697d9 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/apigw_event_2.json @@ -0,0 +1,54 @@ +[ + { + "given": { + "version": "2.0", + "routeKey": "ANY /createpayment", + "rawPath": "/createpayment", + "rawQueryString": "", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/createpayment", + "protocol": "HTTP/1.1", + "sourceIp": "ip", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "ANY /createpayment", + "stage": "$default", + "time": "10/Feb/2021:13:40:43 +0000", + "timeEpoch": 1612964443723 + }, + "body": {"user_id":"xyz","product_id":"123456789"}, + "body64": "eyJ1c2VyX2lkIjoieHl6IiwicHJvZHVjdF9pZCI6IjEyMzQ1Njc4OSJ9", + "bodygzip": "H4sIAAAAAAAAA6tWKi1OLYrPTFGyUqqorFLSUSooyk8pTS6BCBkaGZuYmplbWCrVAgApzA/LKgAAAA==", + "isBase64Encoded": false + }, + "cases": [ + { + "expression": "body.[user_id,product_id]", + "result": ["xyz",123456789] + }, + { + "expression": "powertools_json(body).[user_id,product_id]", + "result": ["xyz",123456789] + }, + { + "expression": "powertools_base64(body64).[user_id,product_id]", + "result": ["xyz",123456789] + }, + { + "expression": "powertools_base64_gzip(bodygzip).[user_id,product_id]", + "result": ["xyz",123456789] + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/basic.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/basic.json new file mode 100644 index 00000000..d550e969 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/basic.json @@ -0,0 +1,96 @@ +[{ + "given": + {"foo": {"bar": {"baz": "correct"}}}, + "cases": [ + { + "expression": "foo", + "result": {"bar": {"baz": "correct"}} + }, + { + "expression": "foo.bar", + "result": {"baz": "correct"} + }, + { + "expression": "foo.bar.baz", + "result": "correct" + }, + { + "expression": "foo\n.\nbar\n.baz", + "result": "correct" + }, + { + "expression": "foo.bar.baz.bad", + "result": null + }, + { + "expression": "foo.bar.bad", + "result": null + }, + { + "expression": "foo.bad", + "result": null + }, + { + "expression": "bad", + "result": null + }, + { + "expression": "bad.morebad.morebad", + "result": null + } + ] +}, +{ + "given": + {"foo": {"bar": ["one", "two", "three"]}}, + "cases": [ + { + "expression": "foo", + "result": {"bar": ["one", "two", "three"]} + }, + { + "expression": "foo.bar", + "result": ["one", "two", "three"] + } + ] +}, +{ + "given": ["one", "two", "three"], + "cases": [ + { + "expression": "one", + "result": null + }, + { + "expression": "two", + "result": null + }, + { + "expression": "three", + "result": null + }, + { + "expression": "one.two", + "result": null + } + ] +}, +{ + "given": + {"foo": {"1": ["one", "two", "three"], "-1": "bar"}}, + "cases": [ + { + "expression": "foo.\"1\"", + "result": ["one", "two", "three"] + }, + { + "expression": "foo.\"1\"[0]", + "result": "one" + }, + { + "expression": "foo.\"-1\"", + "result": "bar" + } + ] +} +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/benchmarks.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/benchmarks.json new file mode 100644 index 00000000..024a5904 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/benchmarks.json @@ -0,0 +1,138 @@ +[ + { + "given": { + "long_name_for_a_field": true, + "a": { + "b": { + "c": { + "d": { + "e": { + "f": { + "g": { + "h": { + "i": { + "j": { + "k": { + "l": { + "m": { + "n": { + "o": { + "p": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "b": true, + "c": { + "d": true + } + }, + "cases": [ + { + "comment": "simple field", + "expression": "b", + "bench": "full" + }, + { + "comment": "simple subexpression", + "expression": "c.d", + "bench": "full" + }, + { + "comment": "deep field selection no match", + "expression": "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s", + "bench": "full" + }, + { + "comment": "deep field selection", + "expression": "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p", + "bench": "full" + }, + { + "comment": "simple or", + "expression": "not_there || b", + "bench": "full" + } + ] + }, + { + "given": { + "a":0,"b":1,"c":2,"d":3,"e":4,"f":5,"g":6,"h":7,"i":8,"j":9,"k":10, + "l":11,"m":12,"n":13,"o":14,"p":15,"q":16,"r":17,"s":18,"t":19,"u":20, + "v":21,"w":22,"x":23,"y":24,"z":25 + }, + "cases": [ + { + "comment": "deep ands", + "expression": "a && b && c && d && e && f && g && h && i && j && k && l && m && n && o && p && q && r && s && t && u && v && w && x && y && z", + "bench": "full" + }, + { + "comment": "deep ors", + "expression": "z || y || x || w || v || u || t || s || r || q || p || o || n || m || l || k || j || i || h || g || f || e || d || c || b || a", + "bench": "full" + }, + { + "comment": "lots of summing", + "expression": "sum([z, y, x, w, v, u, t, s, r, q, p, o, n, m, l, k, j, i, h, g, f, e, d, c, b, a])", + "bench": "full" + }, + { + "comment": "lots of function application", + "expression": "sum([z, sum([y, sum([x, sum([w, sum([v, sum([u, sum([t, sum([s, sum([r, sum([q, sum([p, sum([o, sum([n, sum([m, sum([l, sum([k, sum([j, sum([i, sum([h, sum([g, sum([f, sum([e, sum([d, sum([c, sum([b, a])])])])])])])])])])])])])])])])])])])])])])])])])", + "bench": "full" + }, + { + "comment": "lots of multi list", + "expression": "[z, y, x, w, v, u, t, s, r, q, p, o, n, m, l, k, j, i, h, g, f, e, d, c, b, a]", + "bench": "full" + } + ] + }, + { + "given": {}, + "cases": [ + { + "comment": "field 50", + "expression": "j49.j48.j47.j46.j45.j44.j43.j42.j41.j40.j39.j38.j37.j36.j35.j34.j33.j32.j31.j30.j29.j28.j27.j26.j25.j24.j23.j22.j21.j20.j19.j18.j17.j16.j15.j14.j13.j12.j11.j10.j9.j8.j7.j6.j5.j4.j3.j2.j1.j0", + "bench": "parse" + }, + { + "comment": "pipe 50", + "expression": "j49|j48|j47|j46|j45|j44|j43|j42|j41|j40|j39|j38|j37|j36|j35|j34|j33|j32|j31|j30|j29|j28|j27|j26|j25|j24|j23|j22|j21|j20|j19|j18|j17|j16|j15|j14|j13|j12|j11|j10|j9|j8|j7|j6|j5|j4|j3|j2|j1|j0", + "bench": "parse" + }, + { + "comment": "index 50", + "expression": "[49][48][47][46][45][44][43][42][41][40][39][38][37][36][35][34][33][32][31][30][29][28][27][26][25][24][23][22][21][20][19][18][17][16][15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]", + "bench": "parse" + }, + { + "comment": "long raw string literal", + "expression": "'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'", + "bench": "parse" + }, + { + "comment": "deep projection 104", + "expression": "a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*].a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*].a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*].a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*]", + "bench": "parse" + }, + { + "comment": "filter projection", + "expression": "foo[?bar > baz][?qux > baz]", + "bench": "parse" + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/boolean.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/boolean.json new file mode 100644 index 00000000..60635acb --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/boolean.json @@ -0,0 +1,275 @@ +[ + { + "given": { + "outer": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + } + }, + "cases": [ + { + "expression": "outer.foo || outer.bar", + "result": "foo" + }, + { + "expression": "outer.foo||outer.bar", + "result": "foo" + }, + { + "expression": "outer.bar || outer.baz", + "result": "bar" + }, + { + "expression": "outer.bar||outer.baz", + "result": "bar" + }, + { + "expression": "outer.bad || outer.foo", + "result": "foo" + }, + { + "expression": "outer.bad||outer.foo", + "result": "foo" + }, + { + "expression": "outer.foo || outer.bad", + "result": "foo" + }, + { + "expression": "outer.foo||outer.bad", + "result": "foo" + }, + { + "expression": "outer.bad || outer.alsobad", + "result": null + }, + { + "expression": "outer.bad||outer.alsobad", + "result": null + } + ] + }, + { + "given": { + "outer": { + "foo": "foo", + "bool": false, + "empty_list": [], + "empty_string": "" + } + }, + "cases": [ + { + "expression": "outer.empty_string || outer.foo", + "result": "foo" + }, + { + "expression": "outer.nokey || outer.bool || outer.empty_list || outer.empty_string || outer.foo", + "result": "foo" + } + ] + }, + { + "given": { + "True": true, + "False": false, + "Number": 5, + "EmptyList": [], + "Zero": 0 + }, + "cases": [ + { + "expression": "True && False", + "result": false + }, + { + "expression": "False && True", + "result": false + }, + { + "expression": "True && True", + "result": true + }, + { + "expression": "False && False", + "result": false + }, + { + "expression": "True && Number", + "result": 5 + }, + { + "expression": "Number && True", + "result": true + }, + { + "expression": "Number && False", + "result": false + }, + { + "expression": "Number && EmptyList", + "result": [] + }, + { + "expression": "Number && True", + "result": true + }, + { + "expression": "EmptyList && True", + "result": [] + }, + { + "expression": "EmptyList && False", + "result": [] + }, + { + "expression": "True || False", + "result": true + }, + { + "expression": "True || True", + "result": true + }, + { + "expression": "False || True", + "result": true + }, + { + "expression": "False || False", + "result": false + }, + { + "expression": "Number || EmptyList", + "result": 5 + }, + { + "expression": "Number || True", + "result": 5 + }, + { + "expression": "Number || True && False", + "result": 5 + }, + { + "expression": "(Number || True) && False", + "result": false + }, + { + "expression": "Number || (True && False)", + "result": 5 + }, + { + "expression": "!True", + "result": false + }, + { + "expression": "!False", + "result": true + }, + { + "expression": "!Number", + "result": false + }, + { + "expression": "!EmptyList", + "result": true + }, + { + "expression": "True && !False", + "result": true + }, + { + "expression": "True && !EmptyList", + "result": true + }, + { + "expression": "!False && !EmptyList", + "result": true + }, + { + "expression": "!(True && False)", + "result": true + }, + { + "expression": "!Zero", + "result": false + }, + { + "expression": "!!Zero", + "result": true + } + ] + }, + { + "given": { + "one": 1, + "two": 2, + "three": 3, + "emptylist": [], + "boolvalue": false + }, + "cases": [ + { + "expression": "one < two", + "result": true + }, + { + "expression": "one <= two", + "result": true + }, + { + "expression": "one == one", + "result": true + }, + { + "expression": "one == two", + "result": false + }, + { + "expression": "one > two", + "result": false + }, + { + "expression": "one >= two", + "result": false + }, + { + "expression": "one != two", + "result": true + }, + { + "expression": "emptylist < one", + "result": null + }, + { + "expression": "emptylist < nullvalue", + "result": null + }, + { + "expression": "emptylist < boolvalue", + "result": null + }, + { + "expression": "one < boolvalue", + "result": null + }, + { + "expression": "one < two && three > one", + "result": true + }, + { + "expression": "one < two || three > one", + "result": true + }, + { + "expression": "one < two || three < one", + "result": true + }, + { + "expression": "two < one || three < one", + "result": false + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/cloud_watch_logs.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/cloud_watch_logs.json new file mode 100644 index 00000000..f779f00d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/cloud_watch_logs.json @@ -0,0 +1,15 @@ +[ + { + "given": { + "awslogs": { + "data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA==" + } + }, + "cases": [ + { + "expression": "awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]", + "result": [{"id":"eventId1","timestamp":1440442987000,"message":"[ERROR] First test message"},{"id":"eventId2","timestamp":1440442987001,"message":"[ERROR] Second test message"}] + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/current.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/current.json new file mode 100644 index 00000000..0c26248d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/current.json @@ -0,0 +1,25 @@ +[ + { + "given": { + "foo": [{"name": "a"}, {"name": "b"}], + "bar": {"baz": "qux"} + }, + "cases": [ + { + "expression": "@", + "result": { + "foo": [{"name": "a"}, {"name": "b"}], + "bar": {"baz": "qux"} + } + }, + { + "expression": "@.bar", + "result": {"baz": "qux"} + }, + { + "expression": "@.foo[0]", + "result": {"name": "a"} + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/escape.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/escape.json new file mode 100644 index 00000000..4a62d951 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/escape.json @@ -0,0 +1,46 @@ +[{ + "given": { + "foo.bar": "dot", + "foo bar": "space", + "foo\nbar": "newline", + "foo\"bar": "doublequote", + "c:\\\\windows\\path": "windows", + "/unix/path": "unix", + "\"\"\"": "threequotes", + "bar": {"baz": "qux"} + }, + "cases": [ + { + "expression": "\"foo.bar\"", + "result": "dot" + }, + { + "expression": "\"foo bar\"", + "result": "space" + }, + { + "expression": "\"foo\\nbar\"", + "result": "newline" + }, + { + "expression": "\"foo\\\"bar\"", + "result": "doublequote" + }, + { + "expression": "\"c:\\\\\\\\windows\\\\path\"", + "result": "windows" + }, + { + "expression": "\"/unix/path\"", + "result": "unix" + }, + { + "expression": "\"\\\"\\\"\\\"\"", + "result": "threequotes" + }, + { + "expression": "\"bar\".\"baz\"", + "result": "qux" + } + ] +}] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/example.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/example.json new file mode 100644 index 00000000..871aa8dc --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/example.json @@ -0,0 +1,50 @@ +[ + { + "given": { + "_id": "63ba60670fe420f2fb346866", + "isActive": true, + "balance": "$2,285.51", + "age": 20, + "eyeColor": "blue", + "name": "Eva Sharpe", + "email": "evasharpe@zaggles.com", + "phone": "+1 (950) 479-2130", + "registered": "2023-01-08T08:07:44.1787922+00:00", + "latitude": 46.325291, + "longitude": 5.211461, + "friends": [ + { + "id": 0, + "name": "Nielsen Casey", + "age": 19 + }, + { + "id": 1, + "name": "Carlene Long", + "age": 38 + } + ] + }, + "cases": [ + { + "expression": "balance", + "result": "$2,285.51" + }, + { + "expression": "to_string(latitude)", + "result": "46.325291" + }, + { + "expression": "friends[*].name", + "result": [ + "Nielsen Casey", + "Carlene Long" + ] + }, + { + "expression": "{email: email, name: name}", + "result": {"email": "evasharpe@zaggles.com", "name": "Eva Sharpe"} + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/filters.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/filters.json new file mode 100644 index 00000000..b2141a4e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/filters.json @@ -0,0 +1,468 @@ +[ + { + "given": {"foo": [{"name": "a"}, {"name": "b"}]}, + "cases": [ + { + "comment": "Matching a literal `a`", + "expression": "foo[?name == 'a']", + "result": [{"name": "a"}] + } + ] + }, + { + "given": {"foo": [0, 1], "bar": [2, 3]}, + "cases": [ + { + "comment": "Matching a literal `0`", + "expression": "*[?[0] == `0`]", + "result": [[], []] + } + ] + }, + { + "given": {"foo": [{"first": "foo", "last": "bar"}, + {"first": "foo", "last": "foo"}, + {"first": "foo", "last": "baz"}]}, + "cases": [ + { + "comment": "Matching an expression", + "expression": "foo[?first == last]", + "result": [{"first": "foo", "last": "foo"}] + }, + { + "comment": "Verify projection created from filter", + "expression": "foo[?first == last].first", + "result": ["foo"] + } + ] + }, + { + "given": {"foo": [{"age": 20}, + {"age": 25}, + {"age": 30}]}, + "cases": [ + { + "comment": "Greater than with a number", + "expression": "foo[?age > `25`]", + "result": [{"age": 30}] + }, + { + "expression": "foo[?age >= `25`]", + "result": [{"age": 25}, {"age": 30}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age > `30`]", + "result": [] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age < `25`]", + "result": [{"age": 20}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age <= `25`]", + "result": [{"age": 20}, {"age": 25}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age < `20`]", + "result": [] + }, + { + "expression": "foo[?age == `20`]", + "result": [{"age": 20}] + }, + { + "expression": "foo[?age != `20`]", + "result": [{"age": 25}, {"age": 30}] + } + ] + }, + { + "given": {"foo": [{"top": {"name": "a"}}, + {"top": {"name": "b"}}]}, + "cases": [ + { + "comment": "Filter with subexpression", + "expression": "foo[?top.name == 'a']", + "result": [{"top": {"name": "a"}}] + } + ] + }, + { + "given": {"foo": [{"top": {"first": "foo", "last": "bar"}}, + {"top": {"first": "foo", "last": "foo"}}, + {"top": {"first": "foo", "last": "baz"}}]}, + "cases": [ + { + "comment": "Matching an expression", + "expression": "foo[?top.first == top.last]", + "result": [{"top": {"first": "foo", "last": "foo"}}] + }, + { + "comment": "Matching a JSON array", + "expression": "foo[?top == `{\"first\": \"foo\", \"last\": \"bar\"}`]", + "result": [{"top": {"first": "foo", "last": "bar"}}] + } + ] + }, + { + "given": {"foo": [ + {"key": true}, + {"key": false}, + {"key": 0}, + {"key": 1}, + {"key": [0]}, + {"key": {"bar": [0]}}, + {"key": null}, + {"key": [1]}, + {"key": {"a":2}} + ]}, + "cases": [ + { + "expression": "foo[?key == `true`]", + "result": [{"key": true}] + }, + { + "expression": "foo[?key == `false`]", + "result": [{"key": false}] + }, + { + "expression": "foo[?key == `0`]", + "result": [{"key": 0}] + }, + { + "expression": "foo[?key == `1`]", + "result": [{"key": 1}] + }, + { + "expression": "foo[?key == `[0]`]", + "result": [{"key": [0]}] + }, + { + "expression": "foo[?key == `{\"bar\": [0]}`]", + "result": [{"key": {"bar": [0]}}] + }, + { + "expression": "foo[?key == `null`]", + "result": [{"key": null}] + }, + { + "expression": "foo[?key == `[1]`]", + "result": [{"key": [1]}] + }, + { + "expression": "foo[?key == `{\"a\":2}`]", + "result": [{"key": {"a":2}}] + }, + { + "expression": "foo[?`true` == key]", + "result": [{"key": true}] + }, + { + "expression": "foo[?`false` == key]", + "result": [{"key": false}] + }, + { + "expression": "foo[?`0` == key]", + "result": [{"key": 0}] + }, + { + "expression": "foo[?`1` == key]", + "result": [{"key": 1}] + }, + { + "expression": "foo[?`[0]` == key]", + "result": [{"key": [0]}] + }, + { + "expression": "foo[?`{\"bar\": [0]}` == key]", + "result": [{"key": {"bar": [0]}}] + }, + { + "expression": "foo[?`null` == key]", + "result": [{"key": null}] + }, + { + "expression": "foo[?`[1]` == key]", + "result": [{"key": [1]}] + }, + { + "expression": "foo[?`{\"a\":2}` == key]", + "result": [{"key": {"a":2}}] + }, + { + "expression": "foo[?key != `true`]", + "result": [{"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `false`]", + "result": [{"key": true}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `0`]", + "result": [{"key": true}, {"key": false}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `1`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `null`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `[1]`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `{\"a\":2}`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}] + }, + { + "expression": "foo[?`true` != key]", + "result": [{"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`false` != key]", + "result": [{"key": true}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`0` != key]", + "result": [{"key": true}, {"key": false}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`1` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`null` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`[1]` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`{\"a\":2}` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}] + } + ] + }, + { + "given": {"reservations": [ + {"instances": [ + {"foo": 1, "bar": 2}, {"foo": 1, "bar": 3}, + {"foo": 1, "bar": 2}, {"foo": 2, "bar": 1}]}]}, + "cases": [ + { + "expression": "reservations[].instances[?bar==`1`]", + "result": [[{"foo": 2, "bar": 1}]] + }, + { + "expression": "reservations[*].instances[?bar==`1`]", + "result": [[{"foo": 2, "bar": 1}]] + }, + { + "expression": "reservations[].instances[?bar==`1`][]", + "result": [{"foo": 2, "bar": 1}] + } + ] + }, + { + "given": { + "baz": "other", + "foo": [ + {"bar": 1}, {"bar": 2}, {"bar": 3}, {"bar": 4}, {"bar": 1, "baz": 2} + ] + }, + "cases": [ + { + "expression": "foo[?bar==`1`].bar[0]", + "result": [] + } + ] + }, + { + "given": { + "foo": [ + {"a": 1, "b": {"c": "x"}}, + {"a": 1, "b": {"c": "y"}}, + {"a": 1, "b": {"c": "z"}}, + {"a": 2, "b": {"c": "z"}}, + {"a": 1, "baz": 2} + ] + }, + "cases": [ + { + "expression": "foo[?a==`1`].b.c", + "result": ["x", "y", "z"] + } + ] + }, + { + "given": {"foo": [{"name": "a"}, {"name": "b"}, {"name": "c"}]}, + "cases": [ + { + "comment": "Filter with or expression", + "expression": "foo[?name == 'a' || name == 'b']", + "result": [{"name": "a"}, {"name": "b"}] + }, + { + "expression": "foo[?name == 'a' || name == 'e']", + "result": [{"name": "a"}] + }, + { + "expression": "foo[?name == 'a' || name == 'b' || name == 'c']", + "result": [{"name": "a"}, {"name": "b"}, {"name": "c"}] + } + ] + }, + { + "given": {"foo": [{"a": 1, "b": 2}, {"a": 1, "b": 3}]}, + "cases": [ + { + "comment": "Filter with and expression", + "expression": "foo[?a == `1` && b == `2`]", + "result": [{"a": 1, "b": 2}] + }, + { + "expression": "foo[?a == `1` && b == `4`]", + "result": [] + } + ] + }, + { + "given": {"foo": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}]}, + "cases": [ + { + "comment": "Filter with Or and And expressions", + "expression": "foo[?c == `3` || a == `1` && b == `4`]", + "result": [{"a": 1, "b": 2, "c": 3}] + }, + { + "expression": "foo[?b == `2` || a == `3` && b == `4`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?a == `3` && b == `4` || b == `2`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?(a == `3` && b == `4`) || b == `2`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?((a == `3` && b == `4`)) || b == `2`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?a == `3` && (b == `4` || b == `2`)]", + "result": [{"a": 3, "b": 4}] + }, + { + "expression": "foo[?a == `3` && ((b == `4` || b == `2`))]", + "result": [{"a": 3, "b": 4}] + } + ] + }, + { + "given": {"foo": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}]}, + "cases": [ + { + "comment": "Verify precedence of or/and expressions", + "expression": "foo[?a == `1` || b ==`2` && c == `5`]", + "result": [{"a": 1, "b": 2, "c": 3}] + }, + { + "comment": "Parentheses can alter precedence", + "expression": "foo[?(a == `1` || b ==`2`) && c == `5`]", + "result": [] + }, + { + "comment": "Not expressions combined with and/or", + "expression": "foo[?!(a == `1` || b ==`2`)]", + "result": [{"a": 3, "b": 4}] + } + ] + }, + { + "given": { + "foo": [ + {"key": true}, + {"key": false}, + {"key": []}, + {"key": {}}, + {"key": [0]}, + {"key": {"a": "b"}}, + {"key": 0}, + {"key": 1}, + {"key": null}, + {"notkey": true} + ] + }, + "cases": [ + { + "comment": "Unary filter expression", + "expression": "foo[?key]", + "result": [ + {"key": true}, {"key": [0]}, {"key": {"a": "b"}}, + {"key": 0}, {"key": 1} + ] + }, + { + "comment": "Unary not filter expression", + "expression": "foo[?!key]", + "result": [ + {"key": false}, {"key": []}, {"key": {}}, + {"key": null}, {"notkey": true} + ] + }, + { + "comment": "Equality with null RHS", + "expression": "foo[?key == `null`]", + "result": [ + {"key": null}, {"notkey": true} + ] + } + ] + }, + { + "given": { + "foo": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + "cases": [ + { + "comment": "Using @ in a filter expression", + "expression": "foo[?@ < `5`]", + "result": [0, 1, 2, 3, 4] + }, + { + "comment": "Using @ in a filter expression", + "expression": "foo[?`5` > @]", + "result": [0, 1, 2, 3, 4] + }, + { + "comment": "Using @ in a filter expression", + "expression": "foo[?@ == @]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/functions.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/functions.json new file mode 100644 index 00000000..d2ec9369 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/functions.json @@ -0,0 +1,829 @@ +[{ + "given": + { + "foo": -1, + "zero": 0, + "numbers": [-1, 3, 4, 5], + "array": [-1, 3, 4, 5, "a", "100"], + "strings": ["a", "b", "c"], + "decimals": [1.01, 1.2, -1.5], + "str": "Str", + "false": false, + "empty_list": [], + "empty_hash": {}, + "objects": {"foo": "bar", "bar": "baz"}, + "null_key": null + }, + "cases": [ + { + "expression": "abs(foo)", + "result": 1 + }, + { + "expression": "abs(foo)", + "result": 1 + }, + { + "expression": "abs(str)", + "error": "invalid-type" + }, + { + "expression": "abs(array[1])", + "result": 3 + }, + { + "expression": "abs(array[1])", + "result": 3 + }, + { + "expression": "abs(`false`)", + "error": "invalid-type" + }, + { + "expression": "abs(`-24`)", + "result": 24 + }, + { + "expression": "abs(`-24`)", + "result": 24 + }, + { + "expression": "abs(`1`, `2`)", + "error": "invalid-arity" + }, + { + "expression": "abs()", + "error": "invalid-arity" + }, + { + "expression": "unknown_function(`1`, `2`)", + "error": "unknown-function" + }, + { + "expression": "avg(numbers)", + "result": 2.75 + }, + { + "expression": "avg(array)", + "error": "invalid-type" + }, + { + "expression": "avg('abc')", + "error": "invalid-type" + }, + { + "expression": "avg(foo)", + "error": "invalid-type" + }, + { + "expression": "avg(@)", + "error": "invalid-type" + }, + { + "expression": "avg(strings)", + "error": "invalid-type" + }, + { + "expression": "avg(empty_list)", + "result": null + }, + { + "expression": "ceil(`1.2`)", + "result": 2 + }, + { + "expression": "ceil(decimals[0])", + "result": 2 + }, + { + "expression": "ceil(decimals[1])", + "result": 2 + }, + { + "expression": "ceil(decimals[2])", + "result": -1 + }, + { + "expression": "ceil('string')", + "error": "invalid-type" + }, + { + "expression": "contains('abc', 'a')", + "result": true + }, + { + "expression": "contains('abc', 'd')", + "result": false + }, + { + "expression": "contains(`false`, 'd')", + "error": "invalid-type" + }, + { + "expression": "contains(strings, 'a')", + "result": true + }, + { + "expression": "contains(decimals, `1.2`)", + "result": true + }, + { + "expression": "contains(decimals, `false`)", + "result": false + }, + { + "expression": "ends_with(str, 'r')", + "result": true + }, + { + "expression": "ends_with(str, 'tr')", + "result": true + }, + { + "expression": "ends_with(str, 'Str')", + "result": true + }, + { + "expression": "ends_with(str, 'SStr')", + "result": false + }, + { + "expression": "ends_with(str, 'foo')", + "result": false + }, + { + "expression": "ends_with(str, `0`)", + "error": "invalid-type" + }, + { + "expression": "floor(`1.2`)", + "result": 1 + }, + { + "expression": "floor('string')", + "error": "invalid-type" + }, + { + "expression": "floor(decimals[0])", + "result": 1 + }, + { + "expression": "floor(foo)", + "result": -1 + }, + { + "expression": "floor(str)", + "error": "invalid-type" + }, + { + "expression": "length('abc')", + "result": 3 + }, + { + "expression": "length('✓foo')", + "result": 4 + }, + { + "expression": "length('')", + "result": 0 + }, + { + "expression": "length(@)", + "result": 12 + }, + { + "expression": "length(strings[0])", + "result": 1 + }, + { + "expression": "length(str)", + "result": 3 + }, + { + "expression": "length(array)", + "result": 6 + }, + { + "expression": "length(objects)", + "result": 2 + }, + { + "expression": "length(`false`)", + "error": "invalid-type" + }, + { + "expression": "length(foo)", + "error": "invalid-type" + }, + { + "expression": "length(strings[0])", + "result": 1 + }, + { + "expression": "max(numbers)", + "result": 5 + }, + { + "expression": "max(decimals)", + "result": 1.2 + }, + { + "expression": "max(strings)", + "result": "c" + }, + { + "expression": "max(abc)", + "error": "invalid-type" + }, + { + "expression": "max(array)", + "error": "invalid-type" + }, + { + "expression": "max(decimals)", + "result": 1.2 + }, + { + "expression": "max(empty_list)", + "result": null + }, + { + "expression": "merge(`{}`)", + "result": {} + }, + { + "expression": "merge(`{}`, `{}`)", + "result": {} + }, + { + "expression": "merge(`{\"a\": 1}`, `{\"b\": 2}`)", + "result": {"a": 1, "b": 2} + }, + { + "expression": "merge(`{\"a\": 1}`, `{\"a\": 2}`)", + "result": {"a": 2} + }, + { + "expression": "merge(`{\"a\": 1, \"b\": 2}`, `{\"a\": 2, \"c\": 3}`, `{\"d\": 4}`)", + "result": {"a": 2, "b": 2, "c": 3, "d": 4} + }, + { + "expression": "min(numbers)", + "result": -1 + }, + { + "expression": "min(decimals)", + "result": -1.5 + }, + { + "expression": "min(abc)", + "error": "invalid-type" + }, + { + "expression": "min(array)", + "error": "invalid-type" + }, + { + "expression": "min(empty_list)", + "result": null + }, + { + "expression": "min(decimals)", + "result": -1.5 + }, + { + "expression": "min(strings)", + "result": "a" + }, + { + "expression": "type('abc')", + "result": "string" + }, + { + "expression": "type(`1.0`)", + "result": "number" + }, + { + "expression": "type(`2`)", + "result": "number" + }, + { + "expression": "type(`true`)", + "result": "boolean" + }, + { + "expression": "type(`false`)", + "result": "boolean" + }, + { + "expression": "type(`null`)", + "result": "null" + }, + { + "expression": "type(`[0]`)", + "result": "array" + }, + { + "expression": "type(`{\"a\": \"b\"}`)", + "result": "object" + }, + { + "expression": "type(@)", + "result": "object" + }, + { + "expression": "sort(keys(objects))", + "result": ["bar", "foo"] + }, + { + "expression": "keys(foo)", + "error": "invalid-type" + }, + { + "expression": "keys(strings)", + "error": "invalid-type" + }, + { + "expression": "keys(`false`)", + "error": "invalid-type" + }, + { + "expression": "sort(values(objects))", + "result": ["bar", "baz"] + }, + { + "expression": "keys(empty_hash)", + "result": [] + }, + { + "expression": "values(foo)", + "error": "invalid-type" + }, + { + "expression": "join(', ', strings)", + "result": "a, b, c" + }, + { + "expression": "join(', ', strings)", + "result": "a, b, c" + }, + { + "expression": "join(',', `[\"a\", \"b\"]`)", + "result": "a,b" + }, + { + "expression": "join(',', `[\"a\", 0]`)", + "error": "invalid-type" + }, + { + "expression": "join(', ', str)", + "error": "invalid-type" + }, + { + "expression": "join('|', strings)", + "result": "a|b|c" + }, + { + "expression": "join(`2`, strings)", + "error": "invalid-type" + }, + { + "expression": "join('|', decimals)", + "error": "invalid-type" + }, + { + "expression": "join('|', decimals[].to_string(@))", + "result": "1.01|1.2|-1.5" + }, + { + "expression": "join('|', empty_list)", + "result": "" + }, + { + "expression": "reverse(numbers)", + "result": [5, 4, 3, -1] + }, + { + "expression": "reverse(array)", + "result": ["100", "a", 5, 4, 3, -1] + }, + { + "expression": "reverse(`[]`)", + "result": [] + }, + { + "expression": "reverse('')", + "result": "" + }, + { + "expression": "reverse('hello world')", + "result": "dlrow olleh" + }, + { + "expression": "starts_with(str, 'S')", + "result": true + }, + { + "expression": "starts_with(str, 'St')", + "result": true + }, + { + "expression": "starts_with(str, 'Str')", + "result": true + }, + { + "expression": "starts_with(str, 'String')", + "result": false + }, + { + "expression": "starts_with(str, `0`)", + "error": "invalid-type" + }, + { + "expression": "sum(numbers)", + "result": 11 + }, + { + "expression": "sum(decimals)", + "result": 0.71 + }, + { + "expression": "sum(array)", + "error": "invalid-type" + }, + { + "expression": "sum(array[].to_number(@))", + "result": 111 + }, + { + "expression": "sum(`[]`)", + "result": 0 + }, + { + "expression": "to_array('foo')", + "result": ["foo"] + }, + { + "expression": "to_array(`0`)", + "result": [0] + }, + { + "expression": "to_array(objects)", + "result": [{"foo": "bar", "bar": "baz"}] + }, + { + "expression": "to_array(`[1, 2, 3]`)", + "result": [1, 2, 3] + }, + { + "expression": "to_array(false)", + "result": [false] + }, + { + "expression": "to_string('foo')", + "result": "foo" + }, + { + "expression": "to_string(`1.2`)", + "result": "1.2" + }, + { + "expression": "to_string(`[0, 1]`)", + "result": "[0,1]" + }, + { + "expression": "to_number('1.0')", + "result": 1.0 + }, + { + "expression": "to_number('1.1')", + "result": 1.1 + }, + { + "expression": "to_number('4')", + "result": 4 + }, + { + "expression": "to_number('notanumber')", + "result": null + }, + { + "expression": "to_number(`false`)", + "result": null + }, + { + "expression": "to_number(`null`)", + "result": null + }, + { + "expression": "to_number(`[0]`)", + "result": null + }, + { + "expression": "to_number(`{\"foo\": 0}`)", + "result": null + }, + { + "expression": "\"to_string\"(`1.0`)", + "error": "syntax" + }, + { + "expression": "sort(numbers)", + "result": [-1, 3, 4, 5] + }, + { + "expression": "sort(strings)", + "result": ["a", "b", "c"] + }, + { + "expression": "sort(decimals)", + "result": [-1.5, 1.01, 1.2] + }, + { + "expression": "sort(array)", + "error": "invalid-type" + }, + { + "expression": "sort(abc)", + "error": "invalid-type" + }, + { + "expression": "sort(empty_list)", + "result": [] + }, + { + "expression": "sort(@)", + "error": "invalid-type" + }, + { + "expression": "not_null(unknown_key, str)", + "result": "Str" + }, + { + "expression": "not_null(unknown_key, foo.bar, empty_list, str)", + "result": [] + }, + { + "expression": "not_null(unknown_key, null_key, empty_list, str)", + "result": [] + }, + { + "expression": "not_null(all, expressions, are_null)", + "result": null + }, + { + "expression": "not_null()", + "error": "invalid-arity" + }, + { + "comment": "function projection on single arg function", + "expression": "numbers[].to_string(@)", + "result": ["-1", "3", "4", "5"] + }, + { + "comment": "function projection on single arg function", + "expression": "array[].to_number(@)", + "result": [-1, 3, 4, 5, 100] + } + ] +}, { + "given": + { + "foo": [ + {"b": "b", "a": "a"}, + {"c": "c", "b": "b"}, + {"d": "d", "c": "c"}, + {"e": "e", "d": "d"}, + {"f": "f", "e": "e"} + ] + }, + "cases": [ + { + "comment": "function projection on variadic function", + "expression": "foo[].not_null(f, e, d, c, b, a)", + "result": ["b", "c", "d", "e", "f"] + } + ] +}, { + "given": + { + "people": [ + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"}, + {"age": 10, "age_str": "10", "bool": true, "name": 3} + ] + }, + "cases": [ + { + "comment": "sort by field expression", + "expression": "sort_by(people, &age)", + "result": [ + {"age": 10, "age_str": "10", "bool": true, "name": 3}, + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"} + ] + }, + { + "expression": "sort_by(people, &age_str)", + "result": [ + {"age": 10, "age_str": "10", "bool": true, "name": 3}, + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"} + ] + }, + { + "comment": "sort by function expression", + "expression": "sort_by(people, &to_number(age_str))", + "result": [ + {"age": 10, "age_str": "10", "bool": true, "name": 3}, + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"} + ] + }, + { + "comment": "function projection on sort_by function", + "expression": "sort_by(people, &age)[].name", + "result": [3, "a", "c", "b", "d"] + }, + { + "expression": "sort_by(people, &extra)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, &bool)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, &name)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, name)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, &age)[].extra", + "result": ["foo", "bar"] + }, + { + "expression": "sort_by(`[]`, &age)", + "result": [] + }, + { + "expression": "max_by(people, &age)", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + }, + { + "expression": "max_by(people, &age_str)", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + }, + { + "expression": "max_by(people, &bool)", + "error": "invalid-type" + }, + { + "expression": "max_by(people, &extra)", + "error": "invalid-type" + }, + { + "expression": "max_by(people, &to_number(age_str))", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + }, + { + "expression": "min_by(people, &age)", + "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + }, + { + "expression": "min_by(people, &age_str)", + "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + }, + { + "expression": "min_by(people, &bool)", + "error": "invalid-type" + }, + { + "expression": "min_by(people, &extra)", + "error": "invalid-type" + }, + { + "expression": "min_by(people, &to_number(age_str))", + "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + } + ] +}, { + "given": + { + "people": [ + {"age": 10, "order": "1"}, + {"age": 10, "order": "2"}, + {"age": 10, "order": "3"}, + {"age": 10, "order": "4"}, + {"age": 10, "order": "5"}, + {"age": 10, "order": "6"}, + {"age": 10, "order": "7"}, + {"age": 10, "order": "8"}, + {"age": 10, "order": "9"}, + {"age": 10, "order": "10"}, + {"age": 10, "order": "11"} + ] + }, + "cases": [ + { + "comment": "stable sort order", + "expression": "sort_by(people, &age)", + "result": [ + {"age": 10, "order": "1"}, + {"age": 10, "order": "2"}, + {"age": 10, "order": "3"}, + {"age": 10, "order": "4"}, + {"age": 10, "order": "5"}, + {"age": 10, "order": "6"}, + {"age": 10, "order": "7"}, + {"age": 10, "order": "8"}, + {"age": 10, "order": "9"}, + {"age": 10, "order": "10"}, + {"age": 10, "order": "11"} + ] + } + ] +}, { + "given": + { + "people": [ + {"a": 10, "b": 1, "c": "z"}, + {"a": 10, "b": 2, "c": null}, + {"a": 10, "b": 3}, + {"a": 10, "b": 4, "c": "z"}, + {"a": 10, "b": 5, "c": null}, + {"a": 10, "b": 6}, + {"a": 10, "b": 7, "c": "z"}, + {"a": 10, "b": 8, "c": null}, + {"a": 10, "b": 9} + ], + "empty": [] + }, + "cases": [ + { + "expression": "map(&a, people)", + "result": [10, 10, 10, 10, 10, 10, 10, 10, 10] + }, + { + "expression": "map(&c, people)", + "result": ["z", null, null, "z", null, null, "z", null, null] + }, + { + "expression": "map(&a, badkey)", + "error": "invalid-type" + }, + { + "expression": "map(&foo, empty)", + "result": [] + } + ] +}, { + "given": { + "array": [ + { + "foo": {"bar": "yes1"} + }, + { + "foo": {"bar": "yes2"} + }, + { + "foo1": {"bar": "no"} + } + ]}, + "cases": [ + { + "expression": "map(&foo.bar, array)", + "result": ["yes1", "yes2", null] + }, + { + "expression": "map(&foo1.bar, array)", + "result": [null, null, "no"] + }, + { + "expression": "map(&foo.bar.baz, array)", + "result": [null, null, null] + } + ] +}, { + "given": { + "array": [[1, 2, 3, [4]], [5, 6, 7, [8, 9]]] + }, + "cases": [ + { + "expression": "map(&[], array)", + "result": [[1, 2, 3, 4], [5, 6, 7, 8, 9]] + } + ] +} +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/identifiers.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/identifiers.json new file mode 100644 index 00000000..7998a41a --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/identifiers.json @@ -0,0 +1,1377 @@ +[ + { + "given": { + "__L": true + }, + "cases": [ + { + "expression": "__L", + "result": true + } + ] + }, + { + "given": { + "!\r": true + }, + "cases": [ + { + "expression": "\"!\\r\"", + "result": true + } + ] + }, + { + "given": { + "Y_1623": true + }, + "cases": [ + { + "expression": "Y_1623", + "result": true + } + ] + }, + { + "given": { + "x": true + }, + "cases": [ + { + "expression": "x", + "result": true + } + ] + }, + { + "given": { + "\tF\uCebb": true + }, + "cases": [ + { + "expression": "\"\\tF\\uCebb\"", + "result": true + } + ] + }, + { + "given": { + " \t": true + }, + "cases": [ + { + "expression": "\" \\t\"", + "result": true + } + ] + }, + { + "given": { + " ": true + }, + "cases": [ + { + "expression": "\" \"", + "result": true + } + ] + }, + { + "given": { + "v2": true + }, + "cases": [ + { + "expression": "v2", + "result": true + } + ] + }, + { + "given": { + "\t": true + }, + "cases": [ + { + "expression": "\"\\t\"", + "result": true + } + ] + }, + { + "given": { + "_X": true + }, + "cases": [ + { + "expression": "_X", + "result": true + } + ] + }, + { + "given": { + "\t4\ud9da\udd15": true + }, + "cases": [ + { + "expression": "\"\\t4\\ud9da\\udd15\"", + "result": true + } + ] + }, + { + "given": { + "v24_W": true + }, + "cases": [ + { + "expression": "v24_W", + "result": true + } + ] + }, + { + "given": { + "H": true + }, + "cases": [ + { + "expression": "\"H\"", + "result": true + } + ] + }, + { + "given": { + "\f": true + }, + "cases": [ + { + "expression": "\"\\f\"", + "result": true + } + ] + }, + { + "given": { + "E4": true + }, + "cases": [ + { + "expression": "\"E4\"", + "result": true + } + ] + }, + { + "given": { + "!": true + }, + "cases": [ + { + "expression": "\"!\"", + "result": true + } + ] + }, + { + "given": { + "tM": true + }, + "cases": [ + { + "expression": "tM", + "result": true + } + ] + }, + { + "given": { + " [": true + }, + "cases": [ + { + "expression": "\" [\"", + "result": true + } + ] + }, + { + "given": { + "R!": true + }, + "cases": [ + { + "expression": "\"R!\"", + "result": true + } + ] + }, + { + "given": { + "_6W": true + }, + "cases": [ + { + "expression": "_6W", + "result": true + } + ] + }, + { + "given": { + "\uaBA1\r": true + }, + "cases": [ + { + "expression": "\"\\uaBA1\\r\"", + "result": true + } + ] + }, + { + "given": { + "tL7": true + }, + "cases": [ + { + "expression": "tL7", + "result": true + } + ] + }, + { + "given": { + "<": true + }, + "cases": [ + { + "expression": "\">\"", + "result": true + } + ] + }, + { + "given": { + "hvu": true + }, + "cases": [ + { + "expression": "hvu", + "result": true + } + ] + }, + { + "given": { + "; !": true + }, + "cases": [ + { + "expression": "\"; !\"", + "result": true + } + ] + }, + { + "given": { + "hU": true + }, + "cases": [ + { + "expression": "hU", + "result": true + } + ] + }, + { + "given": { + "!I\n\/": true + }, + "cases": [ + { + "expression": "\"!I\\n\\/\"", + "result": true + } + ] + }, + { + "given": { + "\uEEbF": true + }, + "cases": [ + { + "expression": "\"\\uEEbF\"", + "result": true + } + ] + }, + { + "given": { + "U)\t": true + }, + "cases": [ + { + "expression": "\"U)\\t\"", + "result": true + } + ] + }, + { + "given": { + "fa0_9": true + }, + "cases": [ + { + "expression": "fa0_9", + "result": true + } + ] + }, + { + "given": { + "/": true + }, + "cases": [ + { + "expression": "\"/\"", + "result": true + } + ] + }, + { + "given": { + "Gy": true + }, + "cases": [ + { + "expression": "Gy", + "result": true + } + ] + }, + { + "given": { + "\b": true + }, + "cases": [ + { + "expression": "\"\\b\"", + "result": true + } + ] + }, + { + "given": { + "<": true + }, + "cases": [ + { + "expression": "\"<\"", + "result": true + } + ] + }, + { + "given": { + "\t": true + }, + "cases": [ + { + "expression": "\"\\t\"", + "result": true + } + ] + }, + { + "given": { + "\t&\\\r": true + }, + "cases": [ + { + "expression": "\"\\t&\\\\\\r\"", + "result": true + } + ] + }, + { + "given": { + "#": true + }, + "cases": [ + { + "expression": "\"#\"", + "result": true + } + ] + }, + { + "given": { + "B__": true + }, + "cases": [ + { + "expression": "B__", + "result": true + } + ] + }, + { + "given": { + "\nS \n": true + }, + "cases": [ + { + "expression": "\"\\nS \\n\"", + "result": true + } + ] + }, + { + "given": { + "Bp": true + }, + "cases": [ + { + "expression": "Bp", + "result": true + } + ] + }, + { + "given": { + ",\t;": true + }, + "cases": [ + { + "expression": "\",\\t;\"", + "result": true + } + ] + }, + { + "given": { + "B_q": true + }, + "cases": [ + { + "expression": "B_q", + "result": true + } + ] + }, + { + "given": { + "\/+\t\n\b!Z": true + }, + "cases": [ + { + "expression": "\"\\/+\\t\\n\\b!Z\"", + "result": true + } + ] + }, + { + "given": { + "\udadd\udfc7\\ueFAc": true + }, + "cases": [ + { + "expression": "\"\udadd\udfc7\\\\ueFAc\"", + "result": true + } + ] + }, + { + "given": { + ":\f": true + }, + "cases": [ + { + "expression": "\":\\f\"", + "result": true + } + ] + }, + { + "given": { + "\/": true + }, + "cases": [ + { + "expression": "\"\\/\"", + "result": true + } + ] + }, + { + "given": { + "_BW_6Hg_Gl": true + }, + "cases": [ + { + "expression": "_BW_6Hg_Gl", + "result": true + } + ] + }, + { + "given": { + "\udbcf\udc02": true + }, + "cases": [ + { + "expression": "\"\udbcf\udc02\"", + "result": true + } + ] + }, + { + "given": { + "zs1DC": true + }, + "cases": [ + { + "expression": "zs1DC", + "result": true + } + ] + }, + { + "given": { + "__434": true + }, + "cases": [ + { + "expression": "__434", + "result": true + } + ] + }, + { + "given": { + "\udb94\udd41": true + }, + "cases": [ + { + "expression": "\"\udb94\udd41\"", + "result": true + } + ] + }, + { + "given": { + "Z_5": true + }, + "cases": [ + { + "expression": "Z_5", + "result": true + } + ] + }, + { + "given": { + "z_M_": true + }, + "cases": [ + { + "expression": "z_M_", + "result": true + } + ] + }, + { + "given": { + "YU_2": true + }, + "cases": [ + { + "expression": "YU_2", + "result": true + } + ] + }, + { + "given": { + "_0": true + }, + "cases": [ + { + "expression": "_0", + "result": true + } + ] + }, + { + "given": { + "\b+": true + }, + "cases": [ + { + "expression": "\"\\b+\"", + "result": true + } + ] + }, + { + "given": { + "\"": true + }, + "cases": [ + { + "expression": "\"\\\"\"", + "result": true + } + ] + }, + { + "given": { + "D7": true + }, + "cases": [ + { + "expression": "D7", + "result": true + } + ] + }, + { + "given": { + "_62L": true + }, + "cases": [ + { + "expression": "_62L", + "result": true + } + ] + }, + { + "given": { + "\tK\t": true + }, + "cases": [ + { + "expression": "\"\\tK\\t\"", + "result": true + } + ] + }, + { + "given": { + "\n\\\f": true + }, + "cases": [ + { + "expression": "\"\\n\\\\\\f\"", + "result": true + } + ] + }, + { + "given": { + "I_": true + }, + "cases": [ + { + "expression": "I_", + "result": true + } + ] + }, + { + "given": { + "W_a0_": true + }, + "cases": [ + { + "expression": "W_a0_", + "result": true + } + ] + }, + { + "given": { + "BQ": true + }, + "cases": [ + { + "expression": "BQ", + "result": true + } + ] + }, + { + "given": { + "\tX$\uABBb": true + }, + "cases": [ + { + "expression": "\"\\tX$\\uABBb\"", + "result": true + } + ] + }, + { + "given": { + "Z9": true + }, + "cases": [ + { + "expression": "Z9", + "result": true + } + ] + }, + { + "given": { + "\b%\"\uda38\udd0f": true + }, + "cases": [ + { + "expression": "\"\\b%\\\"\uda38\udd0f\"", + "result": true + } + ] + }, + { + "given": { + "_F": true + }, + "cases": [ + { + "expression": "_F", + "result": true + } + ] + }, + { + "given": { + "!,": true + }, + "cases": [ + { + "expression": "\"!,\"", + "result": true + } + ] + }, + { + "given": { + "\"!": true + }, + "cases": [ + { + "expression": "\"\\\"!\"", + "result": true + } + ] + }, + { + "given": { + "Hh": true + }, + "cases": [ + { + "expression": "Hh", + "result": true + } + ] + }, + { + "given": { + "&": true + }, + "cases": [ + { + "expression": "\"&\"", + "result": true + } + ] + }, + { + "given": { + "9\r\\R": true + }, + "cases": [ + { + "expression": "\"9\\r\\\\R\"", + "result": true + } + ] + }, + { + "given": { + "M_k": true + }, + "cases": [ + { + "expression": "M_k", + "result": true + } + ] + }, + { + "given": { + "!\b\n\udb06\ude52\"\"": true + }, + "cases": [ + { + "expression": "\"!\\b\\n\udb06\ude52\\\"\\\"\"", + "result": true + } + ] + }, + { + "given": { + "6": true + }, + "cases": [ + { + "expression": "\"6\"", + "result": true + } + ] + }, + { + "given": { + "_7": true + }, + "cases": [ + { + "expression": "_7", + "result": true + } + ] + }, + { + "given": { + "0": true + }, + "cases": [ + { + "expression": "\"0\"", + "result": true + } + ] + }, + { + "given": { + "\\8\\": true + }, + "cases": [ + { + "expression": "\"\\\\8\\\\\"", + "result": true + } + ] + }, + { + "given": { + "b7eo": true + }, + "cases": [ + { + "expression": "b7eo", + "result": true + } + ] + }, + { + "given": { + "xIUo9": true + }, + "cases": [ + { + "expression": "xIUo9", + "result": true + } + ] + }, + { + "given": { + "5": true + }, + "cases": [ + { + "expression": "\"5\"", + "result": true + } + ] + }, + { + "given": { + "?": true + }, + "cases": [ + { + "expression": "\"?\"", + "result": true + } + ] + }, + { + "given": { + "sU": true + }, + "cases": [ + { + "expression": "sU", + "result": true + } + ] + }, + { + "given": { + "VH2&H\\\/": true + }, + "cases": [ + { + "expression": "\"VH2&H\\\\\\/\"", + "result": true + } + ] + }, + { + "given": { + "_C": true + }, + "cases": [ + { + "expression": "_C", + "result": true + } + ] + }, + { + "given": { + "_": true + }, + "cases": [ + { + "expression": "_", + "result": true + } + ] + }, + { + "given": { + "<\t": true + }, + "cases": [ + { + "expression": "\"<\\t\"", + "result": true + } + ] + }, + { + "given": { + "\uD834\uDD1E": true + }, + "cases": [ + { + "expression": "\"\\uD834\\uDD1E\"", + "result": true + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/indices.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/indices.json new file mode 100644 index 00000000..aa03b35d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/indices.json @@ -0,0 +1,346 @@ +[{ + "given": + {"foo": {"bar": ["zero", "one", "two"]}}, + "cases": [ + { + "expression": "foo.bar[0]", + "result": "zero" + }, + { + "expression": "foo.bar[1]", + "result": "one" + }, + { + "expression": "foo.bar[2]", + "result": "two" + }, + { + "expression": "foo.bar[3]", + "result": null + }, + { + "expression": "foo.bar[-1]", + "result": "two" + }, + { + "expression": "foo.bar[-2]", + "result": "one" + }, + { + "expression": "foo.bar[-3]", + "result": "zero" + }, + { + "expression": "foo.bar[-4]", + "result": null + } + ] +}, +{ + "given": + {"foo": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}]}, + "cases": [ + { + "expression": "foo.bar", + "result": null + }, + { + "expression": "foo[0].bar", + "result": "one" + }, + { + "expression": "foo[1].bar", + "result": "two" + }, + { + "expression": "foo[2].bar", + "result": "three" + }, + { + "expression": "foo[3].notbar", + "result": "four" + }, + { + "expression": "foo[3].bar", + "result": null + }, + { + "expression": "foo[0]", + "result": {"bar": "one"} + }, + { + "expression": "foo[1]", + "result": {"bar": "two"} + }, + { + "expression": "foo[2]", + "result": {"bar": "three"} + }, + { + "expression": "foo[3]", + "result": {"notbar": "four"} + }, + { + "expression": "foo[4]", + "result": null + } + ] +}, +{ + "given": [ + "one", "two", "three" + ], + "cases": [ + { + "expression": "[0]", + "result": "one" + }, + { + "expression": "[1]", + "result": "two" + }, + { + "expression": "[2]", + "result": "three" + }, + { + "expression": "[-1]", + "result": "three" + }, + { + "expression": "[-2]", + "result": "two" + }, + { + "expression": "[-3]", + "result": "one" + } + ] +}, +{ + "given": {"reservations": [ + {"instances": [{"foo": 1}, {"foo": 2}]} + ]}, + "cases": [ + { + "expression": "reservations[].instances[].foo", + "result": [1, 2] + }, + { + "expression": "reservations[].instances[].bar", + "result": [] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + } + ] +}, +{ + "given": {"reservations": [{ + "instances": [ + {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]}, + {"foo": [{"bar": 5}, {"bar": 6}, {"notbar": [7]}, {"bar": 8}]}, + {"foo": "bar"}, + {"notfoo": [{"bar": 20}, {"bar": 21}, {"notbar": [7]}, {"bar": 22}]}, + {"bar": [{"baz": [1]}, {"baz": [2]}, {"baz": [3]}, {"baz": [4]}]}, + {"baz": [{"baz": [1, 2]}, {"baz": []}, {"baz": []}, {"baz": [3, 4]}]}, + {"qux": [{"baz": []}, {"baz": [1, 2, 3]}, {"baz": [4]}, {"baz": []}]} + ], + "otherkey": {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]} + }, { + "instances": [ + {"a": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]}, + {"b": [{"bar": 5}, {"bar": 6}, {"notbar": [7]}, {"bar": 8}]}, + {"c": "bar"}, + {"notfoo": [{"bar": 23}, {"bar": 24}, {"notbar": [7]}, {"bar": 25}]}, + {"qux": [{"baz": []}, {"baz": [1, 2, 3]}, {"baz": [4]}, {"baz": []}]} + ], + "otherkey": {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]} + } + ]}, + "cases": [ + { + "expression": "reservations[].instances[].foo[].bar", + "result": [1, 2, 4, 5, 6, 8] + }, + { + "expression": "reservations[].instances[].foo[].baz", + "result": [] + }, + { + "expression": "reservations[].instances[].notfoo[].bar", + "result": [20, 21, 22, 23, 24, 25] + }, + { + "expression": "reservations[].instances[].notfoo[].notbar", + "result": [[7], [7]] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + }, + { + "expression": "reservations[].instances[].foo[].notbar", + "result": [3, [7]] + }, + { + "expression": "reservations[].instances[].bar[].baz", + "result": [[1], [2], [3], [4]] + }, + { + "expression": "reservations[].instances[].baz[].baz", + "result": [[1, 2], [], [], [3, 4]] + }, + { + "expression": "reservations[].instances[].qux[].baz", + "result": [[], [1, 2, 3], [4], [], [], [1, 2, 3], [4], []] + }, + { + "expression": "reservations[].instances[].qux[].baz[]", + "result": [1, 2, 3, 4, 1, 2, 3, 4] + } + ] +}, +{ + "given": { + "foo": [ + [["one", "two"], ["three", "four"]], + [["five", "six"], ["seven", "eight"]], + [["nine"], ["ten"]] + ] + }, + "cases": [ + { + "expression": "foo[]", + "result": [["one", "two"], ["three", "four"], ["five", "six"], + ["seven", "eight"], ["nine"], ["ten"]] + }, + { + "expression": "foo[][0]", + "result": ["one", "three", "five", "seven", "nine", "ten"] + }, + { + "expression": "foo[][1]", + "result": ["two", "four", "six", "eight"] + }, + { + "expression": "foo[][0][0]", + "result": [] + }, + { + "expression": "foo[][2][2]", + "result": [] + }, + { + "expression": "foo[][0][0][100]", + "result": [] + } + ] +}, +{ + "given": { + "foo": [{ + "bar": [ + { + "qux": 2, + "baz": 1 + }, + { + "qux": 4, + "baz": 3 + } + ] + }, + { + "bar": [ + { + "qux": 6, + "baz": 5 + }, + { + "qux": 8, + "baz": 7 + } + ] + } + ] + }, + "cases": [ + { + "expression": "foo", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[]", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[].bar", + "result": [[{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}], + [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]] + }, + { + "expression": "foo[].bar[]", + "result": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}, + {"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}] + }, + { + "expression": "foo[].bar[].baz", + "result": [1, 3, 5, 7] + } + ] +}, +{ + "given": { + "string": "string", + "hash": {"foo": "bar", "bar": "baz"}, + "number": 23, + "nullvalue": null + }, + "cases": [ + { + "expression": "string[]", + "result": null + }, + { + "expression": "hash[]", + "result": null + }, + { + "expression": "number[]", + "result": null + }, + { + "expression": "nullvalue[]", + "result": null + }, + { + "expression": "string[].foo", + "result": null + }, + { + "expression": "hash[].foo", + "result": null + }, + { + "expression": "number[].foo", + "result": null + }, + { + "expression": "nullvalue[].foo", + "result": null + }, + { + "expression": "nullvalue[].foo[].bar", + "result": null + } + ] +} +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/kinesis_data_stream.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/kinesis_data_stream.json new file mode 100644 index 00000000..a9e7f236 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/kinesis_data_stream.json @@ -0,0 +1,74 @@ +[ + { + "given": { + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "IlRlc3QgZnJvbSBLaW5lc2lzIg==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600 + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "us-east-1" + } + ] + }, + "cases": [ + { + "expression": "Records[0].kinesis.powertools_json(powertools_base64(data))", + "result": "Test from Kinesis" + } + ] + }, + { + "given": { + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "IlRlc3QgZnJvbSBLaW5lc2lzIg==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600 + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "us-east-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "IlNlY29uZCBSZWNvcmQgU3RyZWFtIg==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600 + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "us-east-1" + } + ] + }, + "cases": [ + { + "expression": "Records[*].kinesis.powertools_json(powertools_base64(data))", + "result": ["Test from Kinesis", "Second Record Stream"] + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/literal.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/literal.json new file mode 100644 index 00000000..b5ddbeda --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/literal.json @@ -0,0 +1,200 @@ +[ + { + "given": { + "foo": [{"name": "a"}, {"name": "b"}], + "bar": {"baz": "qux"} + }, + "cases": [ + { + "expression": "`\"foo\"`", + "result": "foo" + }, + { + "comment": "Interpret escaped unicode.", + "expression": "`\"\\u03a6\"`", + "result": "Φ" + }, + { + "expression": "`\"✓\"`", + "result": "✓" + }, + { + "expression": "`[1, 2, 3]`", + "result": [1, 2, 3] + }, + { + "expression": "`{\"a\": \"b\"}`", + "result": {"a": "b"} + }, + { + "expression": "`true`", + "result": true + }, + { + "expression": "`false`", + "result": false + }, + { + "expression": "`null`", + "result": null + }, + { + "expression": "`0`", + "result": 0 + }, + { + "expression": "`1`", + "result": 1 + }, + { + "expression": "`2`", + "result": 2 + }, + { + "expression": "`3`", + "result": 3 + }, + { + "expression": "`4`", + "result": 4 + }, + { + "expression": "`5`", + "result": 5 + }, + { + "expression": "`6`", + "result": 6 + }, + { + "expression": "`7`", + "result": 7 + }, + { + "expression": "`8`", + "result": 8 + }, + { + "expression": "`9`", + "result": 9 + }, + { + "comment": "Escaping a backtick in quotes", + "expression": "`\"foo\\`bar\"`", + "result": "foo`bar" + }, + { + "comment": "Double quote in literal", + "expression": "`\"foo\\\"bar\"`", + "result": "foo\"bar" + }, + { + "expression": "`\"1\\`\"`", + "result": "1`" + }, + { + "comment": "Multiple literal expressions with escapes", + "expression": "`\"\\\\\"`.{a:`\"b\"`}", + "result": {"a": "b"} + }, + { + "comment": "literal . identifier", + "expression": "`{\"a\": \"b\"}`.a", + "result": "b" + }, + { + "comment": "literal . identifier . identifier", + "expression": "`{\"a\": {\"b\": \"c\"}}`.a.b", + "result": "c" + }, + { + "comment": "literal . identifier bracket-expr", + "expression": "`[0, 1, 2]`[1]", + "result": 1 + } + ] + }, + { + "comment": "Literals", + "given": {"type": "object"}, + "cases": [ + { + "comment": "Literal with leading whitespace", + "expression": "` {\"foo\": true}`", + "result": {"foo": true} + }, + { + "comment": "Literal with trailing whitespace", + "expression": "`{\"foo\": true} `", + "result": {"foo": true} + }, + { + "comment": "Literal on RHS of subexpr not allowed", + "expression": "foo.`\"bar\"`", + "error": "syntax" + } + ] + }, + { + "comment": "Raw String Literals", + "given": {}, + "cases": [ + { + "expression": "'foo'", + "result": "foo" + }, + { + "expression": "' foo '", + "result": " foo " + }, + { + "expression": "'0'", + "result": "0" + }, + { + "expression": "'newline\n'", + "result": "newline\n" + }, + { + "expression": "'\n'", + "result": "\n" + }, + { + "expression": "'✓'", + "result": "✓" + }, + { + "expression": "'𝄞'", + "result": "𝄞" + }, + { + "expression": "' [foo] '", + "result": " [foo] " + }, + { + "expression": "'[foo]'", + "result": "[foo]" + }, + { + "comment": "Do not interpret escaped unicode.", + "expression": "'\\u03a6'", + "result": "\\u03a6" + }, + { + "comment": "Can escape the single quote", + "expression": "'foo\\'bar'", + "result": "foo'bar" + }, + { + "comment": "Backslash not followed by single quote is treated as any other character", + "expression": "'\\z'", + "result": "\\z" + }, + { + "comment": "Backslash not followed by single quote is treated as any other character", + "expression": "'\\\\'", + "result": "\\\\" + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/multiselect.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/multiselect.json new file mode 100644 index 00000000..4f464822 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/multiselect.json @@ -0,0 +1,398 @@ +[{ + "given": { + "foo": { + "bar": "bar", + "baz": "baz", + "qux": "qux", + "nested": { + "one": { + "a": "first", + "b": "second", + "c": "third" + }, + "two": { + "a": "first", + "b": "second", + "c": "third" + }, + "three": { + "a": "first", + "b": "second", + "c": {"inner": "third"} + } + } + }, + "bar": 1, + "baz": 2, + "qux\"": 3 + }, + "cases": [ + { + "expression": "foo.{bar: bar}", + "result": {"bar": "bar"} + }, + { + "expression": "foo.{\"bar\": bar}", + "result": {"bar": "bar"} + }, + { + "expression": "foo.{\"foo.bar\": bar}", + "result": {"foo.bar": "bar"} + }, + { + "expression": "foo.{bar: bar, baz: baz}", + "result": {"bar": "bar", "baz": "baz"} + }, + { + "expression": "foo.{\"bar\": bar, \"baz\": baz}", + "result": {"bar": "bar", "baz": "baz"} + }, + { + "expression": "{\"baz\": baz, \"qux\\\"\": \"qux\\\"\"}", + "result": {"baz": 2, "qux\"": 3} + }, + { + "expression": "foo.{bar:bar,baz:baz}", + "result": {"bar": "bar", "baz": "baz"} + }, + { + "expression": "foo.{bar: bar,qux: qux}", + "result": {"bar": "bar", "qux": "qux"} + }, + { + "expression": "foo.{bar: bar, noexist: noexist}", + "result": {"bar": "bar", "noexist": null} + }, + { + "expression": "foo.{noexist: noexist, alsonoexist: alsonoexist}", + "result": {"noexist": null, "alsonoexist": null} + }, + { + "expression": "foo.badkey.{nokey: nokey, alsonokey: alsonokey}", + "result": null + }, + { + "expression": "foo.nested.*.{a: a,b: b}", + "result": [{"a": "first", "b": "second"}, + {"a": "first", "b": "second"}, + {"a": "first", "b": "second"}] + }, + { + "expression": "foo.nested.three.{a: a, cinner: c.inner}", + "result": {"a": "first", "cinner": "third"} + }, + { + "expression": "foo.nested.three.{a: a, c: c.inner.bad.key}", + "result": {"a": "first", "c": null} + }, + { + "expression": "foo.{a: nested.one.a, b: nested.two.b}", + "result": {"a": "first", "b": "second"} + }, + { + "expression": "{bar: bar, baz: baz}", + "result": {"bar": 1, "baz": 2} + }, + { + "expression": "{bar: bar}", + "result": {"bar": 1} + }, + { + "expression": "{otherkey: bar}", + "result": {"otherkey": 1} + }, + { + "expression": "{no: no, exist: exist}", + "result": {"no": null, "exist": null} + }, + { + "expression": "foo.[bar]", + "result": ["bar"] + }, + { + "expression": "foo.[bar,baz]", + "result": ["bar", "baz"] + }, + { + "expression": "foo.[bar,qux]", + "result": ["bar", "qux"] + }, + { + "expression": "foo.[bar,noexist]", + "result": ["bar", null] + }, + { + "expression": "foo.[noexist,alsonoexist]", + "result": [null, null] + } + ] +}, { + "given": { + "foo": {"bar": 1, "baz": [2, 3, 4]} + }, + "cases": [ + { + "expression": "foo.{bar:bar,baz:baz}", + "result": {"bar": 1, "baz": [2, 3, 4]} + }, + { + "expression": "foo.[bar,baz[0]]", + "result": [1, 2] + }, + { + "expression": "foo.[bar,baz[1]]", + "result": [1, 3] + }, + { + "expression": "foo.[bar,baz[2]]", + "result": [1, 4] + }, + { + "expression": "foo.[bar,baz[3]]", + "result": [1, null] + }, + { + "expression": "foo.[bar[0],baz[3]]", + "result": [null, null] + } + ] +}, { + "given": { + "foo": {"bar": 1, "baz": 2} + }, + "cases": [ + { + "expression": "foo.{bar: bar, baz: baz}", + "result": {"bar": 1, "baz": 2} + }, + { + "expression": "foo.[bar,baz]", + "result": [1, 2] + } + ] +}, { + "given": { + "foo": { + "bar": {"baz": [{"common": "first", "one": 1}, + {"common": "second", "two": 2}]}, + "ignoreme": 1, + "includeme": true + } + }, + "cases": [ + { + "expression": "foo.{bar: bar.baz[1],includeme: includeme}", + "result": {"bar": {"common": "second", "two": 2}, "includeme": true} + }, + { + "expression": "foo.{\"bar.baz.two\": bar.baz[1].two, includeme: includeme}", + "result": {"bar.baz.two": 2, "includeme": true} + }, + { + "expression": "foo.[includeme, bar.baz[*].common]", + "result": [true, ["first", "second"]] + }, + { + "expression": "foo.[includeme, bar.baz[*].none]", + "result": [true, []] + }, + { + "expression": "foo.[includeme, bar.baz[].common]", + "result": [true, ["first", "second"]] + } + ] +}, { + "given": { + "reservations": [{ + "instances": [ + {"id": "id1", + "name": "first"}, + {"id": "id2", + "name": "second"} + ]}, { + "instances": [ + {"id": "id3", + "name": "third"}, + {"id": "id4", + "name": "fourth"} + ]} + ]}, + "cases": [ + { + "expression": "reservations[*].instances[*].{id: id, name: name}", + "result": [[{"id": "id1", "name": "first"}, {"id": "id2", "name": "second"}], + [{"id": "id3", "name": "third"}, {"id": "id4", "name": "fourth"}]] + }, + { + "expression": "reservations[].instances[].{id: id, name: name}", + "result": [{"id": "id1", "name": "first"}, + {"id": "id2", "name": "second"}, + {"id": "id3", "name": "third"}, + {"id": "id4", "name": "fourth"}] + }, + { + "expression": "reservations[].instances[].[id, name]", + "result": [["id1", "first"], + ["id2", "second"], + ["id3", "third"], + ["id4", "fourth"]] + } + ] +}, +{ + "given": { + "foo": [{ + "bar": [ + { + "qux": 2, + "baz": 1 + }, + { + "qux": 4, + "baz": 3 + } + ] + }, + { + "bar": [ + { + "qux": 6, + "baz": 5 + }, + { + "qux": 8, + "baz": 7 + } + ] + } + ] + }, + "cases": [ + { + "expression": "foo", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[]", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[].bar", + "result": [[{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}], + [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]] + }, + { + "expression": "foo[].bar[]", + "result": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}, + {"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}] + }, + { + "expression": "foo[].bar[].[baz, qux]", + "result": [[1, 2], [3, 4], [5, 6], [7, 8]] + }, + { + "expression": "foo[].bar[].[baz]", + "result": [[1], [3], [5], [7]] + }, + { + "expression": "foo[].bar[].[baz, qux][]", + "result": [1, 2, 3, 4, 5, 6, 7, 8] + } + ] +}, +{ + "given": { + "foo": { + "baz": [ + { + "bar": "abc" + }, { + "bar": "def" + } + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].bar, qux[0]]", + "result": [["abc", "def"], "zero"] + } + ] +}, +{ + "given": { + "foo": { + "baz": [ + { + "bar": "a", + "bam": "b", + "boo": "c" + }, { + "bar": "d", + "bam": "e", + "boo": "f" + } + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].[bar, boo], qux[0]]", + "result": [[["a", "c" ], ["d", "f" ]], "zero"] + } + ] +}, +{ + "given": { + "foo": { + "baz": [ + { + "bar": "a", + "bam": "b", + "boo": "c" + }, { + "bar": "d", + "bam": "e", + "boo": "f" + } + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].not_there || baz[*].bar, qux[0]]", + "result": [["a", "d"], "zero"] + } + ] +}, +{ + "given": {"type": "object"}, + "cases": [ + { + "comment": "Nested multiselect", + "expression": "[[*],*]", + "result": [null, ["object"]] + } + ] +}, +{ + "given": [], + "cases": [ + { + "comment": "Nested multiselect", + "expression": "[[*]]", + "result": [[]] + }, + { + "comment": "Select on null", + "expression": "missing.{foo: bar}", + "result": null + } + ] +} +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/pipe.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/pipe.json new file mode 100644 index 00000000..b10c0a49 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/pipe.json @@ -0,0 +1,131 @@ +[{ + "given": { + "foo": { + "bar": { + "baz": "subkey" + }, + "other": { + "baz": "subkey" + }, + "other2": { + "baz": "subkey" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["a", "b", "c"] + } + } + }, + "cases": [ + { + "expression": "foo.*.baz | [0]", + "result": "subkey" + }, + { + "expression": "foo.*.baz | [1]", + "result": "subkey" + }, + { + "expression": "foo.*.baz | [2]", + "result": "subkey" + }, + { + "expression": "foo.bar.* | [0]", + "result": "subkey" + }, + { + "expression": "foo.*.notbaz | [*]", + "result": [["a", "b", "c"], ["a", "b", "c"]] + }, + { + "expression": "{\"a\": foo.bar, \"b\": foo.other} | *.baz", + "result": ["subkey", "subkey"] + } + ] +}, { + "given": { + "foo": { + "bar": { + "baz": "one" + }, + "other": { + "baz": "two" + }, + "other2": { + "baz": "three" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["d", "e", "f"] + } + } + }, + "cases": [ + { + "expression": "foo | bar", + "result": {"baz": "one"} + }, + { + "expression": "foo | bar | baz", + "result": "one" + }, + { + "expression": "foo|bar| baz", + "result": "one" + }, + { + "expression": "not_there | [0]", + "result": null + }, + { + "expression": "not_there | [0]", + "result": null + }, + { + "expression": "[foo.bar, foo.other] | [0]", + "result": {"baz": "one"} + }, + { + "expression": "{\"a\": foo.bar, \"b\": foo.other} | a", + "result": {"baz": "one"} + }, + { + "expression": "{\"a\": foo.bar, \"b\": foo.other} | b", + "result": {"baz": "two"} + }, + { + "expression": "foo.bam || foo.bar | baz", + "result": "one" + }, + { + "expression": "foo | not_there || bar", + "result": {"baz": "one"} + } + ] +}, { + "given": { + "foo": [{ + "bar": [{ + "baz": "one" + }, { + "baz": "two" + }] + }, { + "bar": [{ + "baz": "three" + }, { + "baz": "four" + }] + }] + }, + "cases": [ + { + "expression": "foo[*].bar[*] | [0][0]", + "result": {"baz": "one"} + } + ] +}] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/slice.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/slice.json new file mode 100644 index 00000000..35947727 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/slice.json @@ -0,0 +1,187 @@ +[{ + "given": { + "foo": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "bar": { + "baz": 1 + } + }, + "cases": [ + { + "expression": "bar[0:10]", + "result": null + }, + { + "expression": "foo[0:10:1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0:10]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0:10:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0::1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0::]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[:10:1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[::1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[:10:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[::]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[1:9]", + "result": [1, 2, 3, 4, 5, 6, 7, 8] + }, + { + "expression": "foo[0:10:2]", + "result": [0, 2, 4, 6, 8] + }, + { + "expression": "foo[5:]", + "result": [5, 6, 7, 8, 9] + }, + { + "expression": "foo[5::2]", + "result": [5, 7, 9] + }, + { + "expression": "foo[::2]", + "result": [0, 2, 4, 6, 8] + }, + { + "expression": "foo[::-1]", + "result": [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + "expression": "foo[1::2]", + "result": [1, 3, 5, 7, 9] + }, + { + "expression": "foo[10:0:-1]", + "result": [9, 8, 7, 6, 5, 4, 3, 2, 1] + }, + { + "expression": "foo[10:5:-1]", + "result": [9, 8, 7, 6] + }, + { + "expression": "foo[8:2:-2]", + "result": [8, 6, 4] + }, + { + "expression": "foo[0:20]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[10:-20:-1]", + "result": [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + "expression": "foo[10:-20]", + "result": [] + }, + { + "expression": "foo[-4:-1]", + "result": [6, 7, 8] + }, + { + "expression": "foo[:-5:-1]", + "result": [9, 8, 7, 6] + }, + { + "expression": "foo[8:2:0]", + "error": "invalid-value" + }, + { + "expression": "foo[8:2:0:1]", + "error": "syntax" + }, + { + "expression": "foo[8:2&]", + "error": "syntax" + }, + { + "expression": "foo[2:a:3]", + "error": "syntax" + } + ] +}, { + "given": { + "foo": [{"a": 1}, {"a": 2}, {"a": 3}], + "bar": [{"a": {"b": 1}}, {"a": {"b": 2}}, + {"a": {"b": 3}}], + "baz": 50 + }, + "cases": [ + { + "expression": "foo[:2].a", + "result": [1, 2] + }, + { + "expression": "foo[:2].b", + "result": [] + }, + { + "expression": "foo[:2].a.b", + "result": [] + }, + { + "expression": "bar[::-1].a.b", + "result": [3, 2, 1] + }, + { + "expression": "bar[:2].a.b", + "result": [1, 2] + }, + { + "expression": "baz[:2].a", + "result": null + } + ] +}, { + "given": [{"a": 1}, {"a": 2}, {"a": 3}], + "cases": [ + { + "expression": "[:]", + "result": [{"a": 1}, {"a": 2}, {"a": 3}] + }, + { + "expression": "[:2].a", + "result": [1, 2] + }, + { + "expression": "[::-1].a", + "result": [3, 2, 1] + }, + { + "expression": "[:2].b", + "result": [] + } + ] +}] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/sns.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/sns.json new file mode 100644 index 00000000..45a9c346 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/sns.json @@ -0,0 +1,41 @@ +[ + { + "given": { + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:us-east-1:{{{accountId}}}:ExampleTopic", + "Sns": { + "Type": "Notification", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:ExampleTopic", + "Subject": "example subject", + "Message": "example message", + "Timestamp": "1970-01-01T00:00:00.000Z", + "SignatureVersion": "1", + "Signature": "EXAMPLE", + "SigningCertUrl": "EXAMPLE", + "UnsubscribeUrl": "EXAMPLE", + "MessageAttributes": { + "Test": { + "Type": "String", + "Value": "TestString" + }, + "TestBinary": { + "Type": "Binary", + "Value": "TestBinary" + } + } + } + } + ] + }, + "cases": [ + { + "expression": "Records[0].Sns.Message | powertools_json(@)", + "result": "example message" + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/sqs.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/sqs.json new file mode 100644 index 00000000..d0f45c69 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/sqs.json @@ -0,0 +1,78 @@ +[ + { + "given": { + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "Hello from SQS!", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": {}, + "md5OfBody": "{{{md5_of_body}}}", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + } + ] + }, + "cases": [ + { + "expression": "Records[0].powertools_json(body)", + "result": "Hello from SQS!" + } + ] + }, + { + "given": { + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "Hello from SQS!", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": {}, + "md5OfBody": "{{{md5_of_body}}}", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + }, + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "2nd Message", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": {}, + "md5OfBody": "{{{md5_of_body}}}", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + } + ] + }, + "cases": [ + { + "expression": "Records[0].powertools_json(body)", + "result": "Hello from SQS!" + }, + { + "expression": "Records[*].powertools_json(body)", + "result": ["Hello from SQS!","2nd Message"] + } + ] + } +] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/syntax.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/syntax.json new file mode 100644 index 00000000..9318901f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/syntax.json @@ -0,0 +1,678 @@ +[{ + "comment": "Dot syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo.bar", + "result": null + }, + { + "expression": "foo.1", + "error": "syntax" + }, + { + "expression": "foo.-11", + "error": "syntax" + }, + { + "expression": "foo.", + "error": "syntax" + }, + { + "expression": ".foo", + "error": "syntax" + }, + { + "expression": "foo..bar", + "error": "syntax" + }, + { + "expression": "foo.bar.", + "error": "syntax" + }, + { + "expression": "foo[.]", + "error": "syntax" + } + ] +}, + { + "comment": "Simple token errors", + "given": {"type": "object"}, + "cases": [ + { + "expression": ".", + "error": "syntax" + }, + { + "expression": ":", + "error": "syntax" + }, + { + "expression": ",", + "error": "syntax" + }, + { + "expression": "]", + "error": "syntax" + }, + { + "expression": "[", + "error": "syntax" + }, + { + "expression": "}", + "error": "syntax" + }, + { + "expression": "{", + "error": "syntax" + }, + { + "expression": ")", + "error": "syntax" + }, + { + "expression": "(", + "error": "syntax" + }, + { + "expression": "((&", + "error": "syntax" + }, + { + "expression": "a[", + "error": "syntax" + }, + { + "expression": "a]", + "error": "syntax" + }, + { + "expression": "a][", + "error": "syntax" + }, + { + "expression": "!", + "error": "syntax" + }, + { + "expression": "@=", + "error": "syntax" + }, + { + "expression": "@``", + "error": "syntax" + } + ] + }, + { + "comment": "Boolean syntax errors", + "given": {"type": "object"}, + "cases": [ + { + "expression": "![!(!", + "error": "syntax" + } + ] + }, + { + "comment": "Paren syntax errors", + "given": {}, + "cases": [ + { + "comment": "missing closing paren", + "expression": "(@", + "error": "syntax" + } + ] + }, + { + "comment": "Function syntax errors", + "given": {}, + "cases": [ + { + "comment": "invalid start of function", + "expression": "@(foo)", + "error": "syntax" + }, + { + "comment": "function names cannot be quoted", + "expression": "\"foo\"(bar)", + "error": "syntax" + } + ] + }, + { + "comment": "Wildcard syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "*", + "result": ["object"] + }, + { + "expression": "*.*", + "result": [] + }, + { + "expression": "*.foo", + "result": [] + }, + { + "expression": "*[0]", + "result": [] + }, + { + "expression": ".*", + "error": "syntax" + }, + { + "expression": "*foo", + "error": "syntax" + }, + { + "expression": "*0", + "error": "syntax" + }, + { + "expression": "foo[*]bar", + "error": "syntax" + }, + { + "expression": "foo[*]*", + "error": "syntax" + } + ] + }, + { + "comment": "Flatten syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "[]", + "result": null + } + ] + }, + { + "comment": "Simple bracket syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "[0]", + "result": null + }, + { + "expression": "[*]", + "result": null + }, + { + "expression": "*.[0]", + "error": "syntax" + }, + { + "expression": "*.[\"0\"]", + "result": [[null]] + }, + { + "expression": "[*].bar", + "result": null + }, + { + "expression": "[*][0]", + "result": null + }, + { + "expression": "foo[#]", + "error": "syntax" + }, + { + "comment": "missing rbracket for led wildcard index", + "expression": "led[*", + "error": "syntax" + } + ] + }, + { + "comment": "slice syntax", + "given": {}, + "cases": [ + { + "comment": "slice expected colon or rbracket", + "expression": "[:@]", + "error": "syntax" + }, + { + "comment": "slice has too many colons", + "expression": "[:::]", + "error": "syntax" + }, + { + "comment": "slice expected number", + "expression": "[:@:]", + "error": "syntax" + }, + { + "comment": "slice expected number of colon", + "expression": "[:1@]", + "error": "syntax" + } + ] + }, + { + "comment": "Multi-select list syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo[0]", + "result": null + }, + { + "comment": "Valid multi-select of a list", + "expression": "foo[0, 1]", + "error": "syntax" + }, + { + "expression": "foo.[0]", + "error": "syntax" + }, + { + "expression": "foo.[*]", + "result": null + }, + { + "comment": "Multi-select of a list with trailing comma", + "expression": "foo[0, ]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with trailing comma and no close", + "expression": "foo[0,", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with trailing comma and no close", + "expression": "foo.[a", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with extra comma", + "expression": "foo[0,, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index", + "expression": "foo[abc]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using identifier indices", + "expression": "foo[abc, def]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index", + "expression": "foo[abc, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index with trailing comma", + "expression": "foo[abc, ]", + "error": "syntax" + }, + { + "comment": "Valid multi-select of a hash using an identifier index", + "expression": "foo.[abc]", + "result": null + }, + { + "comment": "Valid multi-select of a hash", + "expression": "foo.[abc, def]", + "result": null + }, + { + "comment": "Multi-select of a hash using a numeric index", + "expression": "foo.[abc, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash with a trailing comma", + "expression": "foo.[abc, ]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash with extra commas", + "expression": "foo.[abc,, def]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash using number indices", + "expression": "foo.[0, 1]", + "error": "syntax" + } + ] + }, + { + "comment": "Multi-select hash syntax", + "given": {"type": "object"}, + "cases": [ + { + "comment": "No key or value", + "expression": "a{}", + "error": "syntax" + }, + { + "comment": "No closing token", + "expression": "a{", + "error": "syntax" + }, + { + "comment": "Not a key value pair", + "expression": "a{foo}", + "error": "syntax" + }, + { + "comment": "Missing value and closing character", + "expression": "a{foo:", + "error": "syntax" + }, + { + "comment": "Missing closing character", + "expression": "a{foo: 0", + "error": "syntax" + }, + { + "comment": "Missing value", + "expression": "a{foo:}", + "error": "syntax" + }, + { + "comment": "Trailing comma and no closing character", + "expression": "a{foo: 0, ", + "error": "syntax" + }, + { + "comment": "Missing value with trailing comma", + "expression": "a{foo: ,}", + "error": "syntax" + }, + { + "comment": "Accessing Array using an identifier", + "expression": "a{foo: bar}", + "error": "syntax" + }, + { + "expression": "a{foo: 0}", + "error": "syntax" + }, + { + "comment": "Missing key-value pair", + "expression": "a.{}", + "error": "syntax" + }, + { + "comment": "Not a key-value pair", + "expression": "a.{foo}", + "error": "syntax" + }, + { + "comment": "Valid multi-select hash extraction", + "expression": "a.{foo: bar}", + "result": null + }, + { + "comment": "Valid multi-select hash extraction", + "expression": "a.{foo: bar, baz: bam}", + "result": null + }, + { + "comment": "Trailing comma", + "expression": "a.{foo: bar, }", + "error": "syntax" + }, + { + "comment": "Missing key in second key-value pair", + "expression": "a.{foo: bar, baz}", + "error": "syntax" + }, + { + "comment": "Missing value in second key-value pair", + "expression": "a.{foo: bar, baz:}", + "error": "syntax" + }, + { + "comment": "Trailing comma", + "expression": "a.{foo: bar, baz: bam, }", + "error": "syntax" + }, + { + "comment": "Nested multi select", + "expression": "{\"\\\\\":{\" \":*}}", + "result": {"\\": {" ": ["object"]}} + }, + { + "comment": "Missing closing } after a valid nud", + "expression": "{a: @", + "error": "syntax" + } + ] + }, + { + "comment": "Or expressions", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo || bar", + "result": null + }, + { + "expression": "foo ||", + "error": "syntax" + }, + { + "expression": "foo.|| bar", + "error": "syntax" + }, + { + "expression": " || foo", + "error": "syntax" + }, + { + "expression": "foo || || foo", + "error": "syntax" + }, + { + "expression": "foo.[a || b]", + "result": null + }, + { + "expression": "foo.[a ||]", + "error": "syntax" + }, + { + "expression": "\"foo", + "error": "syntax" + } + ] + }, + { + "comment": "Filter expressions", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo[?bar==`\"baz\"`]", + "result": null + }, + { + "expression": "foo[? bar == `\"baz\"` ]", + "result": null + }, + { + "expression": "foo[ ?bar==`\"baz\"`]", + "error": "syntax" + }, + { + "expression": "foo[?bar==]", + "error": "syntax" + }, + { + "expression": "foo[?==]", + "error": "syntax" + }, + { + "expression": "foo[?==bar]", + "error": "syntax" + }, + { + "expression": "foo[?bar==baz?]", + "error": "syntax" + }, + { + "expression": "foo[?a.b.c==d.e.f]", + "result": null + }, + { + "expression": "foo[?bar==`[0, 1, 2]`]", + "result": null + }, + { + "expression": "foo[?bar==`[\"a\", \"b\", \"c\"]`]", + "result": null + }, + { + "comment": "Literal char not escaped", + "expression": "foo[?bar==`[\"foo`bar\"]`]", + "error": "syntax" + }, + { + "comment": "Literal char escaped", + "expression": "foo[?bar==`[\"foo\\`bar\"]`]", + "result": null + }, + { + "comment": "Unknown comparator", + "expression": "foo[?bar<>baz]", + "error": "syntax" + }, + { + "comment": "Unknown comparator", + "expression": "foo[?bar^baz]", + "error": "syntax" + }, + { + "expression": "foo[bar==baz]", + "error": "syntax" + }, + { + "comment": "Quoted identifier in filter expression no spaces", + "expression": "[?\"\\\\\">`\"foo\"`]", + "result": null + }, + { + "comment": "Quoted identifier in filter expression with spaces", + "expression": "[?\"\\\\\" > `\"foo\"`]", + "result": null + } + ] + }, + { + "comment": "Filter expression errors", + "given": {"type": "object"}, + "cases": [ + { + "expression": "bar.`\"anything\"`", + "error": "syntax" + }, + { + "expression": "bar.baz.noexists.`\"literal\"`", + "error": "syntax" + }, + { + "comment": "Literal wildcard projection", + "expression": "foo[*].`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[*].name.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.`\"literal\"`.`\"subliteral\"`", + "error": "syntax" + }, + { + "comment": "Projecting a literal onto an empty list", + "expression": "foo[*].name.noexist.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.noexist.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "twolen[*].`\"foo\"`", + "error": "syntax" + }, + { + "comment": "Two level projection of a literal", + "expression": "twolen[*].threelen[*].`\"bar\"`", + "error": "syntax" + }, + { + "comment": "Two level flattened projection of a literal", + "expression": "twolen[].threelen[].`\"bar\"`", + "error": "syntax" + }, + { + "comment": "expects closing ]", + "expression": "foo[? @ | @", + "error": "syntax" + } + ] + }, + { + "comment": "Identifiers", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo", + "result": null + }, + { + "expression": "\"foo\"", + "result": null + }, + { + "expression": "\"\\\\\"", + "result": null + }, + { + "expression": "\"\\u\"", + "error": "syntax" + } + ] + }, + { + "comment": "Combined syntax", + "given": [], + "cases": [ + { + "expression": "*||*|*|*", + "result": null + }, + { + "expression": "*[]||[*]", + "result": [] + }, + { + "expression": "[*.*]", + "result": [null] + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/test.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/test.json new file mode 100644 index 00000000..aa8953c8 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/test.json @@ -0,0 +1,19 @@ +[ + {"given": + { + "people": [ + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"}, + {"age": 10, "age_str": "10", "bool": true, "name": 3} + ] + }, + "cases": [ + { + "expression": "max_by(people, &age)", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/unicode.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/unicode.json new file mode 100644 index 00000000..6b07b0b6 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/unicode.json @@ -0,0 +1,38 @@ +[ + { + "given": {"foo": [{"✓": "✓"}, {"✓": "✗"}]}, + "cases": [ + { + "expression": "foo[].\"✓\"", + "result": ["✓", "✗"] + } + ] + }, + { + "given": {"☯": true}, + "cases": [ + { + "expression": "\"☯\"", + "result": true + } + ] + }, + { + "given": {"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪": true}, + "cases": [ + { + "expression": "\"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪\"", + "result": true + } + ] + }, + { + "given": {"☃": true}, + "cases": [ + { + "expression": "\"☃\"", + "result": true + } + ] + } +] diff --git a/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/wildcard.json b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/wildcard.json new file mode 100644 index 00000000..3bcec302 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.JMESPath.Tests/test_files/wildcard.json @@ -0,0 +1,460 @@ +[{ + "given": { + "foo": { + "bar": { + "baz": "val" + }, + "other": { + "baz": "val" + }, + "other2": { + "baz": "val" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["a", "b", "c"] + }, + "other5": { + "other": { + "a": 1, + "b": 1, + "c": 1 + } + } + } + }, + "cases": [ + { + "expression": "foo.*.baz", + "result": ["val", "val", "val"] + }, + { + "expression": "foo.bar.*", + "result": ["val"] + }, + { + "expression": "foo.*.notbaz", + "result": [["a", "b", "c"], ["a", "b", "c"]] + }, + { + "expression": "foo.*.notbaz[0]", + "result": ["a", "a"] + }, + { + "expression": "foo.*.notbaz[-1]", + "result": ["c", "c"] + } + ] +}, { + "given": { + "foo": { + "first-1": { + "second-1": "val" + }, + "first-2": { + "second-1": "val" + }, + "first-3": { + "second-1": "val" + } + } + }, + "cases": [ + { + "expression": "foo.*", + "result": [{"second-1": "val"}, {"second-1": "val"}, + {"second-1": "val"}] + }, + { + "expression": "foo.*.*", + "result": [["val"], ["val"], ["val"]] + }, + { + "expression": "foo.*.*.*", + "result": [[], [], []] + }, + { + "expression": "foo.*.*.*.*", + "result": [[], [], []] + } + ] +}, { + "given": { + "foo": { + "bar": "one" + }, + "other": { + "bar": "one" + }, + "nomatch": { + "notbar": "three" + } + }, + "cases": [ + { + "expression": "*.bar", + "result": ["one", "one"] + } + ] +}, { + "given": { + "top1": { + "sub1": {"foo": "one"} + }, + "top2": { + "sub1": {"foo": "one"} + } + }, + "cases": [ + { + "expression": "*", + "result": [{"sub1": {"foo": "one"}}, + {"sub1": {"foo": "one"}}] + }, + { + "expression": "*.sub1", + "result": [{"foo": "one"}, + {"foo": "one"}] + }, + { + "expression": "*.*", + "result": [[{"foo": "one"}], + [{"foo": "one"}]] + }, + { + "expression": "*.*.foo[]", + "result": ["one", "one"] + }, + { + "expression": "*.sub1.foo", + "result": ["one", "one"] + } + ] +}, +{ + "given": + {"foo": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}]}, + "cases": [ + { + "expression": "foo[*].bar", + "result": ["one", "two", "three"] + }, + { + "expression": "foo[*].notbar", + "result": ["four"] + } + ] +}, +{ + "given": + [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}], + "cases": [ + { + "expression": "[*]", + "result": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}] + }, + { + "expression": "[*].bar", + "result": ["one", "two", "three"] + }, + { + "expression": "[*].notbar", + "result": ["four"] + } + ] +}, +{ + "given": { + "foo": { + "bar": [ + {"baz": ["one", "two", "three"]}, + {"baz": ["four", "five", "six"]}, + {"baz": ["seven", "eight", "nine"]} + ] + } + }, + "cases": [ + { + "expression": "foo.bar[*].baz", + "result": [["one", "two", "three"], ["four", "five", "six"], ["seven", "eight", "nine"]] + }, + { + "expression": "foo.bar[*].baz[0]", + "result": ["one", "four", "seven"] + }, + { + "expression": "foo.bar[*].baz[1]", + "result": ["two", "five", "eight"] + }, + { + "expression": "foo.bar[*].baz[2]", + "result": ["three", "six", "nine"] + }, + { + "expression": "foo.bar[*].baz[3]", + "result": [] + } + ] +}, +{ + "given": { + "foo": { + "bar": [["one", "two"], ["three", "four"]] + } + }, + "cases": [ + { + "expression": "foo.bar[*]", + "result": [["one", "two"], ["three", "four"]] + }, + { + "expression": "foo.bar[0]", + "result": ["one", "two"] + }, + { + "expression": "foo.bar[0][0]", + "result": "one" + }, + { + "expression": "foo.bar[0][0][0]", + "result": null + }, + { + "expression": "foo.bar[0][0][0][0]", + "result": null + }, + { + "expression": "foo[0][0]", + "result": null + } + ] +}, +{ + "given": { + "foo": [ + {"bar": [{"kind": "basic"}, {"kind": "intermediate"}]}, + {"bar": [{"kind": "advanced"}, {"kind": "expert"}]}, + {"bar": "string"} + ] + + }, + "cases": [ + { + "expression": "foo[*].bar[*].kind", + "result": [["basic", "intermediate"], ["advanced", "expert"]] + }, + { + "expression": "foo[*].bar[0].kind", + "result": ["basic", "advanced"] + } + ] +}, +{ + "given": { + "foo": [ + {"bar": {"kind": "basic"}}, + {"bar": {"kind": "intermediate"}}, + {"bar": {"kind": "advanced"}}, + {"bar": {"kind": "expert"}}, + {"bar": "string"} + ] + }, + "cases": [ + { + "expression": "foo[*].bar.kind", + "result": ["basic", "intermediate", "advanced", "expert"] + } + ] +}, +{ + "given": { + "foo": [{"bar": ["one", "two"]}, {"bar": ["three", "four"]}, {"bar": ["five"]}] + }, + "cases": [ + { + "expression": "foo[*].bar[0]", + "result": ["one", "three", "five"] + }, + { + "expression": "foo[*].bar[1]", + "result": ["two", "four"] + }, + { + "expression": "foo[*].bar[2]", + "result": [] + } + ] +}, +{ + "given": { + "foo": [{"bar": []}, {"bar": []}, {"bar": []}] + }, + "cases": [ + { + "expression": "foo[*].bar[0]", + "result": [] + } + ] +}, +{ + "given": { + "foo": [["one", "two"], ["three", "four"], ["five"]] + }, + "cases": [ + { + "expression": "foo[*][0]", + "result": ["one", "three", "five"] + }, + { + "expression": "foo[*][1]", + "result": ["two", "four"] + } + ] +}, +{ + "given": { + "foo": [ + [ + ["one", "two"], ["three", "four"] + ], [ + ["five", "six"], ["seven", "eight"] + ], [ + ["nine"], ["ten"] + ] + ] + }, + "cases": [ + { + "expression": "foo[*][0]", + "result": [["one", "two"], ["five", "six"], ["nine"]] + }, + { + "expression": "foo[*][1]", + "result": [["three", "four"], ["seven", "eight"], ["ten"]] + }, + { + "expression": "foo[*][0][0]", + "result": ["one", "five", "nine"] + }, + { + "expression": "foo[*][1][0]", + "result": ["three", "seven", "ten"] + }, + { + "expression": "foo[*][0][1]", + "result": ["two", "six"] + }, + { + "expression": "foo[*][1][1]", + "result": ["four", "eight"] + }, + { + "expression": "foo[*][2]", + "result": [] + }, + { + "expression": "foo[*][2][2]", + "result": [] + }, + { + "expression": "bar[*]", + "result": null + }, + { + "expression": "bar[*].baz[*]", + "result": null + } + ] +}, +{ + "given": { + "string": "string", + "hash": {"foo": "bar", "bar": "baz"}, + "number": 23, + "nullvalue": null + }, + "cases": [ + { + "expression": "string[*]", + "result": null + }, + { + "expression": "hash[*]", + "result": null + }, + { + "expression": "number[*]", + "result": null + }, + { + "expression": "nullvalue[*]", + "result": null + }, + { + "expression": "string[*].foo", + "result": null + }, + { + "expression": "hash[*].foo", + "result": null + }, + { + "expression": "number[*].foo", + "result": null + }, + { + "expression": "nullvalue[*].foo", + "result": null + }, + { + "expression": "nullvalue[*].foo[*].bar", + "result": null + } + ] +}, +{ + "given": { + "string": "string", + "hash": {"foo": "val", "bar": "val"}, + "number": 23, + "array": [1, 2, 3], + "nullvalue": null + }, + "cases": [ + { + "expression": "string.*", + "result": null + }, + { + "expression": "hash.*", + "result": ["val", "val"] + }, + { + "expression": "number.*", + "result": null + }, + { + "expression": "array.*", + "result": null + }, + { + "expression": "nullvalue.*", + "result": null + } + ] +}, +{ + "given": { + "a": [0, 1, 2], + "b": [0, 1, 2] + }, + "cases": [ + { + "expression": "*[0]", + "result": [0, 0] + } + ] +} +] diff --git a/mkdocs.yml b/mkdocs.yml index bcb5c176..1fe01511 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,7 @@ nav: - utilities/parameters.md - utilities/idempotency.md - utilities/batch-processing.md + - utilities/jmespath-functions.md theme: name: material @@ -48,7 +49,6 @@ theme: - navigation.tracking - content.code.annotate - toc.follow - - toc.integrate - announce.dismiss icon: repo: fontawesome/brands/github @@ -82,7 +82,7 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format - md_in_html -copyright: Copyright © 2023 Amazon Web Services +copyright: Copyright © 2024 Amazon Web Services plugins: - git-revision-date