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