UNPKG

lambda-live-debugger

Version:

Debug Lambda functions locally like it is running in the cloud

232 lines (205 loc) 6.77 kB
/** * Expression - Parses and stores a tag pattern expression * * Patterns are parsed once and stored in an optimized structure for fast matching. * * @example * const expr = new Expression("root.users.user"); * const expr2 = new Expression("..user[id]:first"); * const expr3 = new Expression("root/users/user", { separator: '/' }); */ export default class Expression { /** * Create a new Expression * @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]") * @param {Object} options - Configuration options * @param {string} options.separator - Path separator (default: '.') */ constructor(pattern, options = {}) { this.pattern = pattern; this.separator = options.separator || '.'; this.segments = this._parse(pattern); // Cache expensive checks for performance (O(1) instead of O(n)) this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard'); this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined); this._hasPositionSelector = this.segments.some(seg => seg.position !== undefined); } /** * Parse pattern string into segments * @private * @param {string} pattern - Pattern to parse * @returns {Array} Array of segment objects */ _parse(pattern) { const segments = []; // Split by separator but handle ".." specially let i = 0; let currentPart = ''; while (i < pattern.length) { if (pattern[i] === this.separator) { // Check if next char is also separator (deep wildcard) if (i + 1 < pattern.length && pattern[i + 1] === this.separator) { // Flush current part if any if (currentPart.trim()) { segments.push(this._parseSegment(currentPart.trim())); currentPart = ''; } // Add deep wildcard segments.push({ type: 'deep-wildcard' }); i += 2; // Skip both separators } else { // Regular separator if (currentPart.trim()) { segments.push(this._parseSegment(currentPart.trim())); } currentPart = ''; i++; } } else { currentPart += pattern[i]; i++; } } // Flush remaining part if (currentPart.trim()) { segments.push(this._parseSegment(currentPart.trim())); } return segments; } /** * Parse a single segment * @private * @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first") * @returns {Object} Segment object */ _parseSegment(part) { const segment = { type: 'tag' }; // NEW NAMESPACE SYNTAX (v2.0): // ============================ // Namespace uses DOUBLE colon (::) // Position uses SINGLE colon (:) // // Examples: // "user" → tag // "user:first" → tag + position // "user[id]" → tag + attribute // "user[id]:first" → tag + attribute + position // "ns::user" → namespace + tag // "ns::user:first" → namespace + tag + position // "ns::user[id]" → namespace + tag + attribute // "ns::user[id]:first" → namespace + tag + attribute + position // "ns::first" → namespace + tag named "first" (NO ambiguity!) // // This eliminates all ambiguity: // :: = namespace separator // : = position selector // [] = attributes // Step 1: Extract brackets [attr] or [attr=value] let bracketContent = null; let withoutBrackets = part; const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/); if (bracketMatch) { withoutBrackets = bracketMatch[1] + bracketMatch[3]; if (bracketMatch[2]) { const content = bracketMatch[2].slice(1, -1); if (content) { bracketContent = content; } } } // Step 2: Check for namespace (double colon ::) let namespace = undefined; let tagAndPosition = withoutBrackets; if (withoutBrackets.includes('::')) { const nsIndex = withoutBrackets.indexOf('::'); namespace = withoutBrackets.substring(0, nsIndex).trim(); tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip :: if (!namespace) { throw new Error(`Invalid namespace in pattern: ${part}`); } } // Step 3: Parse tag and position (single colon :) let tag = undefined; let positionMatch = null; if (tagAndPosition.includes(':')) { const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position const tagPart = tagAndPosition.substring(0, colonIndex).trim(); const posPart = tagAndPosition.substring(colonIndex + 1).trim(); // Verify position is a valid keyword const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) || /^nth\(\d+\)$/.test(posPart); if (isPositionKeyword) { tag = tagPart; positionMatch = posPart; } else { // Not a valid position keyword, treat whole thing as tag tag = tagAndPosition; } } else { tag = tagAndPosition; } if (!tag) { throw new Error(`Invalid segment pattern: ${part}`); } segment.tag = tag; if (namespace) { segment.namespace = namespace; } // Step 4: Parse attributes if (bracketContent) { if (bracketContent.includes('=')) { const eqIndex = bracketContent.indexOf('='); segment.attrName = bracketContent.substring(0, eqIndex).trim(); segment.attrValue = bracketContent.substring(eqIndex + 1).trim(); } else { segment.attrName = bracketContent.trim(); } } // Step 5: Parse position selector if (positionMatch) { const nthMatch = positionMatch.match(/^nth\((\d+)\)$/); if (nthMatch) { segment.position = 'nth'; segment.positionValue = parseInt(nthMatch[1], 10); } else { segment.position = positionMatch; } } return segment; } /** * Get the number of segments * @returns {number} */ get length() { return this.segments.length; } /** * Check if expression contains deep wildcard * @returns {boolean} */ hasDeepWildcard() { return this._hasDeepWildcard; } /** * Check if expression has attribute conditions * @returns {boolean} */ hasAttributeCondition() { return this._hasAttributeCondition; } /** * Check if expression has position selectors * @returns {boolean} */ hasPositionSelector() { return this._hasPositionSelector; } /** * Get string representation * @returns {string} */ toString() { return this.pattern; } }