@ic-reactor/candid 3.0.7-beta.2 → 3.0.8-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.
Files changed (53) hide show
  1. package/dist/display-reactor.d.ts +3 -2
  2. package/dist/display-reactor.d.ts.map +1 -1
  3. package/dist/display-reactor.js +6 -0
  4. package/dist/display-reactor.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/metadata-display-reactor.d.ts +73 -0
  10. package/dist/metadata-display-reactor.d.ts.map +1 -0
  11. package/dist/metadata-display-reactor.js +128 -0
  12. package/dist/metadata-display-reactor.js.map +1 -0
  13. package/dist/visitor/arguments/index.d.ts +69 -0
  14. package/dist/visitor/arguments/index.d.ts.map +1 -0
  15. package/dist/visitor/arguments/index.js +277 -0
  16. package/dist/visitor/arguments/index.js.map +1 -0
  17. package/dist/visitor/arguments/types.d.ts +92 -0
  18. package/dist/visitor/arguments/types.d.ts.map +1 -0
  19. package/dist/visitor/arguments/types.js +2 -0
  20. package/dist/visitor/arguments/types.js.map +1 -0
  21. package/dist/visitor/constants.d.ts +4 -0
  22. package/dist/visitor/constants.d.ts.map +1 -0
  23. package/dist/visitor/constants.js +61 -0
  24. package/dist/visitor/constants.js.map +1 -0
  25. package/dist/visitor/helpers.d.ts +30 -0
  26. package/dist/visitor/helpers.d.ts.map +1 -0
  27. package/dist/visitor/helpers.js +200 -0
  28. package/dist/visitor/helpers.js.map +1 -0
  29. package/dist/visitor/returns/index.d.ts +76 -0
  30. package/dist/visitor/returns/index.d.ts.map +1 -0
  31. package/dist/visitor/returns/index.js +425 -0
  32. package/dist/visitor/returns/index.js.map +1 -0
  33. package/dist/visitor/returns/types.d.ts +142 -0
  34. package/dist/visitor/returns/types.d.ts.map +1 -0
  35. package/dist/visitor/returns/types.js +2 -0
  36. package/dist/visitor/returns/types.js.map +1 -0
  37. package/dist/visitor/types.d.ts +6 -0
  38. package/dist/visitor/types.d.ts.map +1 -0
  39. package/dist/visitor/types.js +3 -0
  40. package/dist/visitor/types.js.map +1 -0
  41. package/package.json +3 -2
  42. package/src/display-reactor.ts +10 -2
  43. package/src/index.ts +1 -0
  44. package/src/metadata-display-reactor.ts +184 -0
  45. package/src/visitor/arguments/index.test.ts +882 -0
  46. package/src/visitor/arguments/index.ts +405 -0
  47. package/src/visitor/arguments/types.ts +168 -0
  48. package/src/visitor/constants.ts +62 -0
  49. package/src/visitor/helpers.ts +221 -0
  50. package/src/visitor/returns/index.test.ts +2027 -0
  51. package/src/visitor/returns/index.ts +545 -0
  52. package/src/visitor/returns/types.ts +271 -0
  53. package/src/visitor/types.ts +29 -0
