@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,882 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { IDL } from "@icp-sdk/core/candid"
3
+ import { ArgumentFieldVisitor } from "./index"
4
+
5
+ describe("ArgumentFieldVisitor", () => {
6
+ const visitor = new ArgumentFieldVisitor()
7
+
8
+ // ════════════════════════════════════════════════════════════════════════
9
+ // Primitive Types
10
+ // ════════════════════════════════════════════════════════════════════════
11
+
12
+ describe("Primitive Types", () => {
13
+ it("should handle text type", () => {
14
+ const field = visitor.visitText(IDL.Text, "username")
15
+
16
+ expect(field.type).toBe("text")
17
+ expect(field.label).toBe("username")
18
+ expect(field.defaultValue).toBe("")
19
+ })
20
+
21
+ it("should handle bool type", () => {
22
+ const field = visitor.visitBool(IDL.Bool, "isActive")
23
+
24
+ expect(field.type).toBe("boolean")
25
+ expect(field.label).toBe("isActive")
26
+ expect(field.defaultValue).toBe(false)
27
+ })
28
+
29
+ it("should handle null type", () => {
30
+ const field = visitor.visitNull(IDL.Null, "empty")
31
+
32
+ expect(field.type).toBe("null")
33
+ expect(field.label).toBe("empty")
34
+ expect(field.defaultValue).toBe(null)
35
+ })
36
+
37
+ it("should handle principal type", () => {
38
+ const field = visitor.visitPrincipal(IDL.Principal, "caller")
39
+
40
+ expect(field.type).toBe("principal")
41
+ expect(field.label).toBe("caller")
42
+ expect(field.defaultValue).toBe("")
43
+ expect(field.minLength).toBe(7)
44
+ expect(field.maxLength).toBe(64)
45
+ })
46
+ })
47
+
48
+ // ════════════════════════════════════════════════════════════════════════
49
+ // Number Types
50
+ // ════════════════════════════════════════════════════════════════════════
51
+
52
+ describe("Number Types", () => {
53
+ it("should handle nat type", () => {
54
+ const field = visitor.visitNat(IDL.Nat, "amount")
55
+
56
+ expect(field.type).toBe("number")
57
+ expect(field.label).toBe("amount")
58
+ expect(field.candidType).toBe("nat")
59
+ expect(field.defaultValue).toBe("")
60
+ })
61
+
62
+ it("should handle int type", () => {
63
+ const field = visitor.visitInt(IDL.Int, "balance")
64
+
65
+ expect(field.type).toBe("number")
66
+ expect(field.candidType).toBe("int")
67
+ })
68
+
69
+ it("should handle nat8 type", () => {
70
+ const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
71
+
72
+ expect(field.type).toBe("number")
73
+ expect(field.candidType).toBe("nat8")
74
+ })
75
+
76
+ it("should handle nat64 type", () => {
77
+ const field = visitor.visitFixedNat(
78
+ IDL.Nat64 as IDL.FixedNatClass,
79
+ "timestamp"
80
+ )
81
+
82
+ expect(field.type).toBe("number")
83
+ expect(field.candidType).toBe("nat64")
84
+ })
85
+
86
+ it("should handle int32 type", () => {
87
+ const field = visitor.visitFixedInt(
88
+ IDL.Int32 as IDL.FixedIntClass,
89
+ "count"
90
+ )
91
+
92
+ expect(field.type).toBe("number")
93
+ expect(field.candidType).toBe("int32")
94
+ })
95
+
96
+ it("should handle float64 type", () => {
97
+ const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "price")
98
+
99
+ expect(field.type).toBe("number")
100
+ expect(field.candidType).toBe("float")
101
+ })
102
+ })
103
+
104
+ // ════════════════════════════════════════════════════════════════════════
105
+ // Compound Types
106
+ // ════════════════════════════════════════════════════════════════════════
107
+
108
+ describe("Record Types", () => {
109
+ it("should handle simple record", () => {
110
+ const recordType = IDL.Record({
111
+ name: IDL.Text,
112
+ age: IDL.Nat,
113
+ })
114
+ const field = visitor.visitRecord(
115
+ recordType,
116
+ [
117
+ ["name", IDL.Text],
118
+ ["age", IDL.Nat],
119
+ ],
120
+ "person"
121
+ )
122
+
123
+ expect(field.type).toBe("record")
124
+ expect(field.label).toBe("person")
125
+ expect(field.fields).toHaveLength(2)
126
+
127
+ const nameField = field.fields.find((f) => f.label === "name")
128
+ if (!nameField || nameField.type !== "text") {
129
+ throw new Error("Name field not found or not text")
130
+ }
131
+ expect(nameField.type).toBe("text")
132
+ expect(nameField.defaultValue).toBe("")
133
+
134
+ const ageField = field.fields.find((f) => f.label === "age")
135
+ if (!ageField || ageField.type !== "number") {
136
+ throw new Error("Age field not found or not number")
137
+ }
138
+ expect(ageField.type).toBe("number")
139
+ expect(ageField.candidType).toBe("nat")
140
+
141
+ expect(field.defaultValues).toEqual({
142
+ name: "",
143
+ age: "",
144
+ })
145
+ })
146
+
147
+ it("should handle nested record", () => {
148
+ const addressType = IDL.Record({
149
+ street: IDL.Text,
150
+ city: IDL.Text,
151
+ })
152
+ const personType = IDL.Record({
153
+ name: IDL.Text,
154
+ address: addressType,
155
+ })
156
+ const field = visitor.visitRecord(
157
+ personType,
158
+ [
159
+ ["name", IDL.Text],
160
+ ["address", addressType],
161
+ ],
162
+ "user"
163
+ )
164
+
165
+ expect(field.type).toBe("record")
166
+ expect(field.fields).toHaveLength(2)
167
+
168
+ const addressField = field.fields.find((f) => f.label === "address")
169
+ if (!addressField || addressField.type !== "record") {
170
+ throw new Error("Address field not found or not record")
171
+ }
172
+ expect(addressField.type).toBe("record")
173
+ expect(addressField.fields).toHaveLength(2)
174
+
175
+ expect(field.defaultValues).toEqual({
176
+ name: "",
177
+ address: {
178
+ street: "",
179
+ city: "",
180
+ },
181
+ })
182
+ })
183
+
184
+ it("should handle ICRC-1 transfer record", () => {
185
+ const transferType = IDL.Record({
186
+ to: IDL.Record({
187
+ owner: IDL.Principal,
188
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
189
+ }),
190
+ amount: IDL.Nat,
191
+ fee: IDL.Opt(IDL.Nat),
192
+ memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
193
+ created_at_time: IDL.Opt(IDL.Nat64),
194
+ })
195
+ const field = visitor.visitRecord(
196
+ transferType,
197
+ [
198
+ [
199
+ "to",
200
+ IDL.Record({
201
+ owner: IDL.Principal,
202
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
203
+ }),
204
+ ],
205
+ ["amount", IDL.Nat],
206
+ ["fee", IDL.Opt(IDL.Nat)],
207
+ ["memo", IDL.Opt(IDL.Vec(IDL.Nat8))],
208
+ ["created_at_time", IDL.Opt(IDL.Nat64)],
209
+ ],
210
+ "transfer"
211
+ )
212
+
213
+ expect(field.type).toBe("record")
214
+ expect(field.fields).toHaveLength(5)
215
+
216
+ // Check 'to' field
217
+ const toField = field.fields.find((f) => f.label === "to")
218
+ if (!toField || toField.type !== "record") {
219
+ throw new Error("To field not found or not record")
220
+ }
221
+ expect(toField.type).toBe("record")
222
+ expect(toField.fields).toHaveLength(2)
223
+
224
+ // Check 'amount' field
225
+ const amountField = field.fields.find((f) => f.label === "amount")
226
+ if (!amountField || amountField.type !== "number") {
227
+ throw new Error("Amount field not found or not number")
228
+ }
229
+ expect(amountField.type).toBe("number")
230
+ expect(amountField.candidType).toBe("nat")
231
+
232
+ // Check optional 'fee' field
233
+ const feeField = field.fields.find((f) => f.label === "fee")
234
+ if (!feeField || feeField.type !== "optional") {
235
+ throw new Error("Fee field not found or not optional")
236
+ }
237
+ expect(feeField.type).toBe("optional")
238
+ expect(feeField.innerField.type).toBe("number")
239
+ })
240
+ })
241
+
242
+ describe("Variant Types", () => {
243
+ it("should handle simple variant", () => {
244
+ const statusType = IDL.Variant({
245
+ Active: IDL.Null,
246
+ Inactive: IDL.Null,
247
+ Pending: IDL.Null,
248
+ })
249
+ const field = visitor.visitVariant(
250
+ statusType,
251
+ [
252
+ ["Inactive", IDL.Null],
253
+ ["Active", IDL.Null],
254
+ ["Pending", IDL.Null],
255
+ ],
256
+ "status"
257
+ )
258
+
259
+ expect(field.type).toBe("variant")
260
+ expect(field.label).toBe("status")
261
+ expect(field.options).toEqual(["Inactive", "Active", "Pending"])
262
+ expect(field.defaultOption).toBe("Inactive")
263
+ expect(field.fields).toHaveLength(3)
264
+
265
+ field.fields.forEach((f) => {
266
+ expect(f.type).toBe("null")
267
+ })
268
+ })
269
+
270
+ it("should handle variant with payloads", () => {
271
+ const actionType = IDL.Variant({
272
+ Transfer: IDL.Record({
273
+ to: IDL.Principal,
274
+ amount: IDL.Nat,
275
+ }),
276
+ Approve: IDL.Record({
277
+ spender: IDL.Principal,
278
+ amount: IDL.Nat,
279
+ }),
280
+ Burn: IDL.Nat,
281
+ })
282
+ const field = visitor.visitVariant(
283
+ actionType,
284
+ [
285
+ [
286
+ "Approve",
287
+ IDL.Record({
288
+ spender: IDL.Principal,
289
+ amount: IDL.Nat,
290
+ }),
291
+ ],
292
+ ["Burn", IDL.Nat],
293
+ [
294
+ "Transfer",
295
+ IDL.Record({
296
+ to: IDL.Principal,
297
+ amount: IDL.Nat,
298
+ }),
299
+ ],
300
+ ],
301
+ "action"
302
+ )
303
+
304
+ expect(field.type).toBe("variant")
305
+ expect(field.options).toEqual(["Approve", "Burn", "Transfer"]) // Sorted order
306
+
307
+ const transferField = field.fields.find((f) => f.label === "Transfer")
308
+ if (!transferField || transferField.type !== "record") {
309
+ throw new Error("Transfer field not found or not record")
310
+ }
311
+ expect(transferField.type).toBe("record")
312
+ expect(transferField.fields).toHaveLength(2)
313
+
314
+ const burnField = field.fields.find((f) => f.label === "Burn")
315
+ if (!burnField || burnField.type !== "number") {
316
+ throw new Error("Burn field not found or not number")
317
+ }
318
+ expect(burnField.type).toBe("number")
319
+ })
320
+
321
+ it("should handle Result variant (Ok/Err)", () => {
322
+ const resultType = IDL.Variant({
323
+ Ok: IDL.Nat,
324
+ Err: IDL.Text,
325
+ })
326
+ const field = visitor.visitVariant(
327
+ resultType,
328
+ [
329
+ ["Ok", IDL.Nat],
330
+ ["Err", IDL.Text],
331
+ ],
332
+ "result"
333
+ )
334
+
335
+ expect(field.type).toBe("variant")
336
+ expect(field.options).toContain("Ok")
337
+ expect(field.options).toContain("Err")
338
+
339
+ const okField = field.fields.find((f) => f.label === "Ok")
340
+ if (!okField || okField.type !== "number") {
341
+ throw new Error("Ok field not found or not number")
342
+ }
343
+ expect(okField.type).toBe("number")
344
+
345
+ const errField = field.fields.find((f) => f.label === "Err")
346
+ if (!errField || errField.type !== "text") {
347
+ throw new Error("Err field not found or not text")
348
+ }
349
+ expect(errField.type).toBe("text")
350
+ })
351
+ })
352
+
353
+ describe("Tuple Types", () => {
354
+ it("should handle simple tuple", () => {
355
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
356
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
357
+
358
+ expect(field.type).toBe("tuple")
359
+ expect(field.label).toBe("pair")
360
+ expect(field.fields).toHaveLength(2)
361
+ expect(field.fields[0].type).toBe("text")
362
+ expect(field.fields[1].type).toBe("number")
363
+ expect(field.defaultValues).toEqual(["", ""])
364
+ })
365
+
366
+ it("triple tuple", () => {
367
+ const tupleType = IDL.Tuple(IDL.Principal, IDL.Nat, IDL.Bool)
368
+ const field = visitor.visitTuple(
369
+ tupleType,
370
+ [IDL.Principal, IDL.Nat, IDL.Bool],
371
+ "triple"
372
+ )
373
+
374
+ expect(field.type).toBe("tuple")
375
+ expect(field.fields).toHaveLength(3)
376
+ expect(field.fields[0].type).toBe("principal")
377
+ expect(field.fields[1].type).toBe("number")
378
+ expect(field.fields[2].type).toBe("boolean")
379
+ expect(field.defaultValues).toEqual(["", "", false])
380
+ })
381
+ })
382
+
383
+ describe("Optional Types", () => {
384
+ it("should handle optional primitive", () => {
385
+ const optType = IDL.Opt(IDL.Text)
386
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
387
+
388
+ expect(field.type).toBe("optional")
389
+ expect(field.label).toBe("nickname")
390
+ expect(field.defaultValue).toBe(null)
391
+ expect(field.innerField.type).toBe("text")
392
+ })
393
+
394
+ it("should handle optional record", () => {
395
+ const recType = IDL.Record({
396
+ name: IDL.Text,
397
+ value: IDL.Nat,
398
+ })
399
+ const optType = IDL.Opt(recType)
400
+ const field = visitor.visitOpt(optType, recType, "metadata")
401
+
402
+ expect(field.type).toBe("optional")
403
+ expect(field.innerField.type).toBe("record")
404
+ expect(field.innerField.type).toBe("record")
405
+ const inner = field.innerField
406
+ if (inner.type === "record") {
407
+ expect(inner.fields).toHaveLength(2)
408
+ } else {
409
+ throw new Error("Inner field is not record")
410
+ }
411
+ })
412
+
413
+ it("should handle nested optional", () => {
414
+ const innerOpt = IDL.Opt(IDL.Nat)
415
+ const optType = IDL.Opt(innerOpt)
416
+ const field = visitor.visitOpt(optType, innerOpt, "maybeNumber")
417
+
418
+ expect(field.type).toBe("optional")
419
+ expect(field.innerField.type).toBe("optional")
420
+ expect(field.innerField.type).toBe("optional")
421
+ const inner = field.innerField
422
+ if (inner.type === "optional") {
423
+ expect(inner.innerField.type).toBe("number")
424
+ } else {
425
+ throw new Error("Inner field is not optional")
426
+ }
427
+ })
428
+ })
429
+
430
+ describe("Vector Types", () => {
431
+ it("should handle vector of primitives", () => {
432
+ const vecType = IDL.Vec(IDL.Text)
433
+ const field = visitor.visitVec(vecType, IDL.Text, "tags")
434
+
435
+ expect(field.type).toBe("vector")
436
+ expect(field.label).toBe("tags")
437
+ expect(field.defaultValue).toEqual([])
438
+ expect(field.itemField.type).toBe("text")
439
+ })
440
+
441
+ it("should handle vector of records", () => {
442
+ const recType = IDL.Record({
443
+ id: IDL.Nat,
444
+ name: IDL.Text,
445
+ })
446
+ const vecType = IDL.Vec(recType)
447
+ const field = visitor.visitVec(vecType, recType, "items")
448
+
449
+ expect(field.type).toBe("vector")
450
+ expect(field.itemField.type).toBe("record")
451
+ const item = field.itemField
452
+ if (item.type === "record") {
453
+ expect(item.fields).toHaveLength(2)
454
+ } else {
455
+ throw new Error("Item field is not record")
456
+ }
457
+ })
458
+
459
+ it("blob (vec nat8)", () => {
460
+ const blobType = IDL.Vec(IDL.Nat8)
461
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
462
+
463
+ expect(field.type).toBe("blob")
464
+ expect(field.label).toBe("data")
465
+ expect(field.defaultValue).toBe("")
466
+ })
467
+
468
+ it("should handle nested vectors", () => {
469
+ const innerVec = IDL.Vec(IDL.Nat)
470
+ const nestedVecType = IDL.Vec(innerVec)
471
+ const field = visitor.visitVec(nestedVecType, innerVec, "matrix")
472
+
473
+ expect(field.type).toBe("vector")
474
+ expect(field.itemField.type).toBe("vector")
475
+ const item = field.itemField
476
+ if (item.type === "vector") {
477
+ expect(item.itemField.type).toBe("number")
478
+ } else {
479
+ throw new Error("Item field is not vector")
480
+ }
481
+ })
482
+ })
483
+
484
+ describe("Recursive Types", () => {
485
+ it("should handle recursive type (tree)", () => {
486
+ const Tree = IDL.Rec()
487
+ Tree.fill(
488
+ IDL.Variant({
489
+ Leaf: IDL.Nat,
490
+ Node: IDL.Record({
491
+ left: Tree,
492
+ right: Tree,
493
+ }),
494
+ })
495
+ )
496
+
497
+ const field = visitor.visitRec(
498
+ Tree,
499
+ IDL.Variant({
500
+ Leaf: IDL.Nat,
501
+ Node: IDL.Record({
502
+ left: Tree,
503
+ right: Tree,
504
+ }),
505
+ }),
506
+ "tree"
507
+ )
508
+
509
+ expect(field.type).toBe("recursive")
510
+ expect(field.label).toBe("tree")
511
+ expect(typeof field.extract).toBe("function")
512
+
513
+ // Extract should return a variant
514
+ const extracted = field.extract()
515
+ if (extracted.type !== "variant") {
516
+ throw new Error("Extracted field is not variant")
517
+ }
518
+ expect(extracted.type).toBe("variant")
519
+ expect(extracted.options).toContain("Leaf")
520
+ expect(extracted.options).toContain("Node")
521
+ })
522
+
523
+ it("should handle recursive linked list", () => {
524
+ const List = IDL.Rec()
525
+ List.fill(
526
+ IDL.Variant({
527
+ Nil: IDL.Null,
528
+ Cons: IDL.Record({
529
+ head: IDL.Nat,
530
+ tail: List,
531
+ }),
532
+ })
533
+ )
534
+
535
+ const field = visitor.visitRec(
536
+ List,
537
+ IDL.Variant({
538
+ Nil: IDL.Null,
539
+ Cons: IDL.Record({
540
+ head: IDL.Nat,
541
+ tail: List,
542
+ }),
543
+ }),
544
+ "list"
545
+ )
546
+
547
+ expect(field.type).toBe("recursive")
548
+
549
+ const extracted = field.extract()
550
+ if (extracted.type !== "variant") {
551
+ throw new Error("Extracted field is not variant")
552
+ }
553
+ expect(extracted.type).toBe("variant")
554
+ expect(extracted.options).toEqual(["Nil", "Cons"])
555
+
556
+ const consField = extracted.fields.find((f) => f.label === "Cons")
557
+ if (!consField || consField.type !== "record") {
558
+ throw new Error("Cons field not found or not record")
559
+ }
560
+ expect(consField.type).toBe("record")
561
+ expect(consField.fields).toHaveLength(2)
562
+ })
563
+ })
564
+
565
+ // ════════════════════════════════════════════════════════════════════════
566
+ // Function Types
567
+ // ════════════════════════════════════════════════════════════════════════
568
+
569
+ describe("Function Types", () => {
570
+ it("should handle query function", () => {
571
+ const funcType = IDL.Func([IDL.Text], [IDL.Opt(IDL.Text)], ["query"])
572
+ const meta = visitor.visitFunc(funcType, "lookup")
573
+
574
+ expect(meta.functionType).toBe("query")
575
+ expect(meta.functionName).toBe("lookup")
576
+ expect(meta.fields).toHaveLength(1)
577
+ expect(meta.fields[0].type).toBe("text")
578
+ expect(meta.defaultValues).toEqual([""])
579
+ })
580
+
581
+ it("should handle update function", () => {
582
+ const funcType = IDL.Func(
583
+ [
584
+ IDL.Record({
585
+ to: IDL.Principal,
586
+ amount: IDL.Nat,
587
+ }),
588
+ ],
589
+ [
590
+ IDL.Variant({
591
+ Ok: IDL.Nat,
592
+ Err: IDL.Text,
593
+ }),
594
+ ],
595
+ []
596
+ )
597
+ const meta = visitor.visitFunc(funcType, "transfer")
598
+
599
+ expect(meta.functionType).toBe("update")
600
+ expect(meta.functionName).toBe("transfer")
601
+ expect(meta.fields).toHaveLength(1)
602
+ expect(meta.fields[0].type).toBe("record")
603
+
604
+ const recordField = meta.fields[0]
605
+ if (recordField.type !== "record") {
606
+ throw new Error("Expected record field")
607
+ }
608
+ expect(recordField.fields).toHaveLength(2)
609
+ })
610
+
611
+ it("should handle function with multiple arguments", () => {
612
+ const funcType = IDL.Func(
613
+ [IDL.Principal, IDL.Nat, IDL.Opt(IDL.Vec(IDL.Nat8))],
614
+ [IDL.Bool],
615
+ []
616
+ )
617
+ const meta = visitor.visitFunc(funcType, "authorize")
618
+
619
+ expect(meta.fields).toHaveLength(3)
620
+ expect(meta.fields[0].type).toBe("principal")
621
+ expect(meta.fields[1].type).toBe("number")
622
+ expect(meta.fields[2].type).toBe("optional")
623
+ })
624
+
625
+ it("should handle function with no arguments", () => {
626
+ const funcType = IDL.Func([], [IDL.Nat], ["query"])
627
+ const meta = visitor.visitFunc(funcType, "getBalance")
628
+
629
+ expect(meta.functionType).toBe("query")
630
+ expect(meta.fields).toHaveLength(0)
631
+ expect(meta.defaultValues).toEqual([])
632
+ })
633
+ })
634
+
635
+ // ════════════════════════════════════════════════════════════════════════
636
+ // Service Types
637
+ // ════════════════════════════════════════════════════════════════════════
638
+
639
+ describe("Service Types", () => {
640
+ it("should handle complete service", () => {
641
+ const serviceType = IDL.Service({
642
+ get_balance: IDL.Func([IDL.Principal], [IDL.Nat], ["query"]),
643
+ transfer: IDL.Func(
644
+ [
645
+ IDL.Record({
646
+ to: IDL.Principal,
647
+ amount: IDL.Nat,
648
+ }),
649
+ ],
650
+ [
651
+ IDL.Variant({
652
+ Ok: IDL.Nat,
653
+ Err: IDL.Text,
654
+ }),
655
+ ],
656
+ []
657
+ ),
658
+ get_metadata: IDL.Func(
659
+ [],
660
+ [IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))],
661
+ ["query"]
662
+ ),
663
+ })
664
+
665
+ const serviceMeta = visitor.visitService(serviceType)
666
+
667
+ expect(Object.keys(serviceMeta)).toHaveLength(3)
668
+ expect(serviceMeta).toHaveProperty("get_balance")
669
+ expect(serviceMeta).toHaveProperty("transfer")
670
+ expect(serviceMeta).toHaveProperty("get_metadata")
671
+
672
+ // Check get_balance
673
+ const getBalanceMeta = serviceMeta["get_balance"]
674
+ expect(getBalanceMeta.functionType).toBe("query")
675
+ expect(getBalanceMeta.fields).toHaveLength(1)
676
+ expect(getBalanceMeta.fields[0].type).toBe("principal")
677
+
678
+ // Check transfer
679
+ const transferMeta = serviceMeta["transfer"]
680
+ expect(transferMeta.functionType).toBe("update")
681
+ expect(transferMeta.fields).toHaveLength(1)
682
+ expect(transferMeta.fields[0].type).toBe("record")
683
+
684
+ // Check get_metadata
685
+ const getMetadataMeta = serviceMeta["get_metadata"]
686
+ expect(getMetadataMeta.functionType).toBe("query")
687
+ expect(getMetadataMeta.fields).toHaveLength(0)
688
+ })
689
+ })
690
+
691
+ // ════════════════════════════════════════════════════════════════════════
692
+ // Path Generation
693
+ // ════════════════════════════════════════════════════════════════════════
694
+
695
+ describe("Path Generation", () => {
696
+ it("should generate correct paths for nested records", () => {
697
+ const funcType = IDL.Func(
698
+ [
699
+ IDL.Record({
700
+ user: IDL.Record({
701
+ name: IDL.Text,
702
+ age: IDL.Nat,
703
+ }),
704
+ active: IDL.Bool,
705
+ }),
706
+ ],
707
+ [],
708
+ []
709
+ )
710
+ const meta = visitor.visitFunc(funcType, "updateUser")
711
+
712
+ const argRecord = meta.fields[0]
713
+ if (argRecord.type !== "record") {
714
+ throw new Error("Expected record field")
715
+ }
716
+ expect(argRecord.path).toBe("[0]")
717
+
718
+ const userRecord = argRecord.fields.find((f) => f.label === "user")
719
+ if (!userRecord || userRecord.type !== "record") {
720
+ throw new Error("User record not found or not record")
721
+ }
722
+ expect(userRecord.path).toBe("[0].user")
723
+
724
+ const nameField = userRecord.fields.find((f) => f.label === "name")
725
+ if (!nameField || nameField.type !== "text") {
726
+ throw new Error("Name field not found or not text")
727
+ }
728
+ expect(nameField.path).toBe("[0].user.name")
729
+
730
+ const ageField = userRecord.fields.find((f) => f.label === "age")
731
+ if (!ageField || ageField.type !== "number") {
732
+ throw new Error("Age field not found or not number")
733
+ }
734
+ expect(ageField.path).toBe("[0].user.age")
735
+ })
736
+
737
+ it("should generate correct paths for vectors", () => {
738
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
739
+ const meta = visitor.visitFunc(funcType, "addTags")
740
+
741
+ const vecField = meta.fields[0]
742
+ if (vecField.type !== "vector") {
743
+ throw new Error("Expected vector field")
744
+ }
745
+ expect(vecField.path).toBe("[0]")
746
+ expect(vecField.itemField.path).toBe("[0][0]")
747
+ })
748
+ })
749
+
750
+ // ════════════════════════════════════════════════════════════════════════
751
+ // Complex Real-World Examples
752
+ // ════════════════════════════════════════════════════════════════════════
753
+
754
+ describe("Real-World Examples", () => {
755
+ it("should handle ICRC-2 approve", () => {
756
+ const ApproveArgs = IDL.Record({
757
+ fee: IDL.Opt(IDL.Nat),
758
+ memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
759
+ from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
760
+ created_at_time: IDL.Opt(IDL.Nat64),
761
+ amount: IDL.Nat,
762
+ expected_allowance: IDL.Opt(IDL.Nat),
763
+ expires_at: IDL.Opt(IDL.Nat64),
764
+ spender: IDL.Record({
765
+ owner: IDL.Principal,
766
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
767
+ }),
768
+ })
769
+
770
+ const field = visitor.visitRecord(
771
+ ApproveArgs,
772
+ [
773
+ ["fee", IDL.Opt(IDL.Nat)],
774
+ ["memo", IDL.Opt(IDL.Vec(IDL.Nat8))],
775
+ ["from_subaccount", IDL.Opt(IDL.Vec(IDL.Nat8))],
776
+ ["created_at_time", IDL.Opt(IDL.Nat64)],
777
+ ["amount", IDL.Nat],
778
+ ["expected_allowance", IDL.Opt(IDL.Nat)],
779
+ ["expires_at", IDL.Opt(IDL.Nat64)],
780
+ [
781
+ "spender",
782
+ IDL.Record({
783
+ owner: IDL.Principal,
784
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
785
+ }),
786
+ ],
787
+ ],
788
+ "approve"
789
+ )
790
+
791
+ expect(field.type).toBe("record")
792
+ expect(field.fields.length).toBeGreaterThan(5)
793
+
794
+ // Check spender field
795
+ const spenderField = field.fields.find((f) => f.label === "spender")
796
+ if (!spenderField || spenderField.type !== "record") {
797
+ throw new Error("Spender field not found or not record")
798
+ }
799
+ expect(spenderField.type).toBe("record")
800
+ expect(spenderField.fields).toHaveLength(2)
801
+
802
+ // Check amount field
803
+ const amountField = field.fields.find((f) => f.label === "amount")
804
+ if (!amountField || amountField.type !== "number") {
805
+ throw new Error("Amount field not found or not number")
806
+ }
807
+ expect(amountField.type).toBe("number")
808
+ expect(amountField.candidType).toBe("nat")
809
+ })
810
+
811
+ it("should handle SNS governance proposal", () => {
812
+ const ProposalType = IDL.Variant({
813
+ Motion: IDL.Record({
814
+ motion_text: IDL.Text,
815
+ }),
816
+ TransferSnsTreasuryFunds: IDL.Record({
817
+ from_treasury: IDL.Nat32,
818
+ to_principal: IDL.Opt(IDL.Principal),
819
+ to_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
820
+ memo: IDL.Opt(IDL.Nat64),
821
+ amount_e8s: IDL.Nat64,
822
+ }),
823
+ UpgradeSnsControlledCanister: IDL.Record({
824
+ new_canister_wasm: IDL.Vec(IDL.Nat8),
825
+ mode: IDL.Opt(IDL.Nat32),
826
+ canister_id: IDL.Opt(IDL.Principal),
827
+ canister_upgrade_arg: IDL.Opt(IDL.Vec(IDL.Nat8)),
828
+ }),
829
+ })
830
+
831
+ const field = visitor.visitVariant(
832
+ ProposalType,
833
+ [
834
+ ["Motion", IDL.Record({ motion_text: IDL.Text })],
835
+ [
836
+ "TransferSnsTreasuryFunds",
837
+ IDL.Record({
838
+ from_treasury: IDL.Nat32,
839
+ to_principal: IDL.Opt(IDL.Principal),
840
+ to_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
841
+ memo: IDL.Opt(IDL.Nat64),
842
+ amount_e8s: IDL.Nat64,
843
+ }),
844
+ ],
845
+ [
846
+ "UpgradeSnsControlledCanister",
847
+ IDL.Record({
848
+ new_canister_wasm: IDL.Vec(IDL.Nat8),
849
+ mode: IDL.Opt(IDL.Nat32),
850
+ canister_id: IDL.Opt(IDL.Principal),
851
+ canister_upgrade_arg: IDL.Opt(IDL.Vec(IDL.Nat8)),
852
+ }),
853
+ ],
854
+ ],
855
+ "proposal"
856
+ )
857
+
858
+ expect(field.type).toBe("variant")
859
+ expect(field.options).toContain("Motion")
860
+ expect(field.options).toContain("TransferSnsTreasuryFunds")
861
+ expect(field.options).toContain("UpgradeSnsControlledCanister")
862
+
863
+ // Check Motion variant
864
+ const motionField = field.fields.find((f) => f.label === "Motion")
865
+ if (!motionField || motionField.type !== "record") {
866
+ throw new Error("Motion field not found or not record")
867
+ }
868
+ expect(motionField.type).toBe("record")
869
+ expect(motionField.fields).toHaveLength(1)
870
+
871
+ // Check TransferSnsTreasuryFunds variant
872
+ const transferField = field.fields.find(
873
+ (f) => f.label === "TransferSnsTreasuryFunds"
874
+ )
875
+ if (!transferField || transferField.type !== "record") {
876
+ throw new Error("Transfer field not found or not record")
877
+ }
878
+ expect(transferField.type).toBe("record")
879
+ expect(transferField.fields.length).toBeGreaterThan(3)
880
+ })
881
+ })
882
+ })