@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,29 +1,130 @@
|
|
|
1
1
|
import { isQuery } from "../helpers"
|
|
2
|
-
import { IDL } from "../types"
|
|
3
2
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
Field,
|
|
4
|
+
RecordField,
|
|
5
|
+
VariantField,
|
|
6
|
+
TupleField,
|
|
7
|
+
OptionalField,
|
|
8
|
+
VectorField,
|
|
9
|
+
BlobField,
|
|
10
|
+
RecursiveField,
|
|
11
|
+
PrincipalField,
|
|
12
|
+
NumberField,
|
|
13
|
+
BooleanField,
|
|
14
|
+
NullField,
|
|
15
|
+
TextField,
|
|
16
|
+
UnknownField,
|
|
17
|
+
ArgumentsMeta,
|
|
18
|
+
ArgumentsServiceMeta,
|
|
19
|
+
RenderHint,
|
|
20
|
+
PrimitiveInputProps,
|
|
21
|
+
BlobLimits,
|
|
22
|
+
BlobValidationResult,
|
|
20
23
|
} from "./types"
|
|
24
|
+
|
|
25
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
26
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
21
27
|
import { BaseActor, FunctionName } from "@ic-reactor/core"
|
|
28
|
+
import * as z from "zod"
|
|
29
|
+
import { formatLabel } from "./helpers"
|
|
22
30
|
|
|
23
31
|
export * from "./types"
|
|
24
32
|
|
|
33
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// Render Hint Helpers
|
|
35
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
const COMPOUND_RENDER_HINT: RenderHint = {
|
|
38
|
+
isCompound: true,
|
|
39
|
+
isPrimitive: false,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TEXT_RENDER_HINT: RenderHint = {
|
|
43
|
+
isCompound: false,
|
|
44
|
+
isPrimitive: true,
|
|
45
|
+
inputType: "text",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const NUMBER_RENDER_HINT: RenderHint = {
|
|
49
|
+
isCompound: false,
|
|
50
|
+
isPrimitive: true,
|
|
51
|
+
inputType: "number",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CHECKBOX_RENDER_HINT: RenderHint = {
|
|
55
|
+
isCompound: false,
|
|
56
|
+
isPrimitive: true,
|
|
57
|
+
inputType: "checkbox",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FILE_RENDER_HINT: RenderHint = {
|
|
61
|
+
isCompound: false,
|
|
62
|
+
isPrimitive: true,
|
|
63
|
+
inputType: "file",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// Blob Field Helpers
|
|
68
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
const DEFAULT_BLOB_LIMITS: BlobLimits = {
|
|
71
|
+
maxHexBytes: 512,
|
|
72
|
+
maxFileBytes: 2 * 1024 * 1024, // 2MB
|
|
73
|
+
maxHexDisplayLength: 128,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeHex(input: string): string {
|
|
77
|
+
// Remove 0x prefix and convert to lowercase
|
|
78
|
+
let hex = input.toLowerCase()
|
|
79
|
+
if (hex.startsWith("0x")) {
|
|
80
|
+
hex = hex.slice(2)
|
|
81
|
+
}
|
|
82
|
+
// Remove any whitespace
|
|
83
|
+
hex = hex.replace(/\s/g, "")
|
|
84
|
+
return hex
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateBlobInput(
|
|
88
|
+
value: string | Uint8Array,
|
|
89
|
+
limits: BlobLimits
|
|
90
|
+
): BlobValidationResult {
|
|
91
|
+
if (value instanceof Uint8Array) {
|
|
92
|
+
if (value.length > limits.maxFileBytes) {
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
error: `File size exceeds maximum of ${limits.maxFileBytes} bytes`,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { valid: true }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// String input (hex)
|
|
102
|
+
const normalized = normalizeHex(value)
|
|
103
|
+
if (normalized.length === 0) {
|
|
104
|
+
return { valid: true } // Empty is valid
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!/^[0-9a-f]*$/.test(normalized)) {
|
|
108
|
+
return { valid: false, error: "Invalid hex characters" }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (normalized.length % 2 !== 0) {
|
|
112
|
+
return { valid: false, error: "Hex string must have even length" }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const byteLength = normalized.length / 2
|
|
116
|
+
if (byteLength > limits.maxHexBytes) {
|
|
117
|
+
return {
|
|
118
|
+
valid: false,
|
|
119
|
+
error: `Hex input exceeds maximum of ${limits.maxHexBytes} bytes`,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { valid: true }
|
|
124
|
+
}
|
|
125
|
+
|
|
25
126
|
/**
|
|
26
|
-
*
|
|
127
|
+
* FieldVisitor generates metadata for form input fields from Candid IDL types.
|
|
27
128
|
*
|
|
28
129
|
* ## Design Principles
|
|
29
130
|
*
|
|
@@ -31,66 +132,90 @@ export * from "./types"
|
|
|
31
132
|
* 2. **No value dependencies** - metadata is independent of actual values
|
|
32
133
|
* 3. **Form-framework agnostic** - output can be used with TanStack, React Hook Form, etc.
|
|
33
134
|
* 4. **Efficient** - single traversal, no runtime type checking
|
|
135
|
+
* 5. **TanStack Form optimized** - name paths compatible with TanStack Form patterns
|
|
34
136
|
*
|
|
35
137
|
* ## Output Structure
|
|
36
138
|
*
|
|
37
139
|
* Each field has:
|
|
38
140
|
* - `type`: The field type (record, variant, text, number, etc.)
|
|
39
|
-
* - `label`:
|
|
40
|
-
* - `
|
|
141
|
+
* - `label`: Raw label from Candid
|
|
142
|
+
* - `displayLabel`: Human-readable formatted label
|
|
143
|
+
* - `name`: TanStack Form compatible path (e.g., "[0]", "[0].owner", "tags[1]")
|
|
144
|
+
* - `component`: Suggested component type for rendering
|
|
145
|
+
* - `renderHint`: Hints for UI rendering strategy
|
|
41
146
|
* - `defaultValue`: Initial value for the form
|
|
147
|
+
* - `schema`: Zod schema for validation
|
|
42
148
|
* - Type-specific properties (options for variant, fields for record, etc.)
|
|
149
|
+
* - Helper methods for dynamic forms (getOptionDefault, getItemDefault, etc.)
|
|
150
|
+
*
|
|
151
|
+
* ## Usage with TanStack Form
|
|
43
152
|
*
|
|
44
153
|
* @example
|
|
45
154
|
* ```typescript
|
|
46
|
-
*
|
|
47
|
-
*
|
|
155
|
+
* import { useForm } from '@tanstack/react-form'
|
|
156
|
+
* import { FieldVisitor } from '@ic-reactor/candid'
|
|
48
157
|
*
|
|
49
|
-
*
|
|
158
|
+
* const visitor = new FieldVisitor()
|
|
159
|
+
* const serviceMeta = service.accept(visitor, null)
|
|
50
160
|
* const methodMeta = serviceMeta["icrc1_transfer"]
|
|
51
|
-
*
|
|
52
|
-
*
|
|
161
|
+
*
|
|
162
|
+
* const form = useForm({
|
|
163
|
+
* defaultValues: methodMeta.defaultValue,
|
|
164
|
+
* validators: { onBlur: methodMeta.schema },
|
|
165
|
+
* onSubmit: async ({ value }) => {
|
|
166
|
+
* await actor.icrc1_transfer(...value)
|
|
167
|
+
* }
|
|
168
|
+
* })
|
|
169
|
+
*
|
|
170
|
+
* // Render fields dynamically
|
|
171
|
+
* methodMeta.fields.map((field, index) => (
|
|
172
|
+
* <form.Field key={index} name={field.name}>
|
|
173
|
+
* {(fieldApi) => <DynamicInput field={field} fieldApi={fieldApi} />}
|
|
174
|
+
* </form.Field>
|
|
175
|
+
* ))
|
|
53
176
|
* ```
|
|
54
177
|
*/
|
|
55
|
-
export class
|
|
178
|
+
export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
56
179
|
string,
|
|
57
|
-
|
|
180
|
+
Field | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
|
|
58
181
|
> {
|
|
59
|
-
|
|
182
|
+
public recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
|
|
60
183
|
|
|
61
|
-
private
|
|
62
|
-
|
|
184
|
+
private nameStack: string[] = []
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Execute function with a name segment pushed onto the stack.
|
|
188
|
+
* Automatically manages stack cleanup.
|
|
189
|
+
*/
|
|
190
|
+
private withName<T>(name: string, fn: () => T): T {
|
|
191
|
+
this.nameStack.push(name)
|
|
63
192
|
try {
|
|
64
193
|
return fn()
|
|
65
194
|
} finally {
|
|
66
|
-
this.
|
|
195
|
+
this.nameStack.pop()
|
|
67
196
|
}
|
|
68
197
|
}
|
|
69
198
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
if (typeof key === "number") {
|
|
77
|
-
return parent ? `${parent}[${key}]` : String(key)
|
|
78
|
-
}
|
|
79
|
-
return parent ? `${parent}.${key}` : key
|
|
199
|
+
/**
|
|
200
|
+
* Get the current full name path for form binding.
|
|
201
|
+
* Returns empty string for root level.
|
|
202
|
+
*/
|
|
203
|
+
private currentName(): string {
|
|
204
|
+
return this.nameStack.join("")
|
|
80
205
|
}
|
|
81
206
|
|
|
82
207
|
// ════════════════════════════════════════════════════════════════════════
|
|
83
208
|
// Service & Function Level
|
|
84
209
|
// ════════════════════════════════════════════════════════════════════════
|
|
85
210
|
|
|
86
|
-
public visitService(t: IDL.ServiceClass):
|
|
87
|
-
const result = {} as
|
|
211
|
+
public visitService(t: IDL.ServiceClass): ArgumentsServiceMeta<A> {
|
|
212
|
+
const result = {} as ArgumentsServiceMeta<A>
|
|
88
213
|
|
|
89
214
|
for (const [functionName, func] of t._fields) {
|
|
90
215
|
result[functionName as FunctionName<A>] = func.accept(
|
|
91
216
|
this,
|
|
92
217
|
functionName
|
|
93
|
-
) as
|
|
218
|
+
) as ArgumentsMeta<A>
|
|
94
219
|
}
|
|
95
220
|
|
|
96
221
|
return result
|
|
@@ -99,35 +224,44 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
99
224
|
public visitFunc(
|
|
100
225
|
t: IDL.FuncClass,
|
|
101
226
|
functionName: FunctionName<A>
|
|
102
|
-
):
|
|
227
|
+
): ArgumentsMeta<A> {
|
|
103
228
|
const functionType = isQuery(t) ? "query" : "update"
|
|
229
|
+
const argCount = t.argTypes.length
|
|
104
230
|
|
|
105
231
|
const fields = t.argTypes.map((arg, index) => {
|
|
106
|
-
return this.
|
|
232
|
+
return this.withName(`[${index}]`, () =>
|
|
107
233
|
arg.accept(this, `__arg${index}`)
|
|
108
|
-
) as
|
|
234
|
+
) as Field
|
|
109
235
|
})
|
|
110
236
|
|
|
111
|
-
const defaultValues = fields.map((field) =>
|
|
237
|
+
const defaultValues = fields.map((field) => field.defaultValue)
|
|
238
|
+
|
|
239
|
+
// Handle empty args case for schema
|
|
240
|
+
// For no-arg functions, use an empty array schema
|
|
241
|
+
// For functions with args, use a proper tuple schema
|
|
242
|
+
const schema =
|
|
243
|
+
argCount === 0
|
|
244
|
+
? (z.tuple([]) as unknown as z.ZodTuple<
|
|
245
|
+
[z.ZodTypeAny, ...z.ZodTypeAny[]]
|
|
246
|
+
>)
|
|
247
|
+
: z.tuple(
|
|
248
|
+
fields.map((field) => field.schema) as [
|
|
249
|
+
z.ZodTypeAny,
|
|
250
|
+
...z.ZodTypeAny[],
|
|
251
|
+
]
|
|
252
|
+
)
|
|
112
253
|
|
|
113
254
|
return {
|
|
114
255
|
functionType,
|
|
115
256
|
functionName,
|
|
116
257
|
fields,
|
|
117
|
-
defaultValues,
|
|
258
|
+
defaultValues: defaultValues,
|
|
259
|
+
schema,
|
|
260
|
+
argCount,
|
|
261
|
+
isNoArgs: argCount === 0,
|
|
118
262
|
}
|
|
119
263
|
}
|
|
120
264
|
|
|
121
|
-
private extractDefaultValue(field: ArgumentField): unknown {
|
|
122
|
-
if ("defaultValue" in field) {
|
|
123
|
-
return field.defaultValue
|
|
124
|
-
}
|
|
125
|
-
if ("defaultValues" in field) {
|
|
126
|
-
return field.defaultValues
|
|
127
|
-
}
|
|
128
|
-
return undefined
|
|
129
|
-
}
|
|
130
|
-
|
|
131
265
|
// ════════════════════════════════════════════════════════════════════════
|
|
132
266
|
// Compound Types
|
|
133
267
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -136,26 +270,38 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
136
270
|
_t: IDL.RecordClass,
|
|
137
271
|
fields_: Array<[string, IDL.Type]>,
|
|
138
272
|
label: string
|
|
139
|
-
):
|
|
140
|
-
const
|
|
141
|
-
const fields:
|
|
142
|
-
const
|
|
273
|
+
): RecordField {
|
|
274
|
+
const name = this.currentName()
|
|
275
|
+
const fields: Field[] = []
|
|
276
|
+
const fieldMap = new Map<string, Field>()
|
|
277
|
+
const defaultValue: Record<string, unknown> = {}
|
|
278
|
+
const schemaShape: Record<string, z.ZodTypeAny> = {}
|
|
143
279
|
|
|
144
280
|
for (const [key, type] of fields_) {
|
|
145
|
-
const field = this.
|
|
281
|
+
const field = this.withName(name ? `.${key}` : key, () =>
|
|
146
282
|
type.accept(this, key)
|
|
147
|
-
) as
|
|
283
|
+
) as Field
|
|
148
284
|
|
|
149
285
|
fields.push(field)
|
|
150
|
-
|
|
286
|
+
fieldMap.set(key, field)
|
|
287
|
+
defaultValue[key] = field.defaultValue
|
|
288
|
+
schemaShape[key] = field.schema
|
|
151
289
|
}
|
|
152
290
|
|
|
291
|
+
const schema = z.object(schemaShape)
|
|
292
|
+
|
|
153
293
|
return {
|
|
154
294
|
type: "record",
|
|
155
295
|
label,
|
|
156
|
-
|
|
296
|
+
displayLabel: formatLabel(label),
|
|
297
|
+
name,
|
|
298
|
+
component: "record-container",
|
|
299
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
157
300
|
fields,
|
|
158
|
-
|
|
301
|
+
fieldMap,
|
|
302
|
+
defaultValue,
|
|
303
|
+
schema,
|
|
304
|
+
candidType: "record",
|
|
159
305
|
}
|
|
160
306
|
}
|
|
161
307
|
|
|
@@ -163,33 +309,80 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
163
309
|
_t: IDL.VariantClass,
|
|
164
310
|
fields_: Array<[string, IDL.Type]>,
|
|
165
311
|
label: string
|
|
166
|
-
):
|
|
167
|
-
const
|
|
168
|
-
const fields:
|
|
312
|
+
): VariantField {
|
|
313
|
+
const name = this.currentName()
|
|
314
|
+
const fields: Field[] = []
|
|
169
315
|
const options: string[] = []
|
|
316
|
+
const optionMap = new Map<string, Field>()
|
|
317
|
+
const variantSchemas: z.ZodTypeAny[] = []
|
|
170
318
|
|
|
171
319
|
for (const [key, type] of fields_) {
|
|
172
|
-
const field = this.
|
|
320
|
+
const field = this.withName(`.${key}`, () =>
|
|
173
321
|
type.accept(this, key)
|
|
174
|
-
) as
|
|
322
|
+
) as Field
|
|
175
323
|
|
|
176
324
|
fields.push(field)
|
|
177
325
|
options.push(key)
|
|
326
|
+
optionMap.set(key, field)
|
|
327
|
+
variantSchemas.push(z.object({ [key]: field.schema }))
|
|
178
328
|
}
|
|
179
329
|
|
|
180
330
|
const defaultOption = options[0]
|
|
181
|
-
const
|
|
182
|
-
|
|
331
|
+
const firstField = fields[0]
|
|
332
|
+
const defaultValue = {
|
|
333
|
+
[defaultOption]: firstField.defaultValue,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const schema = z.union(variantSchemas as [z.ZodTypeAny, ...z.ZodTypeAny[]])
|
|
337
|
+
|
|
338
|
+
// Helper to get default value for any option
|
|
339
|
+
const getOptionDefault = (option: string): Record<string, unknown> => {
|
|
340
|
+
const optField = optionMap.get(option)
|
|
341
|
+
if (!optField) {
|
|
342
|
+
throw new Error(`Unknown variant option: ${option}`)
|
|
343
|
+
}
|
|
344
|
+
return { [option]: optField.defaultValue }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Helper to get field for a specific option
|
|
348
|
+
const getField = (option: string): Field => {
|
|
349
|
+
const optField = optionMap.get(option)
|
|
350
|
+
if (!optField) {
|
|
351
|
+
throw new Error(`Unknown variant option: ${option}`)
|
|
352
|
+
}
|
|
353
|
+
return optField
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Helper to get currently selected option from a value
|
|
357
|
+
const getSelectedOption = (value: Record<string, unknown>): string => {
|
|
358
|
+
const validKeys = Object.keys(value).filter((k) => options.includes(k))
|
|
359
|
+
return validKeys[0] ?? defaultOption
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Helper to get selected field from a value
|
|
363
|
+
const getSelectedField = (value: Record<string, unknown>): Field => {
|
|
364
|
+
const selectedOption = getSelectedOption(value)
|
|
365
|
+
return getField(selectedOption)
|
|
183
366
|
}
|
|
184
367
|
|
|
185
368
|
return {
|
|
186
369
|
type: "variant",
|
|
187
370
|
label,
|
|
188
|
-
|
|
371
|
+
displayLabel: formatLabel(label),
|
|
372
|
+
name,
|
|
373
|
+
component: "variant-select",
|
|
374
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
189
375
|
fields,
|
|
190
376
|
options,
|
|
191
377
|
defaultOption,
|
|
192
|
-
|
|
378
|
+
optionMap,
|
|
379
|
+
defaultValue,
|
|
380
|
+
schema,
|
|
381
|
+
getOptionDefault,
|
|
382
|
+
getField,
|
|
383
|
+
getSelectedOption,
|
|
384
|
+
getSelectedField,
|
|
385
|
+
candidType: "variant",
|
|
193
386
|
}
|
|
194
387
|
}
|
|
195
388
|
|
|
@@ -197,27 +390,36 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
197
390
|
_t: IDL.TupleClass<T>,
|
|
198
391
|
components: IDL.Type[],
|
|
199
392
|
label: string
|
|
200
|
-
):
|
|
201
|
-
const
|
|
202
|
-
const fields:
|
|
203
|
-
const
|
|
393
|
+
): TupleField {
|
|
394
|
+
const name = this.currentName()
|
|
395
|
+
const fields: Field[] = []
|
|
396
|
+
const defaultValue: unknown[] = []
|
|
397
|
+
const schemas: z.ZodTypeAny[] = []
|
|
204
398
|
|
|
205
399
|
for (let index = 0; index < components.length; index++) {
|
|
206
400
|
const type = components[index]
|
|
207
|
-
const field = this.
|
|
401
|
+
const field = this.withName(`[${index}]`, () =>
|
|
208
402
|
type.accept(this, `_${index}_`)
|
|
209
|
-
) as
|
|
403
|
+
) as Field
|
|
210
404
|
|
|
211
405
|
fields.push(field)
|
|
212
|
-
|
|
406
|
+
defaultValue.push(field.defaultValue)
|
|
407
|
+
schemas.push(field.schema)
|
|
213
408
|
}
|
|
214
409
|
|
|
410
|
+
const schema = z.tuple(schemas as [z.ZodTypeAny, ...z.ZodTypeAny[]])
|
|
411
|
+
|
|
215
412
|
return {
|
|
216
413
|
type: "tuple",
|
|
217
414
|
label,
|
|
218
|
-
|
|
415
|
+
displayLabel: formatLabel(label),
|
|
416
|
+
name,
|
|
417
|
+
component: "tuple-container",
|
|
418
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
219
419
|
fields,
|
|
220
|
-
|
|
420
|
+
defaultValue,
|
|
421
|
+
schema,
|
|
422
|
+
candidType: "tuple",
|
|
221
423
|
}
|
|
222
424
|
}
|
|
223
425
|
|
|
@@ -225,19 +427,41 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
225
427
|
_t: IDL.OptClass<T>,
|
|
226
428
|
ty: IDL.Type<T>,
|
|
227
429
|
label: string
|
|
228
|
-
):
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
) as
|
|
430
|
+
): OptionalField {
|
|
431
|
+
const name = this.currentName()
|
|
432
|
+
|
|
433
|
+
// For optional, the inner field keeps the same name path
|
|
434
|
+
// because the value replaces null directly (not nested)
|
|
435
|
+
const innerField = ty.accept(this, label) as Field
|
|
436
|
+
|
|
437
|
+
const schema = z.union([
|
|
438
|
+
innerField.schema,
|
|
439
|
+
z.null(),
|
|
440
|
+
z.undefined().transform(() => null),
|
|
441
|
+
z.literal("").transform(() => null),
|
|
442
|
+
])
|
|
443
|
+
|
|
444
|
+
// Helper to get the inner default when enabling the optional
|
|
445
|
+
const getInnerDefault = (): unknown => innerField.defaultValue
|
|
446
|
+
|
|
447
|
+
// Helper to check if a value represents an enabled optional
|
|
448
|
+
const isEnabled = (value: unknown): boolean => {
|
|
449
|
+
return value !== null && typeof value !== "undefined"
|
|
450
|
+
}
|
|
234
451
|
|
|
235
452
|
return {
|
|
236
453
|
type: "optional",
|
|
237
454
|
label,
|
|
238
|
-
|
|
455
|
+
displayLabel: formatLabel(label),
|
|
456
|
+
name,
|
|
457
|
+
component: "optional-toggle",
|
|
458
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
239
459
|
innerField,
|
|
240
460
|
defaultValue: null,
|
|
461
|
+
schema,
|
|
462
|
+
getInnerDefault,
|
|
463
|
+
isEnabled,
|
|
464
|
+
candidType: "opt",
|
|
241
465
|
}
|
|
242
466
|
}
|
|
243
467
|
|
|
@@ -245,32 +469,80 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
245
469
|
_t: IDL.VecClass<T>,
|
|
246
470
|
ty: IDL.Type<T>,
|
|
247
471
|
label: string
|
|
248
|
-
):
|
|
249
|
-
const
|
|
472
|
+
): VectorField | BlobField {
|
|
473
|
+
const name = this.currentName()
|
|
250
474
|
|
|
251
475
|
// Check if it's blob (vec nat8)
|
|
252
476
|
const isBlob = ty instanceof IDL.FixedNatClass && ty._bits === 8
|
|
253
477
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
478
|
+
// Item field uses [0] as template path
|
|
479
|
+
const itemField = this.withName("[0]", () =>
|
|
480
|
+
ty.accept(this, `${label}_item`)
|
|
481
|
+
) as Field
|
|
257
482
|
|
|
258
483
|
if (isBlob) {
|
|
484
|
+
const schema = z.union([
|
|
485
|
+
z.string(),
|
|
486
|
+
z.array(z.number()),
|
|
487
|
+
z.instanceof(Uint8Array),
|
|
488
|
+
])
|
|
489
|
+
|
|
490
|
+
const limits = { ...DEFAULT_BLOB_LIMITS }
|
|
491
|
+
|
|
259
492
|
return {
|
|
260
493
|
type: "blob",
|
|
261
494
|
label,
|
|
262
|
-
|
|
495
|
+
displayLabel: formatLabel(label),
|
|
496
|
+
name,
|
|
497
|
+
component: "blob-upload",
|
|
498
|
+
renderHint: FILE_RENDER_HINT,
|
|
263
499
|
itemField,
|
|
264
500
|
defaultValue: "",
|
|
501
|
+
schema,
|
|
502
|
+
acceptedFormats: ["hex", "base64", "file"],
|
|
503
|
+
limits,
|
|
504
|
+
normalizeHex,
|
|
505
|
+
validateInput: (value: string | Uint8Array) =>
|
|
506
|
+
validateBlobInput(value, limits),
|
|
507
|
+
candidType: "blob",
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const schema = z.array(itemField.schema)
|
|
512
|
+
|
|
513
|
+
// Helper to get a new item with default values
|
|
514
|
+
const getItemDefault = (): unknown => itemField.defaultValue
|
|
515
|
+
|
|
516
|
+
// Helper to create an item field for a specific index
|
|
517
|
+
const createItemField = (
|
|
518
|
+
index: number,
|
|
519
|
+
overrides?: { label?: string }
|
|
520
|
+
): Field => {
|
|
521
|
+
// Replace [0] in template with actual index
|
|
522
|
+
const itemName = name ? `${name}[${index}]` : `[${index}]`
|
|
523
|
+
const itemLabel = overrides?.label ?? `Item ${index}`
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
...itemField,
|
|
527
|
+
name: itemName,
|
|
528
|
+
label: itemLabel,
|
|
529
|
+
displayLabel: formatLabel(itemLabel),
|
|
265
530
|
}
|
|
266
531
|
}
|
|
267
532
|
|
|
268
533
|
return {
|
|
269
534
|
type: "vector",
|
|
270
535
|
label,
|
|
271
|
-
|
|
536
|
+
displayLabel: formatLabel(label),
|
|
537
|
+
name,
|
|
538
|
+
component: "vector-list",
|
|
539
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
272
540
|
itemField,
|
|
273
541
|
defaultValue: [],
|
|
542
|
+
schema,
|
|
543
|
+
getItemDefault,
|
|
544
|
+
createItemField,
|
|
545
|
+
candidType: "vec",
|
|
274
546
|
}
|
|
275
547
|
}
|
|
276
548
|
|
|
@@ -278,16 +550,39 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
278
550
|
_t: IDL.RecClass<T>,
|
|
279
551
|
ty: IDL.ConstructType<T>,
|
|
280
552
|
label: string
|
|
281
|
-
):
|
|
282
|
-
const
|
|
553
|
+
): RecursiveField {
|
|
554
|
+
const name = this.currentName()
|
|
555
|
+
const typeName = ty.name || "RecursiveType"
|
|
556
|
+
|
|
557
|
+
let schema: z.ZodTypeAny
|
|
558
|
+
|
|
559
|
+
if (this.recursiveSchemas.has(typeName)) {
|
|
560
|
+
schema = this.recursiveSchemas.get(typeName)!
|
|
561
|
+
} else {
|
|
562
|
+
schema = z.lazy(() => (ty.accept(this, label) as Field).schema)
|
|
563
|
+
this.recursiveSchemas.set(typeName, schema)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Lazy extraction to prevent infinite loops
|
|
567
|
+
const extract = (): Field =>
|
|
568
|
+
this.withName(name, () => ty.accept(this, label)) as Field
|
|
569
|
+
|
|
570
|
+
// Helper to get inner default (evaluates lazily)
|
|
571
|
+
const getInnerDefault = (): unknown => extract().defaultValue
|
|
283
572
|
|
|
284
573
|
return {
|
|
285
574
|
type: "recursive",
|
|
286
575
|
label,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
576
|
+
displayLabel: formatLabel(label),
|
|
577
|
+
name,
|
|
578
|
+
component: "recursive-lazy",
|
|
579
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
580
|
+
typeName,
|
|
581
|
+
extract,
|
|
582
|
+
defaultValue: undefined,
|
|
583
|
+
schema,
|
|
584
|
+
getInnerDefault,
|
|
585
|
+
candidType: "rec",
|
|
291
586
|
}
|
|
292
587
|
}
|
|
293
588
|
|
|
@@ -295,111 +590,255 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
295
590
|
// Primitive Types
|
|
296
591
|
// ════════════════════════════════════════════════════════════════════════
|
|
297
592
|
|
|
298
|
-
public visitPrincipal(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
593
|
+
public visitPrincipal(_t: IDL.PrincipalClass, label: string): PrincipalField {
|
|
594
|
+
const schema = z.custom<Principal>(
|
|
595
|
+
(val) => {
|
|
596
|
+
if (val instanceof Principal) return true
|
|
597
|
+
if (typeof val === "string") {
|
|
598
|
+
try {
|
|
599
|
+
Principal.fromText(val)
|
|
600
|
+
return true
|
|
601
|
+
} catch {
|
|
602
|
+
return false
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return false
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
message: "Invalid Principal format",
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
const inputProps: PrimitiveInputProps = {
|
|
613
|
+
type: "text",
|
|
614
|
+
placeholder: "aaaaa-aa or full principal ID",
|
|
615
|
+
minLength: 7,
|
|
616
|
+
maxLength: 64,
|
|
617
|
+
spellCheck: false,
|
|
618
|
+
autoComplete: "off",
|
|
619
|
+
}
|
|
620
|
+
|
|
302
621
|
return {
|
|
303
622
|
type: "principal",
|
|
304
623
|
label,
|
|
305
|
-
|
|
624
|
+
displayLabel: formatLabel(label),
|
|
625
|
+
name: this.currentName(),
|
|
626
|
+
component: "principal-input",
|
|
627
|
+
renderHint: TEXT_RENDER_HINT,
|
|
306
628
|
defaultValue: "",
|
|
307
629
|
maxLength: 64,
|
|
308
630
|
minLength: 7,
|
|
631
|
+
schema,
|
|
632
|
+
inputProps,
|
|
633
|
+
candidType: "principal",
|
|
309
634
|
}
|
|
310
635
|
}
|
|
311
636
|
|
|
312
|
-
public visitText(_t: IDL.TextClass, label: string):
|
|
637
|
+
public visitText(_t: IDL.TextClass, label: string): TextField {
|
|
638
|
+
const inputProps: PrimitiveInputProps = {
|
|
639
|
+
type: "text",
|
|
640
|
+
placeholder: "Enter text...",
|
|
641
|
+
spellCheck: true,
|
|
642
|
+
}
|
|
643
|
+
|
|
313
644
|
return {
|
|
314
645
|
type: "text",
|
|
315
646
|
label,
|
|
316
|
-
|
|
647
|
+
displayLabel: formatLabel(label),
|
|
648
|
+
name: this.currentName(),
|
|
649
|
+
component: "text-input",
|
|
650
|
+
renderHint: TEXT_RENDER_HINT,
|
|
317
651
|
defaultValue: "",
|
|
652
|
+
schema: z.string().min(1, "Required"),
|
|
653
|
+
inputProps,
|
|
654
|
+
candidType: "text",
|
|
318
655
|
}
|
|
319
656
|
}
|
|
320
657
|
|
|
321
|
-
public visitBool(_t: IDL.BoolClass, label: string):
|
|
658
|
+
public visitBool(_t: IDL.BoolClass, label: string): BooleanField {
|
|
659
|
+
const inputProps: PrimitiveInputProps = {
|
|
660
|
+
type: "checkbox",
|
|
661
|
+
}
|
|
662
|
+
|
|
322
663
|
return {
|
|
323
664
|
type: "boolean",
|
|
324
665
|
label,
|
|
325
|
-
|
|
666
|
+
displayLabel: formatLabel(label),
|
|
667
|
+
name: this.currentName(),
|
|
668
|
+
component: "boolean-checkbox",
|
|
669
|
+
renderHint: CHECKBOX_RENDER_HINT,
|
|
326
670
|
defaultValue: false,
|
|
671
|
+
schema: z.boolean(),
|
|
672
|
+
inputProps,
|
|
673
|
+
candidType: "bool",
|
|
327
674
|
}
|
|
328
675
|
}
|
|
329
676
|
|
|
330
|
-
public visitNull(_t: IDL.NullClass, label: string):
|
|
677
|
+
public visitNull(_t: IDL.NullClass, label: string): NullField {
|
|
331
678
|
return {
|
|
332
679
|
type: "null",
|
|
333
680
|
label,
|
|
334
|
-
|
|
681
|
+
displayLabel: formatLabel(label),
|
|
682
|
+
name: this.currentName(),
|
|
683
|
+
component: "null-hidden",
|
|
684
|
+
renderHint: {
|
|
685
|
+
isCompound: false,
|
|
686
|
+
isPrimitive: true,
|
|
687
|
+
},
|
|
335
688
|
defaultValue: null,
|
|
689
|
+
schema: z.null(),
|
|
690
|
+
candidType: "null",
|
|
336
691
|
}
|
|
337
692
|
}
|
|
338
693
|
|
|
339
|
-
//
|
|
694
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
695
|
+
// Number Types with Constraints
|
|
696
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
697
|
+
|
|
340
698
|
private visitNumberType(
|
|
341
699
|
label: string,
|
|
342
|
-
candidType: string
|
|
343
|
-
|
|
700
|
+
candidType: string,
|
|
701
|
+
options: {
|
|
702
|
+
unsigned: boolean
|
|
703
|
+
isFloat: boolean
|
|
704
|
+
bits?: number
|
|
705
|
+
min?: string
|
|
706
|
+
max?: string
|
|
707
|
+
}
|
|
708
|
+
): NumberField | TextField {
|
|
709
|
+
let schema = z.string().min(1, "Required")
|
|
710
|
+
|
|
711
|
+
if (options.isFloat) {
|
|
712
|
+
schema = schema.refine((val) => !isNaN(Number(val)), "Must be a number")
|
|
713
|
+
} else if (options.unsigned) {
|
|
714
|
+
schema = schema.regex(/^\d+$/, "Must be a positive number")
|
|
715
|
+
} else {
|
|
716
|
+
schema = schema.regex(/^-?\d+$/, "Must be a number")
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Use "text" type for large numbers (BigInt) to ensure precision and better UI handling
|
|
720
|
+
// Standard number input has issues with large integers
|
|
721
|
+
const isBigInt = !options.isFloat && (!options.bits || options.bits > 32)
|
|
722
|
+
const type = isBigInt ? "text" : "number"
|
|
723
|
+
|
|
724
|
+
if (type === "text") {
|
|
725
|
+
const inputProps: PrimitiveInputProps = {
|
|
726
|
+
type: "text",
|
|
727
|
+
placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
|
|
728
|
+
inputMode: "numeric",
|
|
729
|
+
pattern: options.unsigned ? "\\d+" : "-?\\d+",
|
|
730
|
+
spellCheck: false,
|
|
731
|
+
autoComplete: "off",
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
type: "text",
|
|
736
|
+
label,
|
|
737
|
+
displayLabel: formatLabel(label),
|
|
738
|
+
name: this.currentName(),
|
|
739
|
+
component: "text-input",
|
|
740
|
+
renderHint: TEXT_RENDER_HINT,
|
|
741
|
+
defaultValue: "",
|
|
742
|
+
candidType,
|
|
743
|
+
schema,
|
|
744
|
+
inputProps,
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const inputProps: PrimitiveInputProps = {
|
|
749
|
+
type: "number",
|
|
750
|
+
placeholder: options.isFloat ? "0.0" : "0",
|
|
751
|
+
inputMode: options.isFloat ? "decimal" : "numeric",
|
|
752
|
+
min: options.min,
|
|
753
|
+
max: options.max,
|
|
754
|
+
step: options.isFloat ? "any" : "1",
|
|
755
|
+
}
|
|
756
|
+
|
|
344
757
|
return {
|
|
345
758
|
type: "number",
|
|
346
759
|
label,
|
|
347
|
-
|
|
760
|
+
displayLabel: formatLabel(label),
|
|
761
|
+
name: this.currentName(),
|
|
762
|
+
component: "number-input",
|
|
763
|
+
renderHint: NUMBER_RENDER_HINT,
|
|
348
764
|
defaultValue: "",
|
|
349
765
|
candidType,
|
|
766
|
+
schema: schema,
|
|
767
|
+
inputProps,
|
|
768
|
+
...options,
|
|
350
769
|
}
|
|
351
770
|
}
|
|
352
771
|
|
|
353
|
-
public visitInt(_t: IDL.IntClass, label: string):
|
|
354
|
-
return this.visitNumberType(label, "int"
|
|
772
|
+
public visitInt(_t: IDL.IntClass, label: string): NumberField | TextField {
|
|
773
|
+
return this.visitNumberType(label, "int", {
|
|
774
|
+
unsigned: false,
|
|
775
|
+
isFloat: false,
|
|
776
|
+
})
|
|
355
777
|
}
|
|
356
778
|
|
|
357
|
-
public visitNat(_t: IDL.NatClass, label: string):
|
|
358
|
-
return this.visitNumberType(label, "nat"
|
|
779
|
+
public visitNat(_t: IDL.NatClass, label: string): NumberField | TextField {
|
|
780
|
+
return this.visitNumberType(label, "nat", {
|
|
781
|
+
unsigned: true,
|
|
782
|
+
isFloat: false,
|
|
783
|
+
})
|
|
359
784
|
}
|
|
360
785
|
|
|
361
|
-
public visitFloat(
|
|
362
|
-
return this.visitNumberType(label,
|
|
786
|
+
public visitFloat(t: IDL.FloatClass, label: string): NumberField {
|
|
787
|
+
return this.visitNumberType(label, `float${t._bits}`, {
|
|
788
|
+
unsigned: false,
|
|
789
|
+
isFloat: true,
|
|
790
|
+
bits: t._bits,
|
|
791
|
+
}) as NumberField
|
|
363
792
|
}
|
|
364
793
|
|
|
365
794
|
public visitFixedInt(
|
|
366
795
|
t: IDL.FixedIntClass,
|
|
367
796
|
label: string
|
|
368
|
-
):
|
|
369
|
-
|
|
797
|
+
): NumberField | TextField {
|
|
798
|
+
const bits = t._bits
|
|
799
|
+
// Calculate min/max for signed integers
|
|
800
|
+
const max = (BigInt(2) ** BigInt(bits - 1) - BigInt(1)).toString()
|
|
801
|
+
const min = (-(BigInt(2) ** BigInt(bits - 1))).toString()
|
|
802
|
+
|
|
803
|
+
return this.visitNumberType(label, `int${bits}`, {
|
|
804
|
+
unsigned: false,
|
|
805
|
+
isFloat: false,
|
|
806
|
+
bits,
|
|
807
|
+
min,
|
|
808
|
+
max,
|
|
809
|
+
})
|
|
370
810
|
}
|
|
371
811
|
|
|
372
812
|
public visitFixedNat(
|
|
373
813
|
t: IDL.FixedNatClass,
|
|
374
814
|
label: string
|
|
375
|
-
):
|
|
376
|
-
|
|
815
|
+
): NumberField | TextField {
|
|
816
|
+
const bits = t._bits
|
|
817
|
+
// Calculate max for unsigned integers
|
|
818
|
+
const max = (BigInt(2) ** BigInt(bits) - BigInt(1)).toString()
|
|
819
|
+
|
|
820
|
+
return this.visitNumberType(label, `nat${bits}`, {
|
|
821
|
+
unsigned: true,
|
|
822
|
+
isFloat: false,
|
|
823
|
+
bits,
|
|
824
|
+
min: "0",
|
|
825
|
+
max,
|
|
826
|
+
})
|
|
377
827
|
}
|
|
378
828
|
|
|
379
|
-
public visitType<T>(_t: IDL.Type<T>, label: string):
|
|
829
|
+
public visitType<T>(_t: IDL.Type<T>, label: string): UnknownField {
|
|
380
830
|
return {
|
|
381
831
|
type: "unknown",
|
|
382
832
|
label,
|
|
383
|
-
|
|
833
|
+
displayLabel: formatLabel(label),
|
|
834
|
+
name: this.currentName(),
|
|
835
|
+
component: "unknown-fallback",
|
|
836
|
+
renderHint: {
|
|
837
|
+
isCompound: false,
|
|
838
|
+
isPrimitive: false,
|
|
839
|
+
},
|
|
384
840
|
defaultValue: undefined,
|
|
841
|
+
schema: z.any(),
|
|
385
842
|
}
|
|
386
843
|
}
|
|
387
844
|
}
|
|
388
|
-
|
|
389
|
-
// ════════════════════════════════════════════════════════════════════════════
|
|
390
|
-
// Legacy Exports (for backward compatibility)
|
|
391
|
-
// ════════════════════════════════════════════════════════════════════════════
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* @deprecated Use ArgumentFieldVisitor instead
|
|
395
|
-
*/
|
|
396
|
-
export { ArgumentFieldVisitor as VisitTanstackField }
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* @deprecated Use ArgumentField instead
|
|
400
|
-
*/
|
|
401
|
-
export type {
|
|
402
|
-
ArgumentField as TanstackAllArgTypes,
|
|
403
|
-
MethodArgumentsMeta as TanstackMethodField,
|
|
404
|
-
ServiceArgumentsMeta as TanstackServiceField,
|
|
405
|
-
}
|