@ic-reactor/candid 3.0.11-beta.2 → 3.0.13-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/display-reactor.d.ts +1 -2
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +1 -1
- package/dist/display-reactor.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +3 -3
- package/dist/metadata-display-reactor.d.ts.map +1 -1
- package/dist/metadata-display-reactor.js +2 -2
- package/dist/metadata-display-reactor.js.map +1 -1
- package/dist/visitor/arguments/helpers.d.ts +40 -0
- package/dist/visitor/arguments/helpers.d.ts.map +1 -0
- package/dist/visitor/arguments/helpers.js +81 -0
- package/dist/visitor/arguments/helpers.js.map +1 -0
- package/dist/visitor/arguments/index.d.ts +64 -42
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +472 -85
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +481 -46
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/helpers.d.ts +1 -1
- package/dist/visitor/helpers.d.ts.map +1 -1
- package/dist/visitor/returns/index.d.ts +1 -2
- package/dist/visitor/returns/index.d.ts.map +1 -1
- package/dist/visitor/returns/index.js +2 -3
- package/dist/visitor/returns/index.js.map +1 -1
- package/dist/visitor/types.d.ts +2 -3
- package/dist/visitor/types.d.ts.map +1 -1
- package/dist/visitor/types.js +1 -2
- package/dist/visitor/types.js.map +1 -1
- package/package.json +6 -3
- package/src/display-reactor.ts +4 -6
- package/src/index.ts +0 -1
- package/src/metadata-display-reactor.ts +8 -8
- package/src/visitor/arguments/helpers.ts +104 -0
- package/src/visitor/arguments/index.test.ts +544 -52
- package/src/visitor/arguments/index.ts +590 -151
- package/src/visitor/arguments/schema.test.ts +215 -0
- package/src/visitor/arguments/types.ts +554 -62
- package/src/visitor/helpers.ts +1 -1
- package/src/visitor/returns/index.test.ts +1 -1
- package/src/visitor/returns/index.ts +2 -3
- package/src/visitor/types.ts +2 -3
|
@@ -1,8 +1,86 @@
|
|
|
1
1
|
import { isQuery } from "../helpers";
|
|
2
|
-
import { IDL } from "
|
|
2
|
+
import { IDL } from "@icp-sdk/core/candid";
|
|
3
|
+
import { Principal } from "@icp-sdk/core/principal";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import { formatLabel } from "./helpers";
|
|
3
6
|
export * from "./types";
|
|
7
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// Render Hint Helpers
|
|
9
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
10
|
+
const COMPOUND_RENDER_HINT = {
|
|
11
|
+
isCompound: true,
|
|
12
|
+
isPrimitive: false,
|
|
13
|
+
};
|
|
14
|
+
const TEXT_RENDER_HINT = {
|
|
15
|
+
isCompound: false,
|
|
16
|
+
isPrimitive: true,
|
|
17
|
+
inputType: "text",
|
|
18
|
+
};
|
|
19
|
+
const NUMBER_RENDER_HINT = {
|
|
20
|
+
isCompound: false,
|
|
21
|
+
isPrimitive: true,
|
|
22
|
+
inputType: "number",
|
|
23
|
+
};
|
|
24
|
+
const CHECKBOX_RENDER_HINT = {
|
|
25
|
+
isCompound: false,
|
|
26
|
+
isPrimitive: true,
|
|
27
|
+
inputType: "checkbox",
|
|
28
|
+
};
|
|
29
|
+
const FILE_RENDER_HINT = {
|
|
30
|
+
isCompound: false,
|
|
31
|
+
isPrimitive: true,
|
|
32
|
+
inputType: "file",
|
|
33
|
+
};
|
|
34
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
// Blob Field Helpers
|
|
36
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
const DEFAULT_BLOB_LIMITS = {
|
|
38
|
+
maxHexBytes: 512,
|
|
39
|
+
maxFileBytes: 2 * 1024 * 1024, // 2MB
|
|
40
|
+
maxHexDisplayLength: 128,
|
|
41
|
+
};
|
|
42
|
+
function normalizeHex(input) {
|
|
43
|
+
// Remove 0x prefix and convert to lowercase
|
|
44
|
+
let hex = input.toLowerCase();
|
|
45
|
+
if (hex.startsWith("0x")) {
|
|
46
|
+
hex = hex.slice(2);
|
|
47
|
+
}
|
|
48
|
+
// Remove any whitespace
|
|
49
|
+
hex = hex.replace(/\s/g, "");
|
|
50
|
+
return hex;
|
|
51
|
+
}
|
|
52
|
+
function validateBlobInput(value, limits) {
|
|
53
|
+
if (value instanceof Uint8Array) {
|
|
54
|
+
if (value.length > limits.maxFileBytes) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
error: `File size exceeds maximum of ${limits.maxFileBytes} bytes`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { valid: true };
|
|
61
|
+
}
|
|
62
|
+
// String input (hex)
|
|
63
|
+
const normalized = normalizeHex(value);
|
|
64
|
+
if (normalized.length === 0) {
|
|
65
|
+
return { valid: true }; // Empty is valid
|
|
66
|
+
}
|
|
67
|
+
if (!/^[0-9a-f]*$/.test(normalized)) {
|
|
68
|
+
return { valid: false, error: "Invalid hex characters" };
|
|
69
|
+
}
|
|
70
|
+
if (normalized.length % 2 !== 0) {
|
|
71
|
+
return { valid: false, error: "Hex string must have even length" };
|
|
72
|
+
}
|
|
73
|
+
const byteLength = normalized.length / 2;
|
|
74
|
+
if (byteLength > limits.maxHexBytes) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: `Hex input exceeds maximum of ${limits.maxHexBytes} bytes`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { valid: true };
|
|
81
|
+
}
|
|
4
82
|
/**
|
|
5
|
-
*
|
|
83
|
+
* FieldVisitor generates metadata for form input fields from Candid IDL types.
|
|
6
84
|
*
|
|
7
85
|
* ## Design Principles
|
|
8
86
|
*
|
|
@@ -10,55 +88,84 @@ export * from "./types";
|
|
|
10
88
|
* 2. **No value dependencies** - metadata is independent of actual values
|
|
11
89
|
* 3. **Form-framework agnostic** - output can be used with TanStack, React Hook Form, etc.
|
|
12
90
|
* 4. **Efficient** - single traversal, no runtime type checking
|
|
91
|
+
* 5. **TanStack Form optimized** - name paths compatible with TanStack Form patterns
|
|
13
92
|
*
|
|
14
93
|
* ## Output Structure
|
|
15
94
|
*
|
|
16
95
|
* Each field has:
|
|
17
96
|
* - `type`: The field type (record, variant, text, number, etc.)
|
|
18
|
-
* - `label`:
|
|
19
|
-
* - `
|
|
97
|
+
* - `label`: Raw label from Candid
|
|
98
|
+
* - `displayLabel`: Human-readable formatted label
|
|
99
|
+
* - `name`: TanStack Form compatible path (e.g., "[0]", "[0].owner", "tags[1]")
|
|
100
|
+
* - `component`: Suggested component type for rendering
|
|
101
|
+
* - `renderHint`: Hints for UI rendering strategy
|
|
20
102
|
* - `defaultValue`: Initial value for the form
|
|
103
|
+
* - `schema`: Zod schema for validation
|
|
21
104
|
* - Type-specific properties (options for variant, fields for record, etc.)
|
|
105
|
+
* - Helper methods for dynamic forms (getOptionDefault, getItemDefault, etc.)
|
|
106
|
+
*
|
|
107
|
+
* ## Usage with TanStack Form
|
|
22
108
|
*
|
|
23
109
|
* @example
|
|
24
110
|
* ```typescript
|
|
25
|
-
*
|
|
26
|
-
*
|
|
111
|
+
* import { useForm } from '@tanstack/react-form'
|
|
112
|
+
* import { FieldVisitor } from '@ic-reactor/candid'
|
|
27
113
|
*
|
|
28
|
-
*
|
|
114
|
+
* const visitor = new FieldVisitor()
|
|
115
|
+
* const serviceMeta = service.accept(visitor, null)
|
|
29
116
|
* const methodMeta = serviceMeta["icrc1_transfer"]
|
|
30
|
-
*
|
|
31
|
-
*
|
|
117
|
+
*
|
|
118
|
+
* const form = useForm({
|
|
119
|
+
* defaultValues: methodMeta.defaultValue,
|
|
120
|
+
* validators: { onBlur: methodMeta.schema },
|
|
121
|
+
* onSubmit: async ({ value }) => {
|
|
122
|
+
* await actor.icrc1_transfer(...value)
|
|
123
|
+
* }
|
|
124
|
+
* })
|
|
125
|
+
*
|
|
126
|
+
* // Render fields dynamically
|
|
127
|
+
* methodMeta.fields.map((field, index) => (
|
|
128
|
+
* <form.Field key={index} name={field.name}>
|
|
129
|
+
* {(fieldApi) => <DynamicInput field={field} fieldApi={fieldApi} />}
|
|
130
|
+
* </form.Field>
|
|
131
|
+
* ))
|
|
32
132
|
* ```
|
|
33
133
|
*/
|
|
34
|
-
export class
|
|
134
|
+
export class FieldVisitor extends IDL.Visitor {
|
|
35
135
|
constructor() {
|
|
36
136
|
super(...arguments);
|
|
37
|
-
Object.defineProperty(this, "
|
|
137
|
+
Object.defineProperty(this, "recursiveSchemas", {
|
|
138
|
+
enumerable: true,
|
|
139
|
+
configurable: true,
|
|
140
|
+
writable: true,
|
|
141
|
+
value: new Map()
|
|
142
|
+
});
|
|
143
|
+
Object.defineProperty(this, "nameStack", {
|
|
38
144
|
enumerable: true,
|
|
39
145
|
configurable: true,
|
|
40
146
|
writable: true,
|
|
41
147
|
value: []
|
|
42
148
|
});
|
|
43
149
|
}
|
|
44
|
-
|
|
45
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Execute function with a name segment pushed onto the stack.
|
|
152
|
+
* Automatically manages stack cleanup.
|
|
153
|
+
*/
|
|
154
|
+
withName(name, fn) {
|
|
155
|
+
this.nameStack.push(name);
|
|
46
156
|
try {
|
|
47
157
|
return fn();
|
|
48
158
|
}
|
|
49
159
|
finally {
|
|
50
|
-
this.
|
|
160
|
+
this.nameStack.pop();
|
|
51
161
|
}
|
|
52
162
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return parent ? `${parent}[${key}]` : String(key);
|
|
60
|
-
}
|
|
61
|
-
return parent ? `${parent}.${key}` : key;
|
|
163
|
+
/**
|
|
164
|
+
* Get the current full name path for form binding.
|
|
165
|
+
* Returns empty string for root level.
|
|
166
|
+
*/
|
|
167
|
+
currentName() {
|
|
168
|
+
return this.nameStack.join("");
|
|
62
169
|
}
|
|
63
170
|
// ════════════════════════════════════════════════════════════════════════
|
|
64
171
|
// Service & Function Level
|
|
@@ -72,206 +179,486 @@ export class ArgumentFieldVisitor extends IDL.Visitor {
|
|
|
72
179
|
}
|
|
73
180
|
visitFunc(t, functionName) {
|
|
74
181
|
const functionType = isQuery(t) ? "query" : "update";
|
|
182
|
+
const argCount = t.argTypes.length;
|
|
75
183
|
const fields = t.argTypes.map((arg, index) => {
|
|
76
|
-
return this.
|
|
184
|
+
return this.withName(`[${index}]`, () => arg.accept(this, `__arg${index}`));
|
|
77
185
|
});
|
|
78
|
-
const defaultValues = fields.map((field) =>
|
|
186
|
+
const defaultValues = fields.map((field) => field.defaultValue);
|
|
187
|
+
// Handle empty args case for schema
|
|
188
|
+
// For no-arg functions, use an empty array schema
|
|
189
|
+
// For functions with args, use a proper tuple schema
|
|
190
|
+
const schema = argCount === 0
|
|
191
|
+
? z.tuple([])
|
|
192
|
+
: z.tuple(fields.map((field) => field.schema));
|
|
79
193
|
return {
|
|
80
194
|
functionType,
|
|
81
195
|
functionName,
|
|
82
196
|
fields,
|
|
83
|
-
defaultValues,
|
|
197
|
+
defaultValues: defaultValues,
|
|
198
|
+
schema,
|
|
199
|
+
argCount,
|
|
200
|
+
isNoArgs: argCount === 0,
|
|
84
201
|
};
|
|
85
202
|
}
|
|
86
|
-
extractDefaultValue(field) {
|
|
87
|
-
if ("defaultValue" in field) {
|
|
88
|
-
return field.defaultValue;
|
|
89
|
-
}
|
|
90
|
-
if ("defaultValues" in field) {
|
|
91
|
-
return field.defaultValues;
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
203
|
// ════════════════════════════════════════════════════════════════════════
|
|
96
204
|
// Compound Types
|
|
97
205
|
// ════════════════════════════════════════════════════════════════════════
|
|
98
206
|
visitRecord(_t, fields_, label) {
|
|
99
|
-
const
|
|
207
|
+
const name = this.currentName();
|
|
100
208
|
const fields = [];
|
|
101
|
-
const
|
|
209
|
+
const fieldMap = new Map();
|
|
210
|
+
const defaultValue = {};
|
|
211
|
+
const schemaShape = {};
|
|
102
212
|
for (const [key, type] of fields_) {
|
|
103
|
-
const field = this.
|
|
213
|
+
const field = this.withName(name ? `.${key}` : key, () => type.accept(this, key));
|
|
104
214
|
fields.push(field);
|
|
105
|
-
|
|
215
|
+
fieldMap.set(key, field);
|
|
216
|
+
defaultValue[key] = field.defaultValue;
|
|
217
|
+
schemaShape[key] = field.schema;
|
|
106
218
|
}
|
|
219
|
+
const schema = z.object(schemaShape);
|
|
107
220
|
return {
|
|
108
221
|
type: "record",
|
|
109
222
|
label,
|
|
110
|
-
|
|
223
|
+
displayLabel: formatLabel(label),
|
|
224
|
+
name,
|
|
225
|
+
component: "record-container",
|
|
226
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
111
227
|
fields,
|
|
112
|
-
|
|
228
|
+
fieldMap,
|
|
229
|
+
defaultValue,
|
|
230
|
+
schema,
|
|
231
|
+
candidType: "record",
|
|
113
232
|
};
|
|
114
233
|
}
|
|
115
234
|
visitVariant(_t, fields_, label) {
|
|
116
|
-
const
|
|
235
|
+
const name = this.currentName();
|
|
117
236
|
const fields = [];
|
|
118
237
|
const options = [];
|
|
238
|
+
const optionMap = new Map();
|
|
239
|
+
const variantSchemas = [];
|
|
119
240
|
for (const [key, type] of fields_) {
|
|
120
|
-
const field = this.
|
|
241
|
+
const field = this.withName(`.${key}`, () => type.accept(this, key));
|
|
121
242
|
fields.push(field);
|
|
122
243
|
options.push(key);
|
|
244
|
+
optionMap.set(key, field);
|
|
245
|
+
variantSchemas.push(z.object({ [key]: field.schema }));
|
|
123
246
|
}
|
|
124
247
|
const defaultOption = options[0];
|
|
125
|
-
const
|
|
126
|
-
|
|
248
|
+
const firstField = fields[0];
|
|
249
|
+
const defaultValue = {
|
|
250
|
+
[defaultOption]: firstField.defaultValue,
|
|
251
|
+
};
|
|
252
|
+
const schema = z.union(variantSchemas);
|
|
253
|
+
// Helper to get default value for any option
|
|
254
|
+
const getOptionDefault = (option) => {
|
|
255
|
+
const optField = optionMap.get(option);
|
|
256
|
+
if (!optField) {
|
|
257
|
+
throw new Error(`Unknown variant option: ${option}`);
|
|
258
|
+
}
|
|
259
|
+
return { [option]: optField.defaultValue };
|
|
260
|
+
};
|
|
261
|
+
// Helper to get field for a specific option
|
|
262
|
+
const getField = (option) => {
|
|
263
|
+
const optField = optionMap.get(option);
|
|
264
|
+
if (!optField) {
|
|
265
|
+
throw new Error(`Unknown variant option: ${option}`);
|
|
266
|
+
}
|
|
267
|
+
return optField;
|
|
268
|
+
};
|
|
269
|
+
// Helper to get currently selected option from a value
|
|
270
|
+
const getSelectedOption = (value) => {
|
|
271
|
+
const validKeys = Object.keys(value).filter((k) => options.includes(k));
|
|
272
|
+
return validKeys[0] ?? defaultOption;
|
|
273
|
+
};
|
|
274
|
+
// Helper to get selected field from a value
|
|
275
|
+
const getSelectedField = (value) => {
|
|
276
|
+
const selectedOption = getSelectedOption(value);
|
|
277
|
+
return getField(selectedOption);
|
|
127
278
|
};
|
|
128
279
|
return {
|
|
129
280
|
type: "variant",
|
|
130
281
|
label,
|
|
131
|
-
|
|
282
|
+
displayLabel: formatLabel(label),
|
|
283
|
+
name,
|
|
284
|
+
component: "variant-select",
|
|
285
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
132
286
|
fields,
|
|
133
287
|
options,
|
|
134
288
|
defaultOption,
|
|
135
|
-
|
|
289
|
+
optionMap,
|
|
290
|
+
defaultValue,
|
|
291
|
+
schema,
|
|
292
|
+
getOptionDefault,
|
|
293
|
+
getField,
|
|
294
|
+
getSelectedOption,
|
|
295
|
+
getSelectedField,
|
|
296
|
+
candidType: "variant",
|
|
136
297
|
};
|
|
137
298
|
}
|
|
138
299
|
visitTuple(_t, components, label) {
|
|
139
|
-
const
|
|
300
|
+
const name = this.currentName();
|
|
140
301
|
const fields = [];
|
|
141
|
-
const
|
|
302
|
+
const defaultValue = [];
|
|
303
|
+
const schemas = [];
|
|
142
304
|
for (let index = 0; index < components.length; index++) {
|
|
143
305
|
const type = components[index];
|
|
144
|
-
const field = this.
|
|
306
|
+
const field = this.withName(`[${index}]`, () => type.accept(this, `_${index}_`));
|
|
145
307
|
fields.push(field);
|
|
146
|
-
|
|
308
|
+
defaultValue.push(field.defaultValue);
|
|
309
|
+
schemas.push(field.schema);
|
|
147
310
|
}
|
|
311
|
+
const schema = z.tuple(schemas);
|
|
148
312
|
return {
|
|
149
313
|
type: "tuple",
|
|
150
314
|
label,
|
|
151
|
-
|
|
315
|
+
displayLabel: formatLabel(label),
|
|
316
|
+
name,
|
|
317
|
+
component: "tuple-container",
|
|
318
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
152
319
|
fields,
|
|
153
|
-
|
|
320
|
+
defaultValue,
|
|
321
|
+
schema,
|
|
322
|
+
candidType: "tuple",
|
|
154
323
|
};
|
|
155
324
|
}
|
|
156
325
|
visitOpt(_t, ty, label) {
|
|
157
|
-
const
|
|
158
|
-
|
|
326
|
+
const name = this.currentName();
|
|
327
|
+
// For optional, the inner field keeps the same name path
|
|
328
|
+
// because the value replaces null directly (not nested)
|
|
329
|
+
const innerField = ty.accept(this, label);
|
|
330
|
+
const schema = z.union([
|
|
331
|
+
innerField.schema,
|
|
332
|
+
z.null(),
|
|
333
|
+
z.undefined().transform(() => null),
|
|
334
|
+
z.literal("").transform(() => null),
|
|
335
|
+
]);
|
|
336
|
+
// Helper to get the inner default when enabling the optional
|
|
337
|
+
const getInnerDefault = () => innerField.defaultValue;
|
|
338
|
+
// Helper to check if a value represents an enabled optional
|
|
339
|
+
const isEnabled = (value) => {
|
|
340
|
+
return value !== null && typeof value !== "undefined";
|
|
341
|
+
};
|
|
159
342
|
return {
|
|
160
343
|
type: "optional",
|
|
161
344
|
label,
|
|
162
|
-
|
|
345
|
+
displayLabel: formatLabel(label),
|
|
346
|
+
name,
|
|
347
|
+
component: "optional-toggle",
|
|
348
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
163
349
|
innerField,
|
|
164
350
|
defaultValue: null,
|
|
351
|
+
schema,
|
|
352
|
+
getInnerDefault,
|
|
353
|
+
isEnabled,
|
|
354
|
+
candidType: "opt",
|
|
165
355
|
};
|
|
166
356
|
}
|
|
167
357
|
visitVec(_t, ty, label) {
|
|
168
|
-
const
|
|
358
|
+
const name = this.currentName();
|
|
169
359
|
// Check if it's blob (vec nat8)
|
|
170
360
|
const isBlob = ty instanceof IDL.FixedNatClass && ty._bits === 8;
|
|
171
|
-
|
|
361
|
+
// Item field uses [0] as template path
|
|
362
|
+
const itemField = this.withName("[0]", () => ty.accept(this, `${label}_item`));
|
|
172
363
|
if (isBlob) {
|
|
364
|
+
const schema = z.union([
|
|
365
|
+
z.string(),
|
|
366
|
+
z.array(z.number()),
|
|
367
|
+
z.instanceof(Uint8Array),
|
|
368
|
+
]);
|
|
369
|
+
const limits = { ...DEFAULT_BLOB_LIMITS };
|
|
173
370
|
return {
|
|
174
371
|
type: "blob",
|
|
175
372
|
label,
|
|
176
|
-
|
|
373
|
+
displayLabel: formatLabel(label),
|
|
374
|
+
name,
|
|
375
|
+
component: "blob-upload",
|
|
376
|
+
renderHint: FILE_RENDER_HINT,
|
|
177
377
|
itemField,
|
|
178
378
|
defaultValue: "",
|
|
379
|
+
schema,
|
|
380
|
+
acceptedFormats: ["hex", "base64", "file"],
|
|
381
|
+
limits,
|
|
382
|
+
normalizeHex,
|
|
383
|
+
validateInput: (value) => validateBlobInput(value, limits),
|
|
384
|
+
candidType: "blob",
|
|
179
385
|
};
|
|
180
386
|
}
|
|
387
|
+
const schema = z.array(itemField.schema);
|
|
388
|
+
// Helper to get a new item with default values
|
|
389
|
+
const getItemDefault = () => itemField.defaultValue;
|
|
390
|
+
// Helper to create an item field for a specific index
|
|
391
|
+
const createItemField = (index, overrides) => {
|
|
392
|
+
// Replace [0] in template with actual index
|
|
393
|
+
const itemName = name ? `${name}[${index}]` : `[${index}]`;
|
|
394
|
+
const itemLabel = overrides?.label ?? `Item ${index}`;
|
|
395
|
+
return {
|
|
396
|
+
...itemField,
|
|
397
|
+
name: itemName,
|
|
398
|
+
label: itemLabel,
|
|
399
|
+
displayLabel: formatLabel(itemLabel),
|
|
400
|
+
};
|
|
401
|
+
};
|
|
181
402
|
return {
|
|
182
403
|
type: "vector",
|
|
183
404
|
label,
|
|
184
|
-
|
|
405
|
+
displayLabel: formatLabel(label),
|
|
406
|
+
name,
|
|
407
|
+
component: "vector-list",
|
|
408
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
185
409
|
itemField,
|
|
186
410
|
defaultValue: [],
|
|
411
|
+
schema,
|
|
412
|
+
getItemDefault,
|
|
413
|
+
createItemField,
|
|
414
|
+
candidType: "vec",
|
|
187
415
|
};
|
|
188
416
|
}
|
|
189
417
|
visitRec(_t, ty, label) {
|
|
190
|
-
const
|
|
418
|
+
const name = this.currentName();
|
|
419
|
+
const typeName = ty.name || "RecursiveType";
|
|
420
|
+
let schema;
|
|
421
|
+
if (this.recursiveSchemas.has(typeName)) {
|
|
422
|
+
schema = this.recursiveSchemas.get(typeName);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
schema = z.lazy(() => ty.accept(this, label).schema);
|
|
426
|
+
this.recursiveSchemas.set(typeName, schema);
|
|
427
|
+
}
|
|
428
|
+
// Lazy extraction to prevent infinite loops
|
|
429
|
+
const extract = () => this.withName(name, () => ty.accept(this, label));
|
|
430
|
+
// Helper to get inner default (evaluates lazily)
|
|
431
|
+
const getInnerDefault = () => extract().defaultValue;
|
|
191
432
|
return {
|
|
192
433
|
type: "recursive",
|
|
193
434
|
label,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
435
|
+
displayLabel: formatLabel(label),
|
|
436
|
+
name,
|
|
437
|
+
component: "recursive-lazy",
|
|
438
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
439
|
+
typeName,
|
|
440
|
+
extract,
|
|
441
|
+
defaultValue: undefined,
|
|
442
|
+
schema,
|
|
443
|
+
getInnerDefault,
|
|
444
|
+
candidType: "rec",
|
|
197
445
|
};
|
|
198
446
|
}
|
|
199
447
|
// ════════════════════════════════════════════════════════════════════════
|
|
200
448
|
// Primitive Types
|
|
201
449
|
// ════════════════════════════════════════════════════════════════════════
|
|
202
450
|
visitPrincipal(_t, label) {
|
|
451
|
+
const schema = z.custom((val) => {
|
|
452
|
+
if (val instanceof Principal)
|
|
453
|
+
return true;
|
|
454
|
+
if (typeof val === "string") {
|
|
455
|
+
try {
|
|
456
|
+
Principal.fromText(val);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}, {
|
|
465
|
+
message: "Invalid Principal format",
|
|
466
|
+
});
|
|
467
|
+
const inputProps = {
|
|
468
|
+
type: "text",
|
|
469
|
+
placeholder: "aaaaa-aa or full principal ID",
|
|
470
|
+
minLength: 7,
|
|
471
|
+
maxLength: 64,
|
|
472
|
+
spellCheck: false,
|
|
473
|
+
autoComplete: "off",
|
|
474
|
+
};
|
|
203
475
|
return {
|
|
204
476
|
type: "principal",
|
|
205
477
|
label,
|
|
206
|
-
|
|
478
|
+
displayLabel: formatLabel(label),
|
|
479
|
+
name: this.currentName(),
|
|
480
|
+
component: "principal-input",
|
|
481
|
+
renderHint: TEXT_RENDER_HINT,
|
|
207
482
|
defaultValue: "",
|
|
208
483
|
maxLength: 64,
|
|
209
484
|
minLength: 7,
|
|
485
|
+
schema,
|
|
486
|
+
inputProps,
|
|
487
|
+
candidType: "principal",
|
|
210
488
|
};
|
|
211
489
|
}
|
|
212
490
|
visitText(_t, label) {
|
|
491
|
+
const inputProps = {
|
|
492
|
+
type: "text",
|
|
493
|
+
placeholder: "Enter text...",
|
|
494
|
+
spellCheck: true,
|
|
495
|
+
};
|
|
213
496
|
return {
|
|
214
497
|
type: "text",
|
|
215
498
|
label,
|
|
216
|
-
|
|
499
|
+
displayLabel: formatLabel(label),
|
|
500
|
+
name: this.currentName(),
|
|
501
|
+
component: "text-input",
|
|
502
|
+
renderHint: TEXT_RENDER_HINT,
|
|
217
503
|
defaultValue: "",
|
|
504
|
+
schema: z.string().min(1, "Required"),
|
|
505
|
+
inputProps,
|
|
506
|
+
candidType: "text",
|
|
218
507
|
};
|
|
219
508
|
}
|
|
220
509
|
visitBool(_t, label) {
|
|
510
|
+
const inputProps = {
|
|
511
|
+
type: "checkbox",
|
|
512
|
+
};
|
|
221
513
|
return {
|
|
222
514
|
type: "boolean",
|
|
223
515
|
label,
|
|
224
|
-
|
|
516
|
+
displayLabel: formatLabel(label),
|
|
517
|
+
name: this.currentName(),
|
|
518
|
+
component: "boolean-checkbox",
|
|
519
|
+
renderHint: CHECKBOX_RENDER_HINT,
|
|
225
520
|
defaultValue: false,
|
|
521
|
+
schema: z.boolean(),
|
|
522
|
+
inputProps,
|
|
523
|
+
candidType: "bool",
|
|
226
524
|
};
|
|
227
525
|
}
|
|
228
526
|
visitNull(_t, label) {
|
|
229
527
|
return {
|
|
230
528
|
type: "null",
|
|
231
529
|
label,
|
|
232
|
-
|
|
530
|
+
displayLabel: formatLabel(label),
|
|
531
|
+
name: this.currentName(),
|
|
532
|
+
component: "null-hidden",
|
|
533
|
+
renderHint: {
|
|
534
|
+
isCompound: false,
|
|
535
|
+
isPrimitive: true,
|
|
536
|
+
},
|
|
233
537
|
defaultValue: null,
|
|
538
|
+
schema: z.null(),
|
|
539
|
+
candidType: "null",
|
|
234
540
|
};
|
|
235
541
|
}
|
|
236
|
-
//
|
|
237
|
-
|
|
542
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
543
|
+
// Number Types with Constraints
|
|
544
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
545
|
+
visitNumberType(label, candidType, options) {
|
|
546
|
+
let schema = z.string().min(1, "Required");
|
|
547
|
+
if (options.isFloat) {
|
|
548
|
+
schema = schema.refine((val) => !isNaN(Number(val)), "Must be a number");
|
|
549
|
+
}
|
|
550
|
+
else if (options.unsigned) {
|
|
551
|
+
schema = schema.regex(/^\d+$/, "Must be a positive number");
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
schema = schema.regex(/^-?\d+$/, "Must be a number");
|
|
555
|
+
}
|
|
556
|
+
// Use "text" type for large numbers (BigInt) to ensure precision and better UI handling
|
|
557
|
+
// Standard number input has issues with large integers
|
|
558
|
+
const isBigInt = !options.isFloat && (!options.bits || options.bits > 32);
|
|
559
|
+
const type = isBigInt ? "text" : "number";
|
|
560
|
+
if (type === "text") {
|
|
561
|
+
const inputProps = {
|
|
562
|
+
type: "text",
|
|
563
|
+
placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
|
|
564
|
+
inputMode: "numeric",
|
|
565
|
+
pattern: options.unsigned ? "\\d+" : "-?\\d+",
|
|
566
|
+
spellCheck: false,
|
|
567
|
+
autoComplete: "off",
|
|
568
|
+
};
|
|
569
|
+
return {
|
|
570
|
+
type: "text",
|
|
571
|
+
label,
|
|
572
|
+
displayLabel: formatLabel(label),
|
|
573
|
+
name: this.currentName(),
|
|
574
|
+
component: "text-input",
|
|
575
|
+
renderHint: TEXT_RENDER_HINT,
|
|
576
|
+
defaultValue: "",
|
|
577
|
+
candidType,
|
|
578
|
+
schema,
|
|
579
|
+
inputProps,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const inputProps = {
|
|
583
|
+
type: "number",
|
|
584
|
+
placeholder: options.isFloat ? "0.0" : "0",
|
|
585
|
+
inputMode: options.isFloat ? "decimal" : "numeric",
|
|
586
|
+
min: options.min,
|
|
587
|
+
max: options.max,
|
|
588
|
+
step: options.isFloat ? "any" : "1",
|
|
589
|
+
};
|
|
238
590
|
return {
|
|
239
591
|
type: "number",
|
|
240
592
|
label,
|
|
241
|
-
|
|
593
|
+
displayLabel: formatLabel(label),
|
|
594
|
+
name: this.currentName(),
|
|
595
|
+
component: "number-input",
|
|
596
|
+
renderHint: NUMBER_RENDER_HINT,
|
|
242
597
|
defaultValue: "",
|
|
243
598
|
candidType,
|
|
599
|
+
schema: schema,
|
|
600
|
+
inputProps,
|
|
601
|
+
...options,
|
|
244
602
|
};
|
|
245
603
|
}
|
|
246
604
|
visitInt(_t, label) {
|
|
247
|
-
return this.visitNumberType(label, "int"
|
|
605
|
+
return this.visitNumberType(label, "int", {
|
|
606
|
+
unsigned: false,
|
|
607
|
+
isFloat: false,
|
|
608
|
+
});
|
|
248
609
|
}
|
|
249
610
|
visitNat(_t, label) {
|
|
250
|
-
return this.visitNumberType(label, "nat"
|
|
611
|
+
return this.visitNumberType(label, "nat", {
|
|
612
|
+
unsigned: true,
|
|
613
|
+
isFloat: false,
|
|
614
|
+
});
|
|
251
615
|
}
|
|
252
|
-
visitFloat(
|
|
253
|
-
return this.visitNumberType(label,
|
|
616
|
+
visitFloat(t, label) {
|
|
617
|
+
return this.visitNumberType(label, `float${t._bits}`, {
|
|
618
|
+
unsigned: false,
|
|
619
|
+
isFloat: true,
|
|
620
|
+
bits: t._bits,
|
|
621
|
+
});
|
|
254
622
|
}
|
|
255
623
|
visitFixedInt(t, label) {
|
|
256
|
-
|
|
624
|
+
const bits = t._bits;
|
|
625
|
+
// Calculate min/max for signed integers
|
|
626
|
+
const max = (BigInt(2) ** BigInt(bits - 1) - BigInt(1)).toString();
|
|
627
|
+
const min = (-(BigInt(2) ** BigInt(bits - 1))).toString();
|
|
628
|
+
return this.visitNumberType(label, `int${bits}`, {
|
|
629
|
+
unsigned: false,
|
|
630
|
+
isFloat: false,
|
|
631
|
+
bits,
|
|
632
|
+
min,
|
|
633
|
+
max,
|
|
634
|
+
});
|
|
257
635
|
}
|
|
258
636
|
visitFixedNat(t, label) {
|
|
259
|
-
|
|
637
|
+
const bits = t._bits;
|
|
638
|
+
// Calculate max for unsigned integers
|
|
639
|
+
const max = (BigInt(2) ** BigInt(bits) - BigInt(1)).toString();
|
|
640
|
+
return this.visitNumberType(label, `nat${bits}`, {
|
|
641
|
+
unsigned: true,
|
|
642
|
+
isFloat: false,
|
|
643
|
+
bits,
|
|
644
|
+
min: "0",
|
|
645
|
+
max,
|
|
646
|
+
});
|
|
260
647
|
}
|
|
261
648
|
visitType(_t, label) {
|
|
262
649
|
return {
|
|
263
650
|
type: "unknown",
|
|
264
651
|
label,
|
|
265
|
-
|
|
652
|
+
displayLabel: formatLabel(label),
|
|
653
|
+
name: this.currentName(),
|
|
654
|
+
component: "unknown-fallback",
|
|
655
|
+
renderHint: {
|
|
656
|
+
isCompound: false,
|
|
657
|
+
isPrimitive: false,
|
|
658
|
+
},
|
|
266
659
|
defaultValue: undefined,
|
|
660
|
+
schema: z.any(),
|
|
267
661
|
};
|
|
268
662
|
}
|
|
269
663
|
}
|
|
270
|
-
// ════════════════════════════════════════════════════════════════════════════
|
|
271
|
-
// Legacy Exports (for backward compatibility)
|
|
272
|
-
// ════════════════════════════════════════════════════════════════════════════
|
|
273
|
-
/**
|
|
274
|
-
* @deprecated Use ArgumentFieldVisitor instead
|
|
275
|
-
*/
|
|
276
|
-
export { ArgumentFieldVisitor as VisitTanstackField };
|
|
277
664
|
//# sourceMappingURL=index.js.map
|