Repository: ChristopheBougere/asl-validator (ts)

Issue:
Validation error for Parameters
**Describe the bug**
New error based on changes made in [101](https://github.com/ChristopheBougere/asl-validator/issues/101) / [102](https://github.com/ChristopheBougere/asl-validator/issues/102)
Validation error
```
 {
    'Error code': 'SCHEMA_VALIDATION_FAILED',
    Message: '/States/Publish to Slack/Parameters/slackMessage/blocks is invalid. must match exactly one schema in oneOf',
    schemaError: {
      instancePath: '/States/Publish to Slack/Parameters/slackMessage/blocks',
      schemaPath: '#/patternProperties/%5E.%2B((%5B%5E.%5D%5B%5E%24%5D)%7C(%5B%5E.%5D%5B%24%5D))%24/oneOf'
    }
  }
```

**To Reproduce**
Please provide a complete state machine definition:
```json
{
  "StartAt": "Publish to Slack",
  "States": {
      "Publish to Slack": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:::function:publish-to-slack-lambda",
      "Parameters": {
        "slackMessage": {
          "channel.$": "$.slackErrorChannel",
          "text": "$.slackMessage.title",
          "blocks": [
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text.$": "$.slackMessage.errorMessage"
              }
            },
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text.$": "$.slackMessage.orderId"
              }
            },
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text.$": "$.slackMessage.orderItemId"
              }
            },
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text.$": "$.slackMessage.stack"
              }
            },
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text.$": "$.slackMessage.link"
              }
            }
          ]
        }
      },
      "Next": "Failure"
    },
    "Failure": {
      "Type": "Fail"
    },
    "Success": {
      "Type": "Succeed"
    }
  }
}
```

**Expected behavior**
No validation error

**Version:**
3.0.2
**Additional context**
Add any other context about the problem here.


Pull request description:
fix schema definition for payload template to avoid the oneOf options intersecting
AJV is failing the input schema since it is matching on two of the available types in its `oneOf` definition. 

closes #104 

fix(schemas): recursive def fixed where oneOf options intersected on array types
fix(check): catch jsonpath error in duplicate-payload-template-fields.ts
refactor(check): decode URI encoded values for cleaner error reporting

Patch:
diff --git a/src/checks/duplicate-payload-template-fields.ts b/src/checks/duplicate-payload-template-fields.ts
index 4397ffb..0fa4076 100644
--- a/src/checks/duplicate-payload-template-fields.ts
+++ b/src/checks/duplicate-payload-template-fields.ts
@@ -9,33 +9,40 @@ export const mustNotHaveDuplicateFieldNamesAfterEvaluation: AslChecker = (defini
     // for each one, examine each node's children for field names
     // that will be in conflict after evaluation.
     const errorMessages: StateMachineError[] = [];
-    JSONPath<Record<string, unknown>[]>({
-        json: definition,
-        // this uses some extensions to the original JSONPath spec supported by the lib
-        //
-        // extension 1: @property.match enables matching on the matched property name.
-        //              This finds and selects all nodes with a name ending in .$
-        // extension 2: ^ operator twice at the end selects the parent node instead of the value for the node
-        //
-        // also, don't miss the array offset [0] before selecting the grandparent.
-        // There may be multiple fields within a node that could be in conflict.
-        // We're checking all the keys in the parent node, so we only need a single
-        // result for that parent.
-        path: `$..[Parameters,ResultSelector]..*[?(@property.match(/^.+\\.\\$$/))][0]^^`
-    })
-        .forEach((parent) => {
-            const keys: Record<string, number> = {};
-            Object.keys(parent).forEach((key) => {
-                const keyPostEval = key.endsWith(".$") ? key.substring(0, key.length - 2) : key;
-                const current = keys[keyPostEval];
-                if (current) {
-                    errorMessages.push({
-                        'Error code': StateMachineErrorCode.DuplicateFieldName,
-                        Message: `A duplicate field will exist after renaming to strip the ".$" suffix from: ${key}.$`,
-                    });
-                }
-                keys[keyPostEval] = current ? current + 1 : 1;
-            });
+    let valuesToInspect: Record<string, unknown>[] = [];
+    try {
+        valuesToInspect = JSONPath<Record<string, unknown>[]>({
+            json: definition,
+            // this uses some extensions to the original JSONPath spec supported by the lib
+            //
+            // extension 1: @property.match enables matching on the matched property name.
+            //              This finds and selects all nodes with a name ending in .$
+            // extension 2: ^ operator twice at the end selects the parent node instead of the value for the node
+            //
+            // also, don't miss the array offset [0] before selecting the grandparent.
+            // There may be multiple fields within a node that could be in conflict.
+            // We're checking all the keys in the parent node, so we only need a single
+            // result for that parent.
+            path: `$..[Parameters,ResultSelector]..*[?(@property.match(/^.+\\.\\$$/))][0]^^`
+        })
+    } catch (err: unknown) {
+        // need to revisit this since the JSONPath lib throws an error when it encounters an array
+        // Error: jsonPath: _$_property.match is not a function: _$_property.match(/^.+\.\$$/)
+    }
+
+    valuesToInspect.forEach((parent) => {
+        const keys: Record<string, number> = {};
+        Object.keys(parent).forEach((key) => {
+            const keyPostEval = key.endsWith(".$") ? key.substring(0, key.length - 2) : key;
+            const current = keys[keyPostEval];
+            if (current) {
+                errorMessages.push({
+                    'Error code': StateMachineErrorCode.DuplicateFieldName,
+                    Message: `A duplicate field will exist after renaming to strip the ".$" suffix from: ${key}.$`,
+                });
+            }
+            keys[keyPostEval] = current ? current + 1 : 1;
         });
+    });
     return errorMessages;
-}
\ No newline at end of file
+}
diff --git a/src/checks/json-schema-errors.ts b/src/checks/json-schema-errors.ts
index 333a03d..de695ed 100644
--- a/src/checks/json-schema-errors.ts
+++ b/src/checks/json-schema-errors.ts
@@ -93,7 +93,7 @@ export const jsonSchemaErrors: AslChecker = (definition, options) => {
             Message: `${error.instancePath} is invalid. ${error.message ?? ""}`,
             schemaError: {
                 instancePath: error.instancePath,
-                schemaPath: error.schemaPath
+                schemaPath: decodeURIComponent(error.schemaPath)
             }
         }));
