Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-crabs-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/respect-core": patch
---

Made Respect's JSONPath criteria compliant with RFC 9535.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,211 +1,10 @@
import { query, type JsonValue } from 'jsonpath-rfc9535';

export function evaluateJSONPathCondition(condition: string, context: JsonValue) {
export function evaluateJSONPathCondition(condition: string, context: JsonValue): boolean {
try {
const resolvedCondition = parseExpressions(condition, context);
const evaluateFn = new Function(`return ${resolvedCondition};`);

return !!evaluateFn();
} catch (error) {
const result = query([context], condition);
return result.length > 0;
} catch {
return false;
}
}

function parseExpressions(condition: string, context: JsonValue): string {
const expressionsParts: Array<string> = [];

let i = 0;
let expressionElements = '';

while (i < condition.length) {
if (condition[i] === '$') {
if (expressionElements.length > 0) {
expressionsParts.push(expressionElements);
expressionElements = '';
}
const start = i;
const expression = parseSingleJSONPath(condition, i);

if (expression.length > 1) {
const evaluatedExpression = evaluateJSONPathExpression(expression, context);

expressionsParts.push(evaluatedExpression);
}
i = start + expression.length;
} else {
expressionElements += condition[i];
i++;
}
}

// Push any remaining content after the while loop
if (expressionElements.length > 0) {
expressionsParts.push(expressionElements);
}

return expressionsParts.join('');
}

function parseSingleJSONPath(condition: string, startIndex: number): string {
let jsonpath = '$';
let bracketDepth = 0;
let inQuotes = false;
let quoteChar = '';
let inFilter = false;
let filterDepth = 0;
let i = startIndex + 1; // Skip the '$'

while (i < condition.length) {
const char = condition[i];

if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar = char;
jsonpath += char;
i++;
continue;
}

if (char === quoteChar && inQuotes) {
inQuotes = false;
jsonpath += char;
i++;
continue;
}

if (inQuotes) {
jsonpath += char;
i++;
continue;
}

if (char === '[') {
bracketDepth++;
if (i + 1 < condition.length && condition[i + 1] === '?') {
inFilter = true;
filterDepth = bracketDepth;
}
jsonpath += char;
i++;
continue;
}

if (char === ']') {
bracketDepth--;
if (inFilter && bracketDepth < filterDepth) {
inFilter = false;
}
jsonpath += char;
i++;
continue;
}

// Stop at logical operators, comparison operators, or whitespace (outside of filters)
if (!inFilter && (/\s/.test(char) || /[<>=!&|,)]/.test(char))) {
break;
}

jsonpath += char;
i++;
}

return jsonpath;
}

function evaluateJSONPathExpression(expression: string, context: JsonValue): string {
// Handle legacy .length suffix for backward compatibility that is not a valid RFC 9535 expression
if (expression.endsWith('.length')) {
const basePath = expression.slice(0, -'.length'.length);
const normalizedPath = transformHyphensToUnderscores(basePath);
const result = query(context, normalizedPath);
const value = result[0] ?? null;
return Array.isArray(value) ? String(value.length) : '0';
}

if (expression.includes('[?(') && expression.includes(')]')) {
return handleFilterExpression(expression, context);
}

const normalizedPath = transformHyphensToUnderscores(expression);
const result = query(context, normalizedPath);
return JSON.stringify(result[0]);
}

function handleFilterExpression(expression: string, context: JsonValue): string {
const filterMatch = expression.match(/\[\?\((.*)\)\]/);
if (!filterMatch) return 'false';

const filterCondition = filterMatch[1];
const basePath = expression.substring(0, expression.indexOf('[?('));
const normalizedBasePath = transformHyphensToUnderscores(basePath);
const jsonpathResult = query(context, normalizedBasePath);

// Flatten the result in case JSONPath returns nested arrays
const arrayToFilter = Array.isArray(jsonpathResult) ? jsonpathResult.flat() : jsonpathResult;

if (!Array.isArray(arrayToFilter)) {
return 'false';
}

const filteredArray = arrayToFilter.filter((item: unknown) => {
const convertedCondition = processFilterCondition(filterCondition, item);

try {
const safeEval = new Function('item', `return ${convertedCondition};`);
return !!safeEval(item);
} catch {
return false;
}
});

const afterFilter = expression.substring(expression.indexOf(')]') + 2);

if (afterFilter.startsWith('.')) {
const propertyMatch = afterFilter.match(/\.([a-zA-Z0-9_-]+)/);
if (propertyMatch) {
const propertyName = propertyMatch[1].replace(/-/g, '_');
const propertyValues = filteredArray.map(
(item: unknown) => (item as Record<string, unknown>)[propertyName]
);
return JSON.stringify(propertyValues);
}
}

return filteredArray.length > 0 ? JSON.stringify(filteredArray) : 'false';
}