@@ -0,0 +1,545 @@
1
+ import { isQuery } from "../helpers"
2
+ import { checkTextFormat, checkNumberFormat } from "../constants"
3
+ import { IDL } from "../types"
4
+ import type {
5
+ ResultField,
6
+ ResultFieldWithValue,
7
+ RecordResultField,
8
+ VariantResultField,
9
+ TupleResultField,
10
+ OptionalResultField,
11
+ VectorResultField,
12
+ BlobResultField,
13
+ RecursiveResultField,
14
+ PrincipalResultField,
15
+ NumberResultField,
16
+ TextResultField,
17
+ BooleanResultField,
18
+ NullResultField,
19
+ UnknownResultField,
20
+ MethodResultMeta,
21
+ ServiceResultMeta,
22
+ ResolvedMethodResult,
23
+ } from "./types"
24
+ import { DisplayCodecVisitor } from "@ic-reactor/core"
25
+ import type { BaseActor, FunctionName, FunctionType } from "@ic-reactor/core"
26
+
27
+ export * from "./types"
28
+
29
+ /**
30
+ * ResultFieldVisitor generates metadata for displaying method results from Candid IDL types.
31
+ *
32
+ * ## Design Principles
33
+ *
34
+ * 1. **Works with raw IDL types BEFORE transformation** - generates metadata at initialization
35
+ * 2. **No value dependencies** - metadata describes structure, not specific values
36
+ * 3. **Describes display format** - includes hints for how values will appear after transformation
37
+ * 4. **Efficient** - single traversal, reusable metadata
38
+ * 5. **Resolvable** - metadata can be combined with values at runtime via .resolve(val)
39
+ *
40
+ * ## Key Insight: Metadata vs Values
41
+ *
42
+ * The visitor generates a "schema" that describes:
43
+ * - What type each field is in Candid (nat, Principal, opt, etc.)
44
+ * - What it becomes after display transformation (string, null, etc.)
45
+ * - How it should be formatted (timestamp, cycle, hex, etc.)
46
+ *
47
+ * Values are NOT passed during traversal. Instead, the generated schema
48
+ * is used at render time to properly display transformed values.
49
+ *
50
+ * ## Display Transformations (applied by DisplayCodecVisitor)
51
+ *
52
+ * | Candid Type | Display Type | Notes |
53
+ * |-------------|--------------|-------|
54
+ * | nat, int, nat64, int64 | string | BigInt → string |
55
+ * | Principal | string | Principal.toText() |
56
+ * | opt T | T \| null | [value] → value, [] → null |
57
+ * | vec nat8 (blob) | string | Uint8Array → hex string |
58
+ * | variant { Ok, Err } | unwrapped | { Ok: val } → val (or throws on Err) |
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const visitor = new ResultFieldVisitor()
63
+ * const serviceMeta = service.accept(visitor, null)
64
+ *
65
+ * // Get method result metadata
66
+ * const methodMeta = serviceMeta["icrc1_balance_of"]
67
+ * // methodMeta.resultFields[0] = {
68
+ * // type: "number",
69
+ * // candidType: "nat",
70
+ * // displayType: "string",
71
+ * // numberFormat: "normal"
72
+ * // }
73
+ *
74
+ * // At render time, apply to transformed value:
75
+ * const transformedResult = "1000000000" // Already transformed by DisplayCodec
76
+ * renderField(methodMeta.resultFields[0], transformedResult)
77
+ * ```
78
+ */
79
+ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
80
+ string,
81
+ ResultField | MethodResultMeta<A> | ServiceResultMeta<A>
82
+ > {
83
+ private displayVisitor = new DisplayCodecVisitor()
84
+
85
+ // ════════════════════════════════════════════════════════════════════════
86
+ // Service & Function Level
87
+ // ════════════════════════════════════════════════════════════════════════
88
+
89
+ public visitService(t: IDL.ServiceClass): ServiceResultMeta<A> {
90
+ const result = {} as ServiceResultMeta<A>
91
+
92
+ for (const [functionName, func] of t._fields) {
93
+ result[functionName as FunctionName<A>] = func.accept(
94
+ this,
95
+ functionName
96
+ ) as MethodResultMeta<A>
97
+ }
98
+
99
+ return result
100
+ }
101
+
102
+ public visitFunc(
103
+ t: IDL.FuncClass,
104
+ functionName: FunctionName<A>
105
+ ): MethodResultMeta<A> {
106
+ const functionType: FunctionType = isQuery(t) ? "query" : "update"
107
+
108
+ const resultFields = t.retTypes.map((retType, index) =>
109
+ retType.accept(this, `__ret${index}`)
110
+ ) as ResultField[]
111
+
112
+ const generateMetadata = (data: unknown): ResolvedMethodResult<A> => {
113
+ const dataArray: unknown[] =
114
+ resultFields.length === 0
115
+ ? []
116
+ : resultFields.length === 1
117
+ ? [data]
118
+ : (data as unknown[])
119
+
120
+ const results = resultFields.map((field, index) =>
121
+ field.resolve(dataArray[index])
122
+ )
123
+
124
+ return {
125
+ functionType,
126
+ functionName,
127
+ results,
128
+ }
129
+ }
130
+
131
+ return {
132
+ functionType,
133
+ functionName,
134
+ resultFields,
135
+ returnCount: t.retTypes.length,
136
+ generateMetadata,
137
+ }
138
+ }
139
+
140
+ // ════════════════════════════════════════════════════════════════════════
141
+ // Compound Types
142
+ // ════════════════════════════════════════════════════════════════════════
143
+
144
+ public visitRecord(
145
+ _t: IDL.RecordClass,
146
+ fields_: Array<[string, IDL.Type]>,
147
+ label: string
148
+ ): RecordResultField {
149
+ const fields = fields_.map(
150
+ ([key, type]) => type.accept(this, key) as ResultField
151
+ )
152
+
153
+ const field: RecordResultField = {
154
+ type: "record",
155
+ label,
156
+ candidType: "record",
157
+ displayType: "object",
158
+ fields,
159
+ resolve(value: unknown): ResultFieldWithValue {
160
+ const record = value as Record<string, unknown> | null | undefined
161
+ if (record == null) {
162
+ return { field, value: null, raw: value }
163
+ }
164
+
165
+ const resolvedFields: Record<string, ResultFieldWithValue> = {}
166
+ for (const f of fields) {
167
+ resolvedFields[f.label] = f.resolve(record[f.label])
168
+ }
169
+
170
+ return { field, value: resolvedFields, raw: value }
171
+ },
172
+ }
173
+ return field
174
+ }
175
+
176
+ public visitVariant(
177
+ _t: IDL.VariantClass,
178
+ fields_: Array<[string, IDL.Type]>,
179
+ label: string
180
+ ): VariantResultField {
181
+ const options: string[] = []
182
+ const optionFields: ResultField[] = []
183
+
184
+ for (const [key, type] of fields_) {
185
+ options.push(key)
186
+ optionFields.push(type.accept(this, key) as ResultField)
187
+ }
188
+
189
+ // Detect if this is a Result type (has Ok and Err options)
190
+ const isResult = options.includes("Ok") && options.includes("Err")
191
+ const displayType = isResult ? "result" : "variant"
192
+
193
+ const field: VariantResultField = {
194
+ type: "variant",
195
+ label,
196
+ candidType: "variant",
197
+ displayType,
198
+ options,
199
+ optionFields,
200
+ resolve(value: unknown): ResultFieldWithValue {
201
+ if (value == null) {
202
+ return { field, value: null, raw: value }
203
+ }
204
+
205
+ const variant = value as Record<string, unknown>
206
+ const optionsInValue = Object.keys(variant)
207
+ const activeOption = optionsInValue.find((opt) => options.includes(opt))
208
+
209
+ if (!activeOption) {
210
+ return { field, value: null, raw: value }
211
+ }
212
+
213
+ const activeValue = variant[activeOption]
214
+ const optionIndex = options.indexOf(activeOption)
215
+ const optionField = optionFields[optionIndex]
216
+
217
+ const specificField: VariantResultField = {
218
+ ...field,
219
+ options,
220
+ optionFields: [optionField],
221
+ }
222
+
223
+ return {
224
+ field: specificField,
225
+ value: {
226
+ option: activeOption,
227
+ value: optionField?.resolve(activeValue) ?? {
228
+ field: optionField,
229
+ value: activeValue,
230
+ raw: activeValue,
231
+ },
232
+ },
233
+ raw: value,
234
+ }
235
+ },
236
+ }
237
+
238
+ return field
239
+ }
240
+
241
+ public visitTuple<T extends IDL.Type[]>(
242
+ _t: IDL.TupleClass<T>,
243
+ components: IDL.Type[],
244
+ label: string
245
+ ): TupleResultField {
246
+ const fields = components.map(
247
+ (type, index) => type.accept(this, `_${index}`) as ResultField
248
+ )
249
+
250
+ const field: TupleResultField = {
251
+ type: "tuple",
252
+ label,
253
+ candidType: "tuple",
254
+ displayType: "array",
255
+ fields,
256
+ resolve(value: unknown): ResultFieldWithValue {
257
+ const tuple = value as unknown[] | null | undefined
258
+ if (tuple == null) {
259
+ return { field, value: null, raw: value }
260
+ }
261
+
262
+ const resolvedItems = fields.map((f, index) => f.resolve(tuple[index]))
263
+ return { field, value: resolvedItems, raw: value }
264
+ },
265
+ }
266
+
267
+ return field
268
+ }
269
+
270
+ public visitOpt<T>(
271
+ _t: IDL.OptClass<T>,
272
+ ty: IDL.Type<T>,
273
+ label: string
274
+ ): OptionalResultField {
275
+ const innerField = ty.accept(this, label) as ResultField
276
+
277
+ const field: OptionalResultField = {
278
+ type: "optional",
279
+ label,
280
+ candidType: "opt",
281
+ displayType: "nullable",
282
+ innerField,
283
+ resolve(value: unknown): ResultFieldWithValue {
284
+ const opt = value as [unknown] | [] | null | undefined
285
+ if (opt == null || opt.length === 0) {
286
+ return { field, value: null, raw: value }
287
+ }
288
+ return { field, value: innerField.resolve(opt[0]), raw: value }
289
+ },
290
+ }
291
+
292
+ return field
293
+ }
294
+
295
+ public visitVec<T>(
296
+ _t: IDL.VecClass<T>,
297
+ ty: IDL.Type<T>,
298
+ label: string
299
+ ): VectorResultField | BlobResultField {
300
+ // Check if it's blob (vec nat8)
301
+ if (ty instanceof IDL.FixedNatClass && ty._bits === 8) {
302
+ const codec = _t.accept(this.displayVisitor, null) as any
303
+ const blobField: BlobResultField = {
304
+ type: "blob",
305
+ label,
306
+ candidType: "blob",
307
+ displayType: "string", // Transformed to hex string
308
+ displayHint: "hex",
309
+ resolve(value: unknown): ResultFieldWithValue {
310
+ return {
311
+ field: blobField,
312
+ value: codec.decode(value),
313
+ raw: value,
314
+ }
315
+ },
316
+ }
317
+ return blobField
318
+ }
319
+
320
+ const itemField = ty.accept(this, "item") as ResultField
321
+
322
+ const field: VectorResultField = {
323
+ type: "vector",
324
+ label,
325
+ candidType: "vec",
326
+ displayType: "array",
327
+ itemField,
328
+ resolve(value: unknown): ResultFieldWithValue {
329
+ const vec = value as unknown[] | null | undefined
330
+ if (vec == null) {
331
+ return { field, value: null, raw: value }
332
+ }
333
+
334
+ const resolvedItems = vec.map((item) => itemField.resolve(item))
335
+ return { field, value: resolvedItems, raw: value }
336
+ },
337
+ }
338
+
339
+ return field
340
+ }
341
+
342
+ public visitRec<T>(
343
+ t: IDL.RecClass<T>,
344
+ ty: IDL.ConstructType<T>,
345
+ label: string
346
+ ): RecursiveResultField {
347
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
348
+ const self = this
349
+
350
+ const field: RecursiveResultField = {
351
+ type: "recursive",
352
+ label,
353
+ candidType: "rec",
354
+ displayType: "recursive",
355
+ typeName: t.name,
356
+ // Lazy extraction to prevent infinite loops
357
+ extract: () => ty.accept(self, label) as ResultField,
358
+ resolve(value: unknown): ResultFieldWithValue {
359
+ // Extract the inner field and resolve with it
360
+ const innerField = field.extract()
361
+ return innerField.resolve(value)
362
+ },
363
+ }
364
+
365
+ return field
366
+ }
367
+
368
+ // ════════════════════════════════════════════════════════════════════════
369
+ // Primitive Types
370
+ // ════════════════════════════════════════════════════════════════════════
371
+
372
+ public visitPrincipal(
373
+ t: IDL.PrincipalClass,
374
+ label: string
375
+ ): PrincipalResultField {
376
+ const codec = t.accept(this.displayVisitor, null) as any
377
+ const field: PrincipalResultField = {
378
+ type: "principal",
379
+ label,
380
+ candidType: "principal",
381
+ displayType: "string", // Principal.toText()
382
+ textFormat: checkTextFormat(label),
383
+ resolve(value: unknown): ResultFieldWithValue {
384
+ return {
385
+ field,
386
+ value: codec.decode(value),
387
+ raw: value,
388
+ }
389
+ },
390
+ }
391
+
392
+ return field
393
+ }
394
+
395
+ public visitText(t: IDL.TextClass, label: string): TextResultField {
396
+ const codec = t.accept(this.displayVisitor, null) as any
397
+ const field: TextResultField = {
398
+ type: "text",
399
+ label,
400
+ candidType: "text",
401
+ displayType: "string",
402
+ textFormat: checkTextFormat(label),
403
+ resolve(value: unknown): ResultFieldWithValue {
404
+ return { field, value: codec.decode(value), raw: value }
405
+ },
406
+ }
407
+
408
+ return field
409
+ }
410
+
411
+ public visitBool(t: IDL.BoolClass, label: string): BooleanResultField {
412
+ const codec = t.accept(this.displayVisitor, null) as any
413
+ const field: BooleanResultField = {
414
+ type: "boolean",
415
+ label,
416
+ candidType: "bool",
417
+ displayType: "boolean",
418
+ resolve(value: unknown): ResultFieldWithValue {
419
+ return { field, value: codec.decode(value), raw: value }
420
+ },
421
+ }
422
+
423
+ return field
424
+ }
425
+
426
+ public visitNull(t: IDL.NullClass, label: string): NullResultField {
427
+ const codec = t.accept(this.displayVisitor, null) as any
428
+ const field: NullResultField = {
429
+ type: "null",
430
+ label,
431
+ candidType: "null",
432
+ displayType: "null",
433
+ resolve(value: unknown): ResultFieldWithValue {
434
+ return { field, value: codec.decode(value), raw: value }
435
+ },
436
+ }
437
+
438
+ return field
439
+ }
440
+
441
+ // Numbers
442
+ public visitInt(t: IDL.IntClass, label: string): NumberResultField {
443
+ const codec = t.accept(this.displayVisitor, null) as any
444
+ const field: NumberResultField = {
445
+ type: "number",
446
+ label,
447
+ candidType: "int",
448
+ displayType: "string", // BigInt → string
449
+ numberFormat: checkNumberFormat(label),
450
+ resolve(value: unknown): ResultFieldWithValue {
451
+ return { field, value: codec.decode(value), raw: value }
452
+ },
453
+ }
454
+
455
+ return field
456
+ }
457
+
458
+ public visitNat(t: IDL.NatClass, label: string): NumberResultField {
459
+ const codec = t.accept(this.displayVisitor, null) as any
460
+ const field: NumberResultField = {
461
+ type: "number",
462
+ label,
463
+ candidType: "nat",
464
+ displayType: "string", // BigInt → string
465
+ numberFormat: checkNumberFormat(label),
466
+ resolve(value: unknown): ResultFieldWithValue {
467
+ return { field, value: codec.decode(value), raw: value }
468
+ },
469
+ }
470
+
471
+ return field
472
+ }
473
+
474
+ public visitFloat(t: IDL.FloatClass, label: string): NumberResultField {
475
+ const codec = t.accept(this.displayVisitor, null) as any
476
+ const field: NumberResultField = {
477
+ type: "number",
478
+ label,
479
+ candidType: `float${t._bits}`,
480
+ displayType: "number", // Floats stay as numbers
481
+ numberFormat: checkNumberFormat(label),
482
+ resolve(value: unknown): ResultFieldWithValue {
483
+ return { field, value: codec.decode(value), raw: value }
484
+ },
485
+ }
486
+
487
+ return field
488
+ }
489
+
490
+ public visitFixedInt(t: IDL.FixedIntClass, label: string): NumberResultField {
491
+ const bits = t._bits
492
+ const codec = t.accept(this.displayVisitor, null) as any
493
+ const field: NumberResultField = {
494
+ type: "number",
495
+ label,
496
+ candidType: `int${bits}`,
497
+ displayType: bits <= 32 ? "number" : "string", // int64 → string
498
+ numberFormat: checkNumberFormat(label),
499
+ resolve(value: unknown): ResultFieldWithValue {
500
+ return {
501
+ field,
502
+ value: codec.decode(value),
503
+ raw: value,
504
+ }
505
+ },
506
+ }
507
+
508
+ return field
509
+ }
510
+
511
+ public visitFixedNat(t: IDL.FixedNatClass, label: string): NumberResultField {
512
+ const bits = t._bits
513
+ const codec = t.accept(this.displayVisitor, null) as any
514
+ const field: NumberResultField = {
515
+ type: "number",
516
+ label,
517
+ candidType: `nat${bits}`,
518
+ displayType: bits <= 32 ? "number" : "string", // nat64 → string
519
+ numberFormat: checkNumberFormat(label),
520
+ resolve(value: unknown): ResultFieldWithValue {
521
+ return {
522
+ field,
523
+ value: codec.decode(value),
524
+ raw: value,
525
+ }
526
+ },
527
+ }
528
+
529
+ return field
530
+ }
531
+
532
+ public visitType<T>(_t: IDL.Type<T>, label: string): UnknownResultField {
533
+ const field: UnknownResultField = {
534
+ type: "unknown",
535
+ label,
536
+ candidType: "unknown",
537
+ displayType: "unknown",
538
+ resolve(value: unknown): ResultFieldWithValue {
539
+ return { field, value, raw: value }
540
+ },
541
+ }
542
+
543
+ return field
544
+ }
545
+ }