import type { Binding, CanvasComponent, ProjectDocument, RestAction, Variable, VariableType, } from '../../types/project'; import { isTargetPropertySupported } from '../Preview/bindingUtils'; export type ResponseTargetOption = { value: string; label: string }; function nextId(prefix: string, used: Set): string { let index = 1; while (used.has(`${prefix}_${index}`)) index += 1; return `${prefix}_${index}`; } export function responseTargetOptions( components: CanvasComponent[], variables: Record, ): ResponseTargetOption[] { const options: ResponseTargetOption[] = []; const nameCounts = new Map(); for (const component of components) { nameCounts.set(component.name, (nameCounts.get(component.name) ?? 0) + 1); } for (const component of components) { // Runtime resolution requires an unambiguous component name. Diagnostics // still report existing bindings that point at duplicate names, but the // visual editor must not create a new ambiguous target. if (nameCounts.get(component.name) !== 1) continue; for (const property of ['value', 'options', 'rows']) { if (isTargetPropertySupported(component.type, property)) { options.push({ value: `components.${component.name}.${property}`, label: `${component.name} (${component.type}.${property})`, }); } } } for (const name of Object.keys(variables)) { options.push({ value: `variables.${name}`, label: `${name} (variable)` }); } return options; } export function createResponseBinding( bindings: Binding[], action: RestAction, target: string, ): Binding { return { id: nextId('binding_response', new Set(bindings.map((binding) => binding.id))), source: `actions.${action.id}.response.body`, target, trigger: 'onSuccess', }; } export function replaceBindingAt( doc: ProjectDocument, index: number, binding: Binding, ): ProjectDocument { const bindings = [...doc.project.bindings]; bindings[index] = binding; return { ...doc, project: { ...doc.project, bindings } }; } export function removeBindingAt(doc: ProjectDocument, index: number): ProjectDocument { return { ...doc, project: { ...doc.project, bindings: doc.project.bindings.filter((_binding, candidate) => candidate !== index), }, }; } export function validateVariableName( name: string, variables: Record, originalName?: string, ): string | null { if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) { return 'Use a letter or underscore first, followed by letters, numbers, underscores, or hyphens.'; } if (name !== originalName && Object.prototype.hasOwnProperty.call(variables, name)) { return `Variable "${name}" already exists.`; } return null; } export function parseVariableDefault(type: VariableType, raw: string): unknown { if (type === 'string') return raw; if (raw.trim() === '') return undefined; if (type === 'number') { const value = Number(raw); if (!Number.isFinite(value)) throw new Error('Enter a finite number.'); return value; } if (type === 'boolean') { if (raw === 'true') return true; if (raw === 'false') return false; throw new Error('Enter true or false.'); } let value: unknown; try { value = JSON.parse(raw); } catch (_error) { throw new Error('Enter valid JSON.'); } if (type === 'array' && !Array.isArray(value)) throw new Error('Enter a JSON array.'); if (type === 'object' && (value === null || Array.isArray(value) || typeof value !== 'object')) { throw new Error('Enter a JSON object.'); } return value; } export function formatVariableDefault(variable: Variable): string { if (variable.defaultValue === undefined) return ''; if (variable.type === 'string') return String(variable.defaultValue); if (variable.type === 'object' || variable.type === 'array') { return JSON.stringify(variable.defaultValue, null, 2); } return String(variable.defaultValue); } export function setVariable( doc: ProjectDocument, name: string, variable: Variable, originalName?: string, ): ProjectDocument { const variables = { ...doc.project.variables }; if (originalName && originalName !== name) delete variables[originalName]; variables[name] = variable; return { ...doc, project: { ...doc.project, variables } }; } export function removeVariable(doc: ProjectDocument, name: string): ProjectDocument { const variables = { ...doc.project.variables }; delete variables[name]; return { ...doc, project: { ...doc.project, variables } }; } export function findVariableReferences(doc: ProjectDocument, name: string): string[] { const needle = `variables.${name}`; const references: string[] = []; doc.project.bindings.forEach((binding) => { if (binding.source === needle || binding.target === needle) references.push(`binding "${binding.id}"`); }); doc.project.actions.forEach((action) => { const values = [ action.url, action.bodyTemplate, ...Object.values(action.headers ?? {}), ...Object.values(action.queryParameters ?? {}), // Variable path-parameter templates are compatibility-only and produce a // diagnostic, but they are still canonical references worth warning about. ...Object.values(action.pathParameters ?? {}), ]; if (values.some((value) => value?.includes(`{{${needle}}}`))) references.push(`action "${action.name}"`); }); return references; }