"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.reconcileDiff = exports.diff = exports.typeofDiff = void 0; const json_pointer_helpers_1 = require("@useoptic/json-pointer-helpers"); const array_identifiers_1 = require("./array-identifiers"); const openapi_matchers_1 = require("./openapi-matchers"); const openapi_traverser_1 = require("../openapi3/implementations/openapi3/openapi-traverser"); function typeofDiff(diff) { return diff.after !== undefined && diff.before !== undefined ? 'changed' : diff.after !== undefined ? 'added' : 'removed'; } exports.typeofDiff = typeofDiff; // Diffs two objects, generating the leaf nodes that have changes function diff(before, after, identityPrefix = '') { const diffResults = []; const stack = [ [ { value: before, path: '' }, { value: after, path: '' }, ], ]; while (stack.length > 0) { const [before, after] = stack.pop(); const comparisons = []; const beforePathIdentity = json_pointer_helpers_1.jsonPointerHelpers.join(identityPrefix, before.path); const afterPathIdentity = json_pointer_helpers_1.jsonPointerHelpers.join(identityPrefix, after.path); // TODO in the future, skip adding comparisons based on diff preprocessing step // Start by matching up values to compare - match up the before and after values by id // for arrays, we look at some known OpenAPI identifiers (such as parameters, or primitives) and fallback to using positional identity // for objects, we just use the key if (Array.isArray(before.value) && Array.isArray(after.value)) { const allValues = [...before.value, ...after.value]; const arrayIdFn = (0, openapi_matchers_1.isPathParameterArray)(beforePathIdentity) && (0, openapi_matchers_1.isPathParameterArray)(afterPathIdentity) ? array_identifiers_1.getParameterIdentity : allValues.every((v) => typeof v !== 'object') ? (v) => String(v) : (_, i) => String(i); const beforeValuesById = new Map(before.value.map((v, i) => [arrayIdFn(v, i), [v, i]])); const afterValuesById = new Map(after.value.map((v, i) => [arrayIdFn(v, i), [v, i]])); const keys = new Set([ ...beforeValuesById.keys(), ...afterValuesById.keys(), ]); for (const key of keys) { const [beforeValue, beforeIdx] = beforeValuesById.get(key) ?? []; const [afterValue, afterIdx] = afterValuesById.get(key) ?? []; const beforePath = json_pointer_helpers_1.jsonPointerHelpers.append(before.path, String(beforeIdx)); const afterPath = json_pointer_helpers_1.jsonPointerHelpers.append(after.path, String(afterIdx)); comparisons.push({ beforeValue, beforePath, afterValue, afterPath, }); } } else if (!Array.isArray(before.value) && !Array.isArray(after.value)) { const objectIdFn = (0, openapi_matchers_1.isPathsMap)(before.path) && (0, openapi_matchers_1.isPathsMap)(after.path) ? (key) => (0, openapi_traverser_1.normalizeOpenApiPath)(key) : (key, value) => String(key); const beforeValuesById = new Map(Object.entries(before.value).map(([k, v]) => [objectIdFn(k, v), [v, k]])); const afterValuesById = new Map(Object.entries(after.value).map(([k, v]) => [objectIdFn(k, v), [v, k]])); const keys = new Set([ ...beforeValuesById.keys(), ...afterValuesById.keys(), ]); for (const key of keys) { const [beforeValue, beforeId] = beforeValuesById.get(key) ?? []; const beforePath = json_pointer_helpers_1.jsonPointerHelpers.append(before.path, String(beforeId)); const [afterValue, afterId] = afterValuesById.get(key) ?? []; const afterPath = json_pointer_helpers_1.jsonPointerHelpers.append(after.path, String(afterId)); comparisons.push({ beforeValue, beforePath, afterValue, afterPath, }); } } else { throw new Error('Unexpectedly found mismatch between array and object in diff traversal'); } // Once we've matched up comparisons to make, we can determine if a key is added, removed or changed // If both are objects / arrays, we continue the diff for (const { beforeValue, beforePath, afterValue, afterPath, } of comparisons) { if (beforeValue !== undefined && afterValue === undefined) { // Because before + after paths can change diverge due to array rearrangement, we need to track this to determine from where something was removed in an after spec // We don't need to look at the last key to see path differences since const beforeParts = json_pointer_helpers_1.jsonPointerHelpers.decode(beforePath).slice(0, -1); const afterParts = json_pointer_helpers_1.jsonPointerHelpers.decode(afterPath).slice(0, -1); const pathReconciliation = []; for (let i = 0; i < beforeParts.length; i++) { const before = beforeParts[i]; const after = afterParts[i]; if (before !== after) { pathReconciliation.push([i, after]); } } // generate path reconciliation diffResults.push({ before: beforePath, pathReconciliation, }); } else if (beforeValue === undefined && afterValue !== undefined) { diffResults.push({ after: afterPath, }); } else { // Check if values are both objects OR both arrays, if they are continue traversing if (typeof beforeValue === 'object' && beforeValue !== null && typeof afterValue === 'object' && afterValue !== null && !Array.isArray(beforeValue) && !Array.isArray(afterValue)) { stack.push([ { value: beforeValue, path: beforePath }, { value: afterValue, path: afterPath }, ]); } else if (typeof beforeValue === 'object' && beforeValue !== null && typeof afterValue === 'object' && afterValue !== null && Array.isArray(beforeValue) && Array.isArray(afterValue)) { stack.push([ { value: beforeValue, path: beforePath }, { value: afterValue, path: afterPath }, ]); } // Next, check if values are the same (strict equality, no deep comparison) else if (beforeValue === afterValue) { // do nothing, because the values are the same // this will fail if types mismatch } else { diffResults.push({ before: beforePath, after: afterPath, }); } } } } return diffResults; } exports.diff = diff; function reconcileDiff(diff) { if (diff.pathReconciliation) { const previousPath = diff.before; const parts = json_pointer_helpers_1.jsonPointerHelpers.decode(previousPath); for (const [index, replacement] of diff.pathReconciliation) { parts[index] = replacement; } return { before: json_pointer_helpers_1.jsonPointerHelpers.compile(parts), after: undefined, pathReconciliation: [], }; } else { return diff; } } exports.reconcileDiff = reconcileDiff; //# sourceMappingURL=diff.js.map