UNPKG

object-deep-compare

Version:

A type-safe collection of comparison methods for objects and arrays in TypeScript/JavaScript

242 lines (210 loc) 7.48 kB
import { PathFilter } from '../types'; /** * Checks if a given path matches any of the provided patterns * Supports wildcard patterns: * - '.fieldName' matches any property named 'fieldName' at any level * - 'parent.*.child' matches any path like 'parent.something.child' * - 'parent[*].child' matches any array index like 'parent[0].child' * * @param path - The property path to check * @param patterns - Array of patterns to match against * @returns Whether the path matches any of the patterns */ export const matchesPathPattern = (path: string, patterns: string[]): boolean => { if (!patterns || patterns.length === 0) { return false; } for (const pattern of patterns) { // Handle leading dot for any level match if (pattern.startsWith('.')) { const fieldName = pattern.substring(1); // Check if path equals the field name, ends with the field name, or contains it as a property name if (path === fieldName || path.endsWith(`.${fieldName}`) || path.includes(`${fieldName}.`) || path.match(new RegExp(`\\[\\d+\\]\\.${fieldName}`)) || // Match pattern[0].fieldName path.match(new RegExp(`\\.${fieldName}\\[`))) { // Match pattern.fieldName[ return true; } continue; } // Handle exact match if (pattern === path) { return true; } // Match array index patterns if (pattern.includes('[*]')) { const arrayPattern = pattern.replace(/\[\*\]/g, '\\[\\d+\\]'); const regexPattern = '^' + arrayPattern.replace(/\./g, '\\.') + '$'; try { const regex = new RegExp(regexPattern); if (regex.test(path)) { return true; } } catch (e) { // If regex fails, fall back to exact match } } // Match wildcard patterns if (pattern.includes('*')) { // Convert pattern to regex const regexPattern = '^' + pattern .replace(/\./g, '\\.') .replace(/\[/g, '\\[') .replace(/\]/g, '\\]') .replace(/\*/g, '[^.\\[\\]]*') + '$'; try { const regex = new RegExp(regexPattern); if (regex.test(path)) { return true; } } catch (e) { // If regex fails, fall back to exact match } } // Check for parent paths in array cases // If the pattern is 'posts' and the path is 'posts[0].title', it should match if (path.startsWith(pattern + '[') || path.startsWith(pattern + '.')) { return true; } } return false; }; /** * Determine if a path or any of its parent paths should be filtered out * This helps handle structured data like arrays where we might want to filter * at the parent level * * @param path - The property path to check * @param pathFilter - Path filter configuration * @returns Whether the path should be filtered */ export const shouldFilterPath = (path: string, pathFilter?: PathFilter): boolean => { if (!pathFilter || !pathFilter.patterns || pathFilter.patterns.length === 0) { return false; // If no filter is defined, nothing is filtered } // Check if the path itself matches any pattern if (matchesPathPattern(path, pathFilter.patterns)) { return true; } // Check for parent paths in case of arrays // For example, if filtering 'posts.*.title', we should also filter 'posts' // This is needed because arrays report conflicts at the parent level const parts = path.split('.'); let currentPath = ''; for (const part of parts) { // Handle array notation in path segments const arrayMatch = part.match(/^([^\[]+)(\[\d+\])(.*)$/); if (arrayMatch) { const beforeBracket = arrayMatch[1]; const bracketPart = arrayMatch[2]; const afterBracket = arrayMatch[3]; // Build the path up to this segment if (currentPath) { currentPath += '.'; } currentPath += beforeBracket; // Check if this array path matches any pattern if (matchesPathPattern(currentPath, pathFilter.patterns)) { return true; } // Include the bracket part and continue currentPath += bracketPart; if (afterBracket) { currentPath += afterBracket; } } else { // Handle normal path segments if (currentPath) { currentPath += '.'; } currentPath += part; // Check if this path matches any pattern if (matchesPathPattern(currentPath, pathFilter.patterns)) { return true; } } } return false; }; /** * Determines if a path should be compared based on the pathFilter configuration * * @param path - The property path to check * @param pathFilter - Path filter configuration * @returns Whether the path should be compared */ export const shouldComparePath = (path: string, pathFilter?: PathFilter): boolean => { if (!pathFilter || !pathFilter.patterns || pathFilter.patterns.length === 0) { return true; // If no filter is defined, compare all paths } const mode = pathFilter.mode || 'exclude'; // If we're in exclude mode, check if the path matches any pattern if (mode === 'exclude') { return !shouldFilterPath(path, pathFilter); } // For include mode, we need more flexible matching // Direct match - check if the path exactly matches any pattern if (pathFilter.patterns.includes(path)) { return true; } // Check if path matches any pattern if (shouldFilterPath(path, pathFilter)) { return true; } // Handle array notation specially if (path.includes('[')) { // Convert array indices to wildcards for matching const wildcardPath = path.replace(/\[\d+\]/g, '[*]'); if (pathFilter.patterns.includes(wildcardPath)) { return true; } // Check array element direct match // e.g., if pattern is '[*].content', path could be '[0].content' for (const pattern of pathFilter.patterns) { if (pattern.startsWith('[*]') && path.match(/^\[\d+\]/)) { const patternSuffix = pattern.substring(3); // Remove '[*]' const pathSuffix = path.replace(/^\[\d+\]/, ''); // Remove '[0]' if (patternSuffix === pathSuffix) { return true; } } } } // Check parent paths for include patterns // e.g., if pattern is 'settings.*', we should include 'settings.theme' const parts = path.split('.'); let currentPath = ''; for (let i = 0; i < parts.length; i++) { if (i > 0) { currentPath += '.'; } currentPath += parts[i]; // Check if the current path segment followed by wildcard is in patterns const wildcardPattern = `${currentPath}.*`; if (pathFilter.patterns.includes(wildcardPattern)) { return true; } // Also check for other wildcard patterns for (const pattern of pathFilter.patterns) { if (pattern.includes('*') && !pattern.startsWith('.')) { // Convert pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\[/g, '\\[') .replace(/\]/g, '\\]') .replace(/\*/g, '[^.\\[\\]]*'); try { const regex = new RegExp(`^${regexPattern}`); if (regex.test(path)) { return true; } } catch (e) { // If regex fails, continue } } } } // For include mode, return false if no pattern matches return false; };