-}
\ No newline at end of file
+}
diff --git a/src/schemas/paths.json b/src/schemas/paths.json
index 04e276b..cb5d0d2 100644
--- a/src/schemas/paths.json
+++ b/src/schemas/paths.json
@@ -37,7 +37,7 @@
               }
             },
             {
-              "$ref": "#/definitions/_asl_payload_template"
+              "$ref": "#/definitions/_payload_template_object"
             }
           ]
         }


Test patch:
diff --git a/src/__tests__/definitions/valid-parameters-issue104.json b/src/__tests__/definitions/valid-parameters-issue104.json
new file mode 100644
index 0000000..31a0f17
--- /dev/null
+++ b/src/__tests__/definitions/valid-parameters-issue104.json
@@ -0,0 +1,28 @@
+{
+  "StartAt": "Publish to Slack",
+  "States": {
+    "Publish to Slack": {
+      "Type": "Task",
+      "Resource": "arn:aws:lambda:::function:publish-to-slack-lambda",
+      "Parameters": {
+        "slackMessage": {
+          "channel.$": "$.slackErrorChannel",
+          "text.$": "$.slackMessage.title",
+          "blocks": [
+            {
+              "type": "section",
+              "text": {
+                "type": "mrkdwn",
+                "text.$": "$.slackMessage.errorMessage"
+              }
+            }
+          ]
+        }
+      },
+      "Next": "Failure"
+    },
+    "Failure": {
+      "Type": "Fail"
+    }
+  }
+}
diff --git a/src/__tests__/error-reporting.test.ts b/src/__tests__/error-reporting.test.ts
index 76a6cb6..a9ca239 100644
--- a/src/__tests__/error-reporting.test.ts
+++ b/src/__tests__/error-reporting.test.ts
@@ -43,8 +43,7 @@ describe("tests with definitions containing errors to see what's reported", () =
                     Message: "/States/Hello, World/Parameters/lorem.$ is invalid. must match format \"asl_payload_template\"",
                     schemaError: {
                         instancePath: "/States/Hello, World/Parameters/lorem.$",
-                        // the regular expression in the path is escaped by AJV when it reports the location of the error
-                        schemaPath: "#/patternProperties/%5E.%2B%5C.%5C%24%24/format"
+                        schemaPath: "#/patternProperties/^.+\\.\\$$/format"
                     }
                 }
             ]