function processFilterCondition(filterCondition: string, item: unknown): string {
let convertedCondition = filterCondition;

// Handle @.property.match(/pattern/) expressions
convertedCondition = convertedCondition.replace(
/@\.([a-zA-Z0-9_-]+)\.match\(([^)]+)\)/g,
(_, prop: string, pattern: string) => {
const normalizedProp = prop.replace(/-/g, '_');
const value = (item as Record<string, unknown>)[normalizedProp];
if (typeof value !== 'string') return 'false';

try {
let cleanPattern = pattern.replace(/^["']|["']$/g, ''); // Remove quotes
cleanPattern = cleanPattern.replace(/^\/|\/$/g, ''); // Remove leading/trailing slashes
const regex = new RegExp(cleanPattern);
return String(regex.test(value));
} catch {
return 'false';
}
}
);

// Handle @.property expressions (simple property access)
convertedCondition = convertedCondition.replace(/@\.([a-zA-Z0-9_-]+)/g, (_, prop: string) => {
const normalizedProp = prop.replace(/-/g, '_');
const value = (item as Record<string, unknown>)[normalizedProp];
return JSON.stringify(value);
});

return convertedCondition;
}

function transformHyphensToUnderscores(path: string): string {
return path.replace(/\.([a-zA-Z0-9_-]+)/g, (_, prop) => '.' + prop.replace(/-/g, '_'));
}
4 changes: 2 additions & 2 deletions resources/museum-api.arazzo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
outputs:
createdEventId: $response.body#/eventId
Expand Down Expand Up @@ -104,7 +104,7 @@ workflows:
successCriteria:
- condition: $statusCode == 200
- context: $response.body
condition: $.name == 'Orca Identification and Analysis'
condition: "$[?@.name == 'Orca Identification and Analysis']"
type:
type: jsonpath
version: draft-goessner-dispatch-jsonpath-00
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ exports[`should pass inputs to step target workflow with additional input parame
      }

    ✗ success criteria check - $statusCode == 201
    ✗ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✗ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand All @@ -59,7 +59,7 @@ exports[`should pass inputs to step target workflow with additional input parame
      Checking simple criteria: {"condition":"$statusCode == 201"}

    ✗ success criteria check
      Checking jsonpath criteria: $.name == 'Mermaid Treasure Identification and Analysis'
      Checking jsonpath criteria: $[?@.name == 'Mermaid Treasure Identification and Analysis']

  Summary for inputs-passed-to-step-target-workflow-and-remapped.arazzo.yaml

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ exports[`should pass inputs to step target workflow with additional input parame
      }

    ✓ success criteria check - $statusCode == 201
    ✓ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✓ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ exports[`should use inputs from CLI and env 1`] = `
      }

    ✓ success criteria check - $statusCode == 201
    ✓ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✓ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,5 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ exports[`should hide sensitive input values 1`] = `
      }

    ✓ success criteria check - $statusCode == 201
    ✓ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✓ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,5 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ exports[`should reveal masked input values 1`] = `
      }

    ✓ success criteria check - $statusCode == 201
    ✓ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✓ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,5 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ exports[`should use error severity level 1`] = `
      }

    ✓ success criteria check - $statusCode == 201
    ✓ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✓ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand Down Expand Up @@ -159,7 +159,7 @@ exports[`should use error severity level 1`] = `
      }

    ✓ success criteria check - $statusCode == 200
    ✓ success criteria check - $.name == 'Orca Identification and Analysis'
    ✓ success criteria check - $[?@.name == 'Orca Identification and Analysis']
    ✓ status code check - $statusCode in [200, 400, 404]
    ✗ content-type check

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ workflows:
successCriteria:
- condition: $statusCode == 201
- context: $response.body
condition: $.name == 'Mermaid Treasure Identification and Analysis'
condition: "$[?@.name == 'Mermaid Treasure Identification and Analysis']"
type: jsonpath
outputs:
createdEventId: $response.body#/eventId
Expand All @@ -81,7 +81,7 @@ workflows:
successCriteria:
- condition: $statusCode == 200
- context: $response.body
condition: $.name == 'Orca Identification and Analysis'
condition: "$[?@.name == 'Orca Identification and Analysis']"
type:
type: jsonpath
version: draft-goessner-dispatch-jsonpath-00
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ exports[`should use off severity level 1`] = `
      }

    ✓ success criteria check - $statusCode == 201
    ✓ success criteria check - $.name == 'Mermaid Treasure Identification and Ana...
    ✓ success criteria check - $[?@.name == 'Mermaid Treasure Identification and ...
    ✓ status code check - $statusCode in [201, 400, 404]
    ✓ content-type check
    ✓ schema check
Expand Down Expand Up @@ -159,7 +159,7 @@ exports[`should use off severity level 1`] = `
      }

    ✓ success criteria check - $statusCode == 200
    ✓ success criteria check - $.name == 'Orca Identification and Analysis'
    ✓ success criteria check - $[?@.name == 'Orca Identification and Analysis']
    ✓ status code check - $statusCode in [200, 400, 404]
    ✗ content-type check

Expand Down
Loading
Loading