import { isQuery } from "../helpers.js" import { checkTextFormat, checkNumberFormat } from "../constants.js" import { MetadataError } from "./types.js" import type { FieldNode, RecordField, VariantField, TupleField, OptionalField, VectorField, BlobField, RecursiveField, PrincipalField, NumberField, BooleanField, NullField, TextField, UnknownField, ArgumentsMeta, ArgumentsServiceMeta, RenderHint, PrimitiveInputProps, BlobLimits, BlobValidationResult, TextFormat, NumberFormat, } from "./types.js" import { IDL } from "@icp-sdk/core/candid" import { Principal } from "@icp-sdk/core/principal" import { BaseActor, FunctionName } from "@ic-reactor/core" import * as z from "zod" import { formatLabel } from "./helpers.js" export * from "./types.js" export * from "./helpers.js" export { checkTextFormat, checkNumberFormat } from "../constants.js" // ════════════════════════════════════════════════════════════════════════════ // Render Hint Helpers // ════════════════════════════════════════════════════════════════════════════ const COMPOUND_RENDER_HINT: RenderHint = { isCompound: true, isPrimitive: false, } const TEXT_RENDER_HINT: RenderHint = { isCompound: false, isPrimitive: true, inputType: "text", } const NUMBER_RENDER_HINT: RenderHint = { isCompound: false, isPrimitive: true, inputType: "number", } const CHECKBOX_RENDER_HINT: RenderHint = { isCompound: false, isPrimitive: true, inputType: "checkbox", } const FILE_RENDER_HINT: RenderHint = { isCompound: false, isPrimitive: true, inputType: "file", } // ════════════════════════════════════════════════════════════════════════════ // Blob Field Helpers // ════════════════════════════════════════════════════════════════════════════ const DEFAULT_BLOB_LIMITS: BlobLimits = { maxHexBytes: 512, maxFileBytes: 2 * 1024 * 1024, // 2MB maxHexDisplayLength: 128, } function normalizeHex(input: string): string { // Remove 0x prefix and convert to lowercase let hex = input.toLowerCase() if (hex.startsWith("0x")) { hex = hex.slice(2) } // Remove any whitespace hex = hex.replace(/\s/g, "") return hex } function validateBlobInput( value: string | Uint8Array, limits: BlobLimits ): BlobValidationResult { if (value instanceof Uint8Array) { if (value.length > limits.maxFileBytes) { return { valid: false, error: `File size exceeds maximum of ${limits.maxFileBytes} bytes`, } } return { valid: true } } // String input (hex) const normalized = normalizeHex(value) if (normalized.length === 0) { return { valid: true } // Empty is valid } if (!/^[0-9a-f]*$/.test(normalized)) { return { valid: false, error: "Invalid hex characters" } } if (normalized.length % 2 !== 0) { return { valid: false, error: "Hex string must have even length" } } const byteLength = normalized.length / 2 if (byteLength > limits.maxHexBytes) { return { valid: false, error: `Hex input exceeds maximum of ${limits.maxHexBytes} bytes`, } } return { valid: true } } /** * FieldVisitor generates metadata for form input fields from Candid IDL types. * * ## Design Principles * * 1. **Works with raw IDL types** - generates metadata at initialization time * 2. **No value dependencies** - metadata is independent of actual values * 3. **Form-framework agnostic** - output can be used with TanStack, React Hook Form, etc. * 4. **Efficient** - single traversal, no runtime type checking * 5. **TanStack Form optimized** - name paths compatible with TanStack Form patterns * * ## Output Structure * * Each field has: * - `type`: The field type (record, variant, text, number, etc.) * - `label`: Raw label from Candid * - `displayLabel`: Human-readable formatted label * - `name`: TanStack Form compatible path (e.g., "[0]", "[0].owner", "tags[1]") * - `component`: Suggested component type for rendering * - `renderHint`: Hints for UI rendering strategy * - `defaultValue`: Initial value for the form * - `schema`: Zod schema for validation * - Type-specific properties (options for variant, fields for record, etc.) * - Helper methods for dynamic forms (getOptionDefault, getItemDefault, etc.) * * ## Usage with TanStack Form * * @example * ```typescript * import { useForm } from '@tanstack/react-form' * import { FieldVisitor } from '@ic-reactor/candid' * * const visitor = new FieldVisitor() * const serviceMeta = service.accept(visitor, null) * const methodMeta = serviceMeta["icrc1_transfer"] * * const form = useForm({ * defaultValues: methodMeta.defaultValue, * validators: { onBlur: methodMeta.schema }, * onSubmit: async ({ value }) => { * await actor.icrc1_transfer(...value) * } * }) * * // Render fields dynamically * methodMeta.fields.map((field, index) => ( *