@ic-reactor/candid 3.0.18-beta.1 → 3.1.1
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/README.md +41 -14
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +4 -4
- package/dist/metadata-display-reactor.d.ts.map +1 -1
- package/dist/metadata-display-reactor.js +1 -1
- package/dist/metadata-display-reactor.js.map +1 -1
- package/dist/metadata-reactor.d.ts +51 -0
- package/dist/metadata-reactor.d.ts.map +1 -0
- package/dist/metadata-reactor.js +193 -0
- package/dist/metadata-reactor.js.map +1 -0
- package/dist/reactor.d.ts +3 -2
- package/dist/reactor.d.ts.map +1 -1
- package/dist/reactor.js +6 -0
- package/dist/reactor.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/visitor/candid/helpers.d.ts +4 -0
- package/dist/visitor/candid/helpers.d.ts.map +1 -0
- package/dist/visitor/candid/helpers.js +84 -0
- package/dist/visitor/candid/helpers.js.map +1 -0
- package/dist/visitor/candid/index.d.ts +45 -0
- package/dist/visitor/candid/index.d.ts.map +1 -0
- package/dist/visitor/candid/index.js +417 -0
- package/dist/visitor/candid/index.js.map +1 -0
- package/dist/visitor/candid/types.d.ts +167 -0
- package/dist/visitor/candid/types.d.ts.map +1 -0
- package/dist/visitor/candid/types.js +2 -0
- package/dist/visitor/candid/types.js.map +1 -0
- package/dist/visitor/constants.d.ts +1 -1
- package/dist/visitor/constants.d.ts.map +1 -1
- package/dist/visitor/index.d.ts +1 -0
- package/dist/visitor/index.d.ts.map +1 -1
- package/dist/visitor/index.js +1 -0
- package/dist/visitor/index.js.map +1 -1
- package/dist/visitor/returns/index.d.ts.map +1 -1
- package/dist/visitor/returns/index.js +1 -1
- package/dist/visitor/returns/index.js.map +1 -1
- package/dist/visitor/returns/types.d.ts +1 -1
- package/dist/visitor/returns/types.d.ts.map +1 -1
- package/package.json +11 -7
- package/src/display-reactor.ts +1 -2
- package/src/index.ts +1 -0
- package/src/metadata-display-reactor.ts +4 -4
- package/src/metadata-reactor.ts +287 -0
- package/src/reactor.ts +12 -2
- package/src/types.ts +11 -0
- package/src/visitor/candid/helpers.ts +89 -0
- package/src/visitor/candid/index.ts +658 -0
- package/src/visitor/candid/types.ts +291 -0
- package/src/visitor/constants.ts +1 -1
- package/src/visitor/index.ts +1 -0
- package/src/visitor/returns/index.ts +6 -4
- package/src/visitor/returns/types.ts +0 -2
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
2
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
3
|
+
import type { BaseActor, FunctionName } from "@ic-reactor/core"
|
|
4
|
+
import * as z from "zod"
|
|
5
|
+
import { isQuery } from "../helpers"
|
|
6
|
+
import { formatLabel } from "../arguments/helpers"
|
|
7
|
+
import type {
|
|
8
|
+
FormServiceMeta,
|
|
9
|
+
FormArgumentsMeta,
|
|
10
|
+
FormFieldNode,
|
|
11
|
+
FormFieldType,
|
|
12
|
+
FormRenderHint,
|
|
13
|
+
VariableRefCandidate,
|
|
14
|
+
} from "./types"
|
|
15
|
+
import { cloneField, toFormValue } from "./helpers"
|
|
16
|
+
|
|
17
|
+
export * from "./types"
|
|
18
|
+
|
|
19
|
+
const COMPOUND_RENDER_HINT: FormRenderHint = {
|
|
20
|
+
isCompound: true,
|
|
21
|
+
isPrimitive: false,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TEXT_RENDER_HINT: FormRenderHint = {
|
|
25
|
+
isCompound: false,
|
|
26
|
+
isPrimitive: true,
|
|
27
|
+
inputType: "text",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NUMBER_RENDER_HINT: FormRenderHint = {
|
|
31
|
+
isCompound: false,
|
|
32
|
+
isPrimitive: true,
|
|
33
|
+
inputType: "number",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CHECKBOX_RENDER_HINT: FormRenderHint = {
|
|
37
|
+
isCompound: false,
|
|
38
|
+
isPrimitive: true,
|
|
39
|
+
inputType: "checkbox",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const FILE_RENDER_HINT: FormRenderHint = {
|
|
43
|
+
isCompound: false,
|
|
44
|
+
isPrimitive: true,
|
|
45
|
+
inputType: "file",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Visitor that generates form-oriented metadata from Candid IDL types.
|
|
50
|
+
*
|
|
51
|
+
* Each generated field includes:
|
|
52
|
+
* - `schema` for validation
|
|
53
|
+
* - `component` for renderer selection
|
|
54
|
+
* - `renderHint` for UI behavior hints
|
|
55
|
+
*/
|
|
56
|
+
export class CandidFormVisitor<A = BaseActor> extends IDL.Visitor<
|
|
57
|
+
string,
|
|
58
|
+
FormFieldNode | FormArgumentsMeta | FormServiceMeta<A>
|
|
59
|
+
> {
|
|
60
|
+
private recCache = new Map<IDL.RecClass<any>, FormFieldNode>()
|
|
61
|
+
private recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
|
|
62
|
+
private nameStack: string[] = []
|
|
63
|
+
|
|
64
|
+
private withName<T>(name: string, fn: () => T): T {
|
|
65
|
+
this.nameStack.push(name)
|
|
66
|
+
try {
|
|
67
|
+
return fn()
|
|
68
|
+
} finally {
|
|
69
|
+
this.nameStack.pop()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private currentName(): string {
|
|
74
|
+
return this.nameStack.join("")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public visitService(t: IDL.ServiceClass): FormServiceMeta<A> {
|
|
78
|
+
const result = {} as FormServiceMeta<A>
|
|
79
|
+
for (const [functionName, func] of t._fields) {
|
|
80
|
+
result[functionName as FunctionName<A>] = func.accept(
|
|
81
|
+
this,
|
|
82
|
+
functionName
|
|
83
|
+
) as FormArgumentsMeta
|
|
84
|
+
}
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public visitFunc(t: IDL.FuncClass, functionName: string): FormArgumentsMeta {
|
|
89
|
+
const functionType = isQuery(t) ? "query" : "update"
|
|
90
|
+
const args = t.argTypes.map(
|
|
91
|
+
(argType, index) =>
|
|
92
|
+
this.withName(`[${index}]`, () =>
|
|
93
|
+
argType.accept(this, `__arg${index}`)
|
|
94
|
+
) as FormFieldNode
|
|
95
|
+
)
|
|
96
|
+
const argCount = args.length
|
|
97
|
+
const schema =
|
|
98
|
+
argCount === 0
|
|
99
|
+
? (z.tuple([]) as unknown as z.ZodTuple<
|
|
100
|
+
[z.ZodTypeAny, ...z.ZodTypeAny[]]
|
|
101
|
+
>)
|
|
102
|
+
: z.tuple(
|
|
103
|
+
args.map((field) => field.schema) as [
|
|
104
|
+
z.ZodTypeAny,
|
|
105
|
+
...z.ZodTypeAny[],
|
|
106
|
+
]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
candidType: t.name,
|
|
111
|
+
functionType,
|
|
112
|
+
functionName,
|
|
113
|
+
args,
|
|
114
|
+
defaults: args.map((arg) => arg.defaultValue),
|
|
115
|
+
argCount,
|
|
116
|
+
isEmpty: argCount === 0,
|
|
117
|
+
schema,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public buildFunctionMeta(
|
|
122
|
+
func: IDL.FuncClass,
|
|
123
|
+
functionName: string
|
|
124
|
+
): FormArgumentsMeta {
|
|
125
|
+
return func.accept(this, functionName) as FormArgumentsMeta
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public buildValueMeta(
|
|
129
|
+
valueType: IDL.Type,
|
|
130
|
+
functionName = "__value"
|
|
131
|
+
): FormArgumentsMeta {
|
|
132
|
+
const valueField = this.withName("[0]", () =>
|
|
133
|
+
valueType.accept(this, "__arg0")
|
|
134
|
+
) as FormFieldNode
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
candidType: valueType.display?.() ?? valueType.name ?? "value",
|
|
138
|
+
functionType: "value",
|
|
139
|
+
functionName,
|
|
140
|
+
args: [valueField],
|
|
141
|
+
defaults: [valueField.defaultValue],
|
|
142
|
+
argCount: 1,
|
|
143
|
+
isEmpty: false,
|
|
144
|
+
schema: z.tuple([valueField.schema]),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public toFormValuesFromDecodedArgs(
|
|
149
|
+
fields: FormFieldNode[],
|
|
150
|
+
decodedArgs: unknown[]
|
|
151
|
+
): unknown[] {
|
|
152
|
+
return fields.map((field, index) => toFormValue(field, decodedArgs[index]))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public collectRefCandidatesFromRoot(
|
|
156
|
+
sourceNodeId: string,
|
|
157
|
+
rootExpr: string,
|
|
158
|
+
rootLabel: string,
|
|
159
|
+
rootField: FormFieldNode
|
|
160
|
+
): VariableRefCandidate[] {
|
|
161
|
+
const out: VariableRefCandidate[] = []
|
|
162
|
+
const walk = (field: FormFieldNode, expr: string, label: string) => {
|
|
163
|
+
out.push({
|
|
164
|
+
expr,
|
|
165
|
+
label,
|
|
166
|
+
candidType: field.candidType,
|
|
167
|
+
fieldType: field.type,
|
|
168
|
+
sourceNodeId,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
switch (field.type) {
|
|
172
|
+
case "record":
|
|
173
|
+
case "tuple":
|
|
174
|
+
for (const child of field.fields) {
|
|
175
|
+
walk(child, `${expr}.${child.label}`, `${label}.${child.label}`)
|
|
176
|
+
}
|
|
177
|
+
break
|
|
178
|
+
case "variant":
|
|
179
|
+
for (const child of field.options) {
|
|
180
|
+
walk(child, `${expr}.${child.label}`, `${label}.${child.label}`)
|
|
181
|
+
}
|
|
182
|
+
break
|
|
183
|
+
case "optional":
|
|
184
|
+
walk(field.innerField, `${expr}.some`, `${label}.some`)
|
|
185
|
+
break
|
|
186
|
+
case "vector":
|
|
187
|
+
case "recursive":
|
|
188
|
+
case "unknown":
|
|
189
|
+
case "blob":
|
|
190
|
+
case "principal":
|
|
191
|
+
case "text":
|
|
192
|
+
case "number":
|
|
193
|
+
case "boolean":
|
|
194
|
+
case "null":
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
walk(rootField, rootExpr, rootLabel)
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public buildFieldForType(
|
|
204
|
+
type: IDL.Type,
|
|
205
|
+
label: string,
|
|
206
|
+
path: string
|
|
207
|
+
): FormFieldNode {
|
|
208
|
+
return this.withName(path, () => type.accept(this, label)) as FormFieldNode
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public buildTupleFieldForTypes(
|
|
212
|
+
types: IDL.Type[],
|
|
213
|
+
label: string,
|
|
214
|
+
path: string
|
|
215
|
+
): FormFieldNode {
|
|
216
|
+
const tupleType = IDL.Tuple(...types)
|
|
217
|
+
return this.withName(path, () =>
|
|
218
|
+
tupleType.accept(this, label)
|
|
219
|
+
) as FormFieldNode
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public visitRecord(
|
|
223
|
+
t: IDL.RecordClass,
|
|
224
|
+
fields_: Array<[string, IDL.Type]>,
|
|
225
|
+
label: string
|
|
226
|
+
): FormFieldNode {
|
|
227
|
+
const name = this.currentName()
|
|
228
|
+
const fields = fields_.map(
|
|
229
|
+
([key, childType]) =>
|
|
230
|
+
this.withName(name ? `.${key}` : key, () =>
|
|
231
|
+
childType.accept(this, key)
|
|
232
|
+
) as FormFieldNode
|
|
233
|
+
)
|
|
234
|
+
const schema = z.object(
|
|
235
|
+
Object.fromEntries(fields.map((field) => [field.label, field.schema]))
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
type: "record",
|
|
240
|
+
label,
|
|
241
|
+
displayLabel: formatLabel(label),
|
|
242
|
+
name,
|
|
243
|
+
component: "record-container",
|
|
244
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
245
|
+
candidType: t.display?.() ?? t.name ?? "record",
|
|
246
|
+
fields,
|
|
247
|
+
defaultValue: Object.fromEntries(
|
|
248
|
+
fields.map((f) => [f.label, f.defaultValue])
|
|
249
|
+
),
|
|
250
|
+
schema,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public visitTuple<T extends IDL.Type[]>(
|
|
255
|
+
_t: IDL.TupleClass<T>,
|
|
256
|
+
components: IDL.Type[],
|
|
257
|
+
label: string
|
|
258
|
+
): FormFieldNode {
|
|
259
|
+
const name = this.currentName()
|
|
260
|
+
const fields = components.map(
|
|
261
|
+
(childType, index) =>
|
|
262
|
+
this.withName(`[${index}]`, () =>
|
|
263
|
+
childType.accept(this, String(index))
|
|
264
|
+
) as FormFieldNode
|
|
265
|
+
)
|
|
266
|
+
const schema =
|
|
267
|
+
fields.length === 0
|
|
268
|
+
? (z.tuple([]) as unknown as z.ZodTuple<
|
|
269
|
+
[z.ZodTypeAny, ...z.ZodTypeAny[]]
|
|
270
|
+
>)
|
|
271
|
+
: z.tuple(
|
|
272
|
+
fields.map((field) => field.schema) as [
|
|
273
|
+
z.ZodTypeAny,
|
|
274
|
+
...z.ZodTypeAny[],
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
type: "tuple",
|
|
280
|
+
label,
|
|
281
|
+
displayLabel: formatLabel(label),
|
|
282
|
+
name,
|
|
283
|
+
component: "tuple-container",
|
|
284
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
285
|
+
candidType: "tuple",
|
|
286
|
+
fields,
|
|
287
|
+
defaultValue: fields.map((f) => f.defaultValue),
|
|
288
|
+
schema,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
public visitVariant(
|
|
293
|
+
t: IDL.VariantClass,
|
|
294
|
+
fields_: Array<[string, IDL.Type]>,
|
|
295
|
+
label: string
|
|
296
|
+
): FormFieldNode {
|
|
297
|
+
const name = this.currentName()
|
|
298
|
+
const options = fields_.map(
|
|
299
|
+
([key, childType]) =>
|
|
300
|
+
this.withName(`.${key}`, () =>
|
|
301
|
+
childType.accept(this, key)
|
|
302
|
+
) as FormFieldNode
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const first =
|
|
306
|
+
options[0] ??
|
|
307
|
+
this.primitive("null", "null", `${name}.null`, "null", null, z.null())
|
|
308
|
+
const defaultOption = first.label
|
|
309
|
+
const variantSchemas = options.map((option) =>
|
|
310
|
+
option.type === "null"
|
|
311
|
+
? z.object({ _type: z.literal(option.label) })
|
|
312
|
+
: z.object({
|
|
313
|
+
_type: z.literal(option.label),
|
|
314
|
+
[option.label]: option.schema,
|
|
315
|
+
})
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
const getOption = (option: string): FormFieldNode => {
|
|
319
|
+
const found = options.find((o) => o.label === option)
|
|
320
|
+
if (!found) {
|
|
321
|
+
throw new Error(`Unknown variant option: ${option}`)
|
|
322
|
+
}
|
|
323
|
+
return found
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const getOptionDefault = (option: string): Record<string, unknown> => {
|
|
327
|
+
const field = getOption(option)
|
|
328
|
+
return field.type === "null"
|
|
329
|
+
? { _type: option }
|
|
330
|
+
: { _type: option, [option]: field.defaultValue }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const getSelectedKey = (value: Record<string, unknown>): string => {
|
|
334
|
+
if (typeof value?._type === "string") return value._type
|
|
335
|
+
const firstPresent = Object.keys(value ?? {}).find((k) =>
|
|
336
|
+
options.some((o) => o.label === k)
|
|
337
|
+
)
|
|
338
|
+
return firstPresent ?? defaultOption
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const getSelectedOption = (value: Record<string, unknown>): FormFieldNode =>
|
|
342
|
+
getOption(getSelectedKey(value))
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
type: "variant",
|
|
346
|
+
label,
|
|
347
|
+
displayLabel: formatLabel(label),
|
|
348
|
+
name,
|
|
349
|
+
component: "variant-select",
|
|
350
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
351
|
+
candidType: t.display?.() ?? t.name ?? "variant",
|
|
352
|
+
options,
|
|
353
|
+
defaultOption,
|
|
354
|
+
defaultValue: getOptionDefault(defaultOption),
|
|
355
|
+
schema:
|
|
356
|
+
variantSchemas.length === 0
|
|
357
|
+
? z.object({ _type: z.literal(defaultOption) })
|
|
358
|
+
: z.union(
|
|
359
|
+
variantSchemas as unknown as [z.ZodTypeAny, ...z.ZodTypeAny[]]
|
|
360
|
+
),
|
|
361
|
+
getOptionDefault,
|
|
362
|
+
getOption,
|
|
363
|
+
getSelectedKey,
|
|
364
|
+
getSelectedOption,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public visitOpt<T>(
|
|
369
|
+
_t: IDL.OptClass<T>,
|
|
370
|
+
ty: IDL.Type<T>,
|
|
371
|
+
label: string
|
|
372
|
+
): FormFieldNode {
|
|
373
|
+
const name = this.currentName()
|
|
374
|
+
const innerField = ty.accept(this, label) as FormFieldNode
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
type: "optional",
|
|
378
|
+
label,
|
|
379
|
+
displayLabel: formatLabel(label),
|
|
380
|
+
name,
|
|
381
|
+
component: "optional-toggle",
|
|
382
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
383
|
+
candidType: `opt ${innerField.candidType}`,
|
|
384
|
+
innerField,
|
|
385
|
+
defaultValue: null,
|
|
386
|
+
schema: z.union([
|
|
387
|
+
innerField.schema,
|
|
388
|
+
z.null(),
|
|
389
|
+
z.undefined().transform(() => null),
|
|
390
|
+
]),
|
|
391
|
+
getInnerDefault: () => innerField.defaultValue,
|
|
392
|
+
isEnabled: (value: unknown) => value !== null && value !== undefined,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
public visitVec<T>(
|
|
397
|
+
_t: IDL.VecClass<T>,
|
|
398
|
+
ty: IDL.Type<T>,
|
|
399
|
+
label: string
|
|
400
|
+
): FormFieldNode {
|
|
401
|
+
const name = this.currentName()
|
|
402
|
+
|
|
403
|
+
if (ty instanceof IDL.FixedNatClass && ty._bits === 8) {
|
|
404
|
+
return this.primitive(
|
|
405
|
+
"blob",
|
|
406
|
+
label,
|
|
407
|
+
name,
|
|
408
|
+
"blob",
|
|
409
|
+
"",
|
|
410
|
+
z.union([z.string(), z.array(z.number()), z.instanceof(Uint8Array)])
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const itemFieldTemplate = this.withName("[0]", () =>
|
|
415
|
+
ty.accept(this, `${label}_item`)
|
|
416
|
+
) as FormFieldNode
|
|
417
|
+
|
|
418
|
+
const createItemField = (index: number, overrides?: { label?: string }) => {
|
|
419
|
+
return this.withName(`[${index}]`, () =>
|
|
420
|
+
ty.accept(this, overrides?.label ?? String(index))
|
|
421
|
+
) as FormFieldNode
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
type: "vector",
|
|
426
|
+
label,
|
|
427
|
+
displayLabel: formatLabel(label),
|
|
428
|
+
name,
|
|
429
|
+
component: "vector-list",
|
|
430
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
431
|
+
candidType: `vec ${ty.display?.() ?? ty.name}`,
|
|
432
|
+
itemField: itemFieldTemplate,
|
|
433
|
+
defaultValue: [],
|
|
434
|
+
schema: z.array(itemFieldTemplate.schema),
|
|
435
|
+
getItemDefault: () => cloneField(itemFieldTemplate).defaultValue,
|
|
436
|
+
createItemField,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
public visitRec<T>(
|
|
441
|
+
t: IDL.RecClass<T>,
|
|
442
|
+
ty: IDL.ConstructType<T>,
|
|
443
|
+
label: string
|
|
444
|
+
): FormFieldNode {
|
|
445
|
+
const name = this.currentName()
|
|
446
|
+
const typeName = ty.name || "RecursiveType"
|
|
447
|
+
let schema: z.ZodTypeAny
|
|
448
|
+
|
|
449
|
+
if (this.recCache.has(t)) {
|
|
450
|
+
return this.recCache.get(t)!
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (this.recursiveSchemas.has(typeName)) {
|
|
454
|
+
schema = this.recursiveSchemas.get(typeName)!
|
|
455
|
+
} else {
|
|
456
|
+
schema = z.lazy(() => (ty.accept(this, label) as FormFieldNode).schema)
|
|
457
|
+
this.recursiveSchemas.set(typeName, schema)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const node: FormFieldNode = {
|
|
461
|
+
type: "recursive",
|
|
462
|
+
label,
|
|
463
|
+
displayLabel: formatLabel(label),
|
|
464
|
+
name,
|
|
465
|
+
component: "recursive-lazy",
|
|
466
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
467
|
+
candidType: ty.name,
|
|
468
|
+
defaultValue: undefined,
|
|
469
|
+
schema,
|
|
470
|
+
typeName: ty.name,
|
|
471
|
+
extract: () =>
|
|
472
|
+
this.withName(name, () => ty.accept(this, label)) as FormFieldNode,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.recCache.set(t, node)
|
|
476
|
+
return node
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
public visitPrincipal(_t: IDL.PrincipalClass, label: string): FormFieldNode {
|
|
480
|
+
return this.primitive(
|
|
481
|
+
"principal",
|
|
482
|
+
label,
|
|
483
|
+
this.currentName(),
|
|
484
|
+
"principal",
|
|
485
|
+
"",
|
|
486
|
+
z.custom<Principal>(
|
|
487
|
+
(val) => {
|
|
488
|
+
if (val instanceof Principal) return true
|
|
489
|
+
if (typeof val === "string") {
|
|
490
|
+
try {
|
|
491
|
+
Principal.fromText(val)
|
|
492
|
+
return true
|
|
493
|
+
} catch {
|
|
494
|
+
return false
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return false
|
|
498
|
+
},
|
|
499
|
+
{ message: "Invalid Principal format" }
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
public visitText(_t: IDL.TextClass, label: string): FormFieldNode {
|
|
505
|
+
return this.primitive(
|
|
506
|
+
"text",
|
|
507
|
+
label,
|
|
508
|
+
this.currentName(),
|
|
509
|
+
"text",
|
|
510
|
+
"",
|
|
511
|
+
z.string().min(1, "Required")
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
public visitBool(_t: IDL.BoolClass, label: string): FormFieldNode {
|
|
516
|
+
return this.primitive(
|
|
517
|
+
"boolean",
|
|
518
|
+
label,
|
|
519
|
+
this.currentName(),
|
|
520
|
+
"bool",
|
|
521
|
+
false,
|
|
522
|
+
z.boolean()
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
public visitNull(_t: IDL.NullClass, label: string): FormFieldNode {
|
|
527
|
+
return this.primitive(
|
|
528
|
+
"null",
|
|
529
|
+
label,
|
|
530
|
+
this.currentName(),
|
|
531
|
+
"null",
|
|
532
|
+
null,
|
|
533
|
+
z.null()
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
public visitInt(_t: IDL.IntClass, label: string): FormFieldNode {
|
|
538
|
+
return this.primitive(
|
|
539
|
+
"number",
|
|
540
|
+
label,
|
|
541
|
+
this.currentName(),
|
|
542
|
+
"int",
|
|
543
|
+
"",
|
|
544
|
+
z
|
|
545
|
+
.string()
|
|
546
|
+
.min(1, "Required")
|
|
547
|
+
.regex(/^-?\d+$/, "Must be an integer")
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
public visitNat(_t: IDL.NatClass, label: string): FormFieldNode {
|
|
552
|
+
return this.primitive(
|
|
553
|
+
"number",
|
|
554
|
+
label,
|
|
555
|
+
this.currentName(),
|
|
556
|
+
"nat",
|
|
557
|
+
"",
|
|
558
|
+
z.string().regex(/^\d+$/, "Must be a positive number")
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public visitFloat(t: IDL.FloatClass, label: string): FormFieldNode {
|
|
563
|
+
return this.primitive(
|
|
564
|
+
"number",
|
|
565
|
+
label,
|
|
566
|
+
this.currentName(),
|
|
567
|
+
`float${t._bits}`,
|
|
568
|
+
"",
|
|
569
|
+
z
|
|
570
|
+
.string()
|
|
571
|
+
.min(1, "Required")
|
|
572
|
+
.refine((val) => !isNaN(Number(val)) && isFinite(Number(val)), {
|
|
573
|
+
message: "Must be a valid number",
|
|
574
|
+
})
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
public visitFixedInt(t: IDL.FixedIntClass, label: string): FormFieldNode {
|
|
579
|
+
return this.primitive(
|
|
580
|
+
"number",
|
|
581
|
+
label,
|
|
582
|
+
this.currentName(),
|
|
583
|
+
`int${t._bits}`,
|
|
584
|
+
"",
|
|
585
|
+
z
|
|
586
|
+
.string()
|
|
587
|
+
.min(1, "Required")
|
|
588
|
+
.regex(/^-?\d+$/, "Must be an integer")
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
public visitFixedNat(t: IDL.FixedNatClass, label: string): FormFieldNode {
|
|
593
|
+
return this.primitive(
|
|
594
|
+
"number",
|
|
595
|
+
label,
|
|
596
|
+
this.currentName(),
|
|
597
|
+
`nat${t._bits}`,
|
|
598
|
+
"",
|
|
599
|
+
z.string().regex(/^\d+$/, "Must be a positive number")
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
public visitType<T>(t: IDL.Type<T>, label: string): FormFieldNode {
|
|
604
|
+
return this.primitive(
|
|
605
|
+
"unknown",
|
|
606
|
+
label,
|
|
607
|
+
this.currentName(),
|
|
608
|
+
t.name ?? "unknown",
|
|
609
|
+
null,
|
|
610
|
+
z.any()
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private primitive<T extends FormFieldType>(
|
|
615
|
+
type: T,
|
|
616
|
+
label: string,
|
|
617
|
+
name: string,
|
|
618
|
+
candidType: string,
|
|
619
|
+
defaultValue: unknown,
|
|
620
|
+
schema: z.ZodTypeAny
|
|
621
|
+
): Extract<FormFieldNode, { type: T }> {
|
|
622
|
+
const component =
|
|
623
|
+
type === "blob"
|
|
624
|
+
? "blob-upload"
|
|
625
|
+
: type === "principal"
|
|
626
|
+
? "principal-input"
|
|
627
|
+
: type === "text"
|
|
628
|
+
? "text-input"
|
|
629
|
+
: type === "number"
|
|
630
|
+
? "number-input"
|
|
631
|
+
: type === "boolean"
|
|
632
|
+
? "boolean-checkbox"
|
|
633
|
+
: type === "null"
|
|
634
|
+
? "null-hidden"
|
|
635
|
+
: "unknown-fallback"
|
|
636
|
+
|
|
637
|
+
const renderHint =
|
|
638
|
+
type === "blob"
|
|
639
|
+
? FILE_RENDER_HINT
|
|
640
|
+
: type === "number"
|
|
641
|
+
? NUMBER_RENDER_HINT
|
|
642
|
+
: type === "boolean"
|
|
643
|
+
? CHECKBOX_RENDER_HINT
|
|
644
|
+
: TEXT_RENDER_HINT
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
type,
|
|
648
|
+
label,
|
|
649
|
+
displayLabel: formatLabel(label),
|
|
650
|
+
name,
|
|
651
|
+
component,
|
|
652
|
+
renderHint,
|
|
653
|
+
candidType,
|
|
654
|
+
defaultValue,
|
|
655
|
+
schema,
|
|
656
|
+
} as Extract<FormFieldNode, { type: T }>
|
|
657
|
+
}
|
|
658
|
+
}
|