import type { VariableReference } from './types'; /** * Regular expression to match variable references * Supports two syntaxes: * 1. ${type:path.to.value} - Explicit braces for concatenation (e.g., "${self:disk}G") * 2. $type:path.to.value - Simple syntax for standalone variables (e.g., "vmid = $self:vmid") * * Examples: * - $self:container_ip * - ${self:disk}G * - $self:domains[0] ← array indexing on $self: refs * - $capability:dns_registrar.primary_domain * - ${system:base_url}/api * * Path must start with letter or underscore, not digit. An optional * [N] suffix indexes into an array variable (currently only * meaningful on $self: refs whose target is `type: array` — e.g. * namecheap's `domains` declared as an array; the manifest's * capability data block can then read `$self:domains[0]` as a * computed alias for the canonical default). */ const VARIABLE_PATTERN = /\$\{(self|system|system_secret|secret|capability|infra):([a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)\}|\$(self|system|system_secret|secret|capability|infra):([a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)/g; /** * Parse template content to extract variable references * * Policy function (Rule 10.1) - parses only, no side effects * * @param content - Template content * @returns Array of variable references found in template */ export function parseVariables(content: string): VariableReference[] { const variables: VariableReference[] = []; const matches = content.matchAll(VARIABLE_PATTERN); for (const match of matches) { const [raw, bracedType, bracedPath, simpleType, simplePath] = match; // Check which syntax matched (braced vs simple) const type = bracedType || simpleType; const path = bracedPath || simplePath; if (type && path) { variables.push({ type: type as VariableReference['type'], path, raw, }); } } return variables; } /** * Check if a string contains any variable references * * @param content - String to check * @returns True if content contains variables */ export function hasVariables(content: string): boolean { // Create new regex without state to avoid issues with global flag // Matches both ${type:path} and $type:path (with optional [N] index) const pattern = /\$\{?(self|system|system_secret|secret|capability|infra):([a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)/; return pattern.test(content); } /** * Validate variable reference format * * @param variable - Variable string to validate * @returns True if format is valid */ export function isValidVariableFormat(variable: string): boolean { // Path must start with letter or underscore, not digit // Accepts both ${type:path} and $type:path with optional [N] indexing const pattern = /^(?:\$\{(self|system|system_secret|secret|capability|infra):[a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?\}|\$(self|system|system_secret|secret|capability|infra):[a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)$/; return pattern.test(variable); } /** * Split a variable path into its base name and optional array index. * * Examples: * parsePath("domains") → { name: "domains" } * parsePath("domains[0]") → { name: "domains", index: 0 } * parsePath("a.b.c[2]") → { name: "a.b.c", index: 2 } * * Pure helper used wherever `$self:NAME[N]` semantics need to be * applied to a resolved value (currently the capability-data * resolver in `context.ts` and the lazy-resolution path in * `resolver.ts`). */ export function parsePath(path: string): { name: string; index?: number } { const match = /^(.+)\[(\d+)\]$/.exec(path); if (!match) return { name: path }; return { name: match[1], index: Number.parseInt(match[2], 10) }; } /** * Apply an optional `[N]` index to a value. Used by the variable * resolver after looking up a name like `domains` in a config map — * if the original path had `[N]`, we drill into the array. * * Returns `undefined` for out-of-bounds indexes or when an index is * applied to a non-array. Callers turn that into a resolver error. */ export function applyIndex(value: unknown, index: number | undefined): unknown { if (index === undefined) return value; if (!Array.isArray(value)) return undefined; if (index < 0 || index >= value.length) return undefined; return value[index]; }