@ic-reactor/candid 3.0.13-beta.0 → 3.0.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/visitor/arguments/helpers.d.ts +5 -5
- package/dist/visitor/arguments/helpers.d.ts.map +1 -1
- package/dist/visitor/arguments/helpers.js.map +1 -1
- package/dist/visitor/arguments/index.d.ts +12 -2
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +136 -12
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +112 -359
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/returns/types.d.ts +3 -4
- package/dist/visitor/returns/types.d.ts.map +1 -1
- package/dist/visitor/types.d.ts +14 -0
- package/dist/visitor/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/visitor/arguments/helpers.ts +5 -5
- package/src/visitor/arguments/index.test.ts +43 -22
- package/src/visitor/arguments/index.ts +167 -32
- package/src/visitor/arguments/schema.test.ts +114 -4
- package/src/visitor/arguments/types.ts +178 -444
- package/src/visitor/returns/types.ts +4 -27
- package/src/visitor/types.ts +45 -0
package/dist/visitor/types.d.ts
CHANGED
|
@@ -2,4 +2,18 @@ export type FieldType = "functionRecord" | "function" | "record" | "variant" | "
|
|
|
2
2
|
export type { Principal } from "@icp-sdk/core/principal";
|
|
3
3
|
import type { IDL } from "@icp-sdk/core/candid";
|
|
4
4
|
export type AllNumberTypes = IDL.NatClass | IDL.IntClass | IDL.FixedNatClass | IDL.FixedIntClass | IDL.FloatClass;
|
|
5
|
+
/**
|
|
6
|
+
* The core Candid type category used across visitors.
|
|
7
|
+
*/
|
|
8
|
+
export type VisitorDataType = "record" | "variant" | "tuple" | "optional" | "vector" | "blob" | "recursive" | "principal" | "number" | "text" | "boolean" | "null" | "unknown";
|
|
9
|
+
/**
|
|
10
|
+
* Detected format for text fields based on label heuristics.
|
|
11
|
+
* Used to provide format-specific validation and display.
|
|
12
|
+
*/
|
|
13
|
+
export type TextFormat = "plain" | "timestamp" | "uuid" | "url" | "email" | "phone" | "btc" | "eth" | "account-id" | "principal" | "cycle";
|
|
14
|
+
/**
|
|
15
|
+
* Detected format for number fields based on label heuristics.
|
|
16
|
+
* Used to provide format-specific validation and display.
|
|
17
|
+
*/
|
|
18
|
+
export type NumberFormat = "timestamp" | "cycle" | "value" | "token" | "normal";
|
|
5
19
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/visitor/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,gBAAgB,GAChB,UAAU,GACV,QAAQ,GACR,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,SAAS,GACT,MAAM,GACN,QAAQ,GACR,WAAW,GACX,SAAS,GACT,MAAM,CAAA;AAEV,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC/C,MAAM,MAAM,cAAc,GACtB,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,UAAU,CAAA"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/visitor/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,gBAAgB,GAChB,UAAU,GACV,QAAQ,GACR,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,SAAS,GACT,MAAM,GACN,QAAQ,GACR,WAAW,GACX,SAAS,GACT,MAAM,CAAA;AAEV,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC/C,MAAM,MAAM,cAAc,GACtB,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,UAAU,CAAA;AAMlB;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,MAAM,GACN,WAAW,GACX,WAAW,GACX,QAAQ,GACR,MAAM,GACN,SAAS,GACT,MAAM,GACN,SAAS,CAAA;AAEb;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,MAAM,GACN,KAAK,GACL,OAAO,GACP,OAAO,GACP,KAAK,GACL,KAAK,GACL,YAAY,GACZ,WAAW,GACX,OAAO,CAAA;AAEX;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/candid",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.14-beta.0",
|
|
4
4
|
"description": "IC Reactor Candid Adapter - Fetch and parse Candid definitions from Internet Computer canisters",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@noble/hashes": "^2.0.1",
|
|
46
46
|
"zod": "^4.3.5",
|
|
47
|
-
"@ic-reactor/core": "^3.0.
|
|
47
|
+
"@ic-reactor/core": "^3.0.14-beta.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"zod": "^4.3.5",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArgumentFieldType,
|
|
3
3
|
CompoundField,
|
|
4
|
-
|
|
4
|
+
FieldNode,
|
|
5
5
|
FieldByType,
|
|
6
6
|
PrimitiveField,
|
|
7
7
|
RecordField,
|
|
@@ -28,14 +28,14 @@ import {
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export function isFieldType<T extends ArgumentFieldType>(
|
|
31
|
-
field:
|
|
31
|
+
field: FieldNode,
|
|
32
32
|
type: T
|
|
33
33
|
): field is FieldByType<T> {
|
|
34
34
|
return field.type === type
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** Check if a field is a compound type (contains other fields) */
|
|
38
|
-
export function isCompoundField(field:
|
|
38
|
+
export function isCompoundField(field: FieldNode): field is CompoundField {
|
|
39
39
|
return [
|
|
40
40
|
"record",
|
|
41
41
|
"variant",
|
|
@@ -47,13 +47,13 @@ export function isCompoundField(field: Field): field is CompoundField {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Check if a field is a primitive type */
|
|
50
|
-
export function isPrimitiveField(field:
|
|
50
|
+
export function isPrimitiveField(field: FieldNode): field is PrimitiveField {
|
|
51
51
|
return ["principal", "number", "text", "boolean", "null"].includes(field.type)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Check if a field has children (for iteration) */
|
|
55
55
|
export function hasChildFields(
|
|
56
|
-
field:
|
|
56
|
+
field: FieldNode
|
|
57
57
|
): field is RecordField | VariantField | TupleField {
|
|
58
58
|
return "fields" in field && Array.isArray((field as RecordField).fields)
|
|
59
59
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import { IDL } from "@icp-sdk/core/candid"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FieldVisitor,
|
|
5
|
+
OptionalField,
|
|
6
|
+
RecordField,
|
|
7
|
+
VariantField,
|
|
8
|
+
VectorField,
|
|
9
|
+
} from "./index"
|
|
4
10
|
|
|
5
11
|
describe("ArgumentFieldVisitor", () => {
|
|
6
12
|
const visitor = new FieldVisitor()
|
|
@@ -186,7 +192,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
186
192
|
expect(field.type).toBe("record")
|
|
187
193
|
expect(field.fields).toHaveLength(2)
|
|
188
194
|
|
|
189
|
-
const addressField = field.fields.find(
|
|
195
|
+
const addressField = field.fields.find(
|
|
196
|
+
(f) => f.label === "address"
|
|
197
|
+
) as RecordField
|
|
190
198
|
if (!addressField || addressField.type !== "record") {
|
|
191
199
|
throw new Error("Address field not found or not record")
|
|
192
200
|
}
|
|
@@ -235,7 +243,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
235
243
|
expect(field.fields).toHaveLength(5)
|
|
236
244
|
|
|
237
245
|
// Check 'to' field
|
|
238
|
-
const toField = field.fields.find((f) => f.label === "to")
|
|
246
|
+
const toField = field.fields.find((f) => f.label === "to") as RecordField
|
|
239
247
|
if (!toField || toField.type !== "record") {
|
|
240
248
|
throw new Error("To field not found or not record")
|
|
241
249
|
}
|
|
@@ -251,7 +259,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
251
259
|
expect(amountField.candidType).toBe("nat")
|
|
252
260
|
|
|
253
261
|
// Check optional 'fee' field
|
|
254
|
-
const feeField = field.fields.find(
|
|
262
|
+
const feeField = field.fields.find(
|
|
263
|
+
(f) => f.label === "fee"
|
|
264
|
+
) as OptionalField
|
|
255
265
|
if (!feeField || feeField.type !== "optional") {
|
|
256
266
|
throw new Error("Fee field not found or not optional")
|
|
257
267
|
}
|
|
@@ -289,8 +299,8 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
289
299
|
})
|
|
290
300
|
|
|
291
301
|
// Test getOptionDefault helper
|
|
292
|
-
expect(field.getOptionDefault("Active")).toEqual({
|
|
293
|
-
expect(field.getOptionDefault("Pending")).toEqual({
|
|
302
|
+
expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
|
|
303
|
+
expect(field.getOptionDefault("Pending")).toEqual({ _type: "Pending" })
|
|
294
304
|
})
|
|
295
305
|
|
|
296
306
|
it("should handle variant with payloads", () => {
|
|
@@ -330,7 +340,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
330
340
|
expect(field.type).toBe("variant")
|
|
331
341
|
expect(field.options).toEqual(["Approve", "Burn", "Transfer"]) // Sorted order
|
|
332
342
|
|
|
333
|
-
const transferField = field.fields.find(
|
|
343
|
+
const transferField = field.fields.find(
|
|
344
|
+
(f) => f.label === "Transfer"
|
|
345
|
+
) as RecordField
|
|
334
346
|
if (!transferField || transferField.type !== "record") {
|
|
335
347
|
throw new Error("Transfer field not found or not record")
|
|
336
348
|
}
|
|
@@ -429,7 +441,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
429
441
|
expect(field.type).toBe("optional")
|
|
430
442
|
expect(field.innerField.type).toBe("record")
|
|
431
443
|
expect(field.innerField.type).toBe("record")
|
|
432
|
-
const inner = field.innerField
|
|
444
|
+
const inner = field.innerField as RecordField
|
|
433
445
|
if (inner.type === "record") {
|
|
434
446
|
expect(inner.fields).toHaveLength(2)
|
|
435
447
|
} else {
|
|
@@ -448,7 +460,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
448
460
|
expect(field.type).toBe("optional")
|
|
449
461
|
expect(field.innerField.type).toBe("optional")
|
|
450
462
|
expect(field.innerField.type).toBe("optional")
|
|
451
|
-
const inner = field.innerField
|
|
463
|
+
const inner = field.innerField as OptionalField
|
|
452
464
|
if (inner.type === "optional") {
|
|
453
465
|
expect(inner.innerField.type).toBe("text")
|
|
454
466
|
} else {
|
|
@@ -479,7 +491,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
479
491
|
|
|
480
492
|
expect(field.type).toBe("vector")
|
|
481
493
|
expect(field.itemField.type).toBe("record")
|
|
482
|
-
const item = field.itemField
|
|
494
|
+
const item = field.itemField as RecordField
|
|
483
495
|
if (item.type === "record") {
|
|
484
496
|
expect(item.fields).toHaveLength(2)
|
|
485
497
|
} else {
|
|
@@ -509,7 +521,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
509
521
|
|
|
510
522
|
expect(field.type).toBe("vector")
|
|
511
523
|
expect(field.itemField.type).toBe("vector")
|
|
512
|
-
const item = field.itemField
|
|
524
|
+
const item = field.itemField as VectorField
|
|
513
525
|
if (item.type === "vector") {
|
|
514
526
|
expect(item.itemField.type).toBe("text")
|
|
515
527
|
} else {
|
|
@@ -550,7 +562,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
550
562
|
expect(typeof field.getInnerDefault).toBe("function")
|
|
551
563
|
|
|
552
564
|
// Extract should return a variant
|
|
553
|
-
const extracted = field.extract()
|
|
565
|
+
const extracted = field.extract() as VariantField
|
|
554
566
|
if (extracted.type !== "variant") {
|
|
555
567
|
throw new Error("Extracted field is not variant")
|
|
556
568
|
}
|
|
@@ -585,14 +597,16 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
585
597
|
|
|
586
598
|
expect(field.type).toBe("recursive")
|
|
587
599
|
|
|
588
|
-
const extracted = field.extract()
|
|
600
|
+
const extracted = field.extract() as VariantField
|
|
589
601
|
if (extracted.type !== "variant") {
|
|
590
602
|
throw new Error("Extracted field is not variant")
|
|
591
603
|
}
|
|
592
604
|
expect(extracted.type).toBe("variant")
|
|
593
605
|
expect(extracted.options).toEqual(["Nil", "Cons"])
|
|
594
606
|
|
|
595
|
-
const consField = extracted.fields.find(
|
|
607
|
+
const consField = extracted.fields.find(
|
|
608
|
+
(f) => f.label === "Cons"
|
|
609
|
+
) as RecordField
|
|
596
610
|
if (!consField || consField.type !== "record") {
|
|
597
611
|
throw new Error("Cons field not found or not record")
|
|
598
612
|
}
|
|
@@ -642,7 +656,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
642
656
|
expect(meta.fields).toHaveLength(1)
|
|
643
657
|
expect(meta.fields[0].type).toBe("record")
|
|
644
658
|
|
|
645
|
-
const recordField = meta.fields[0]
|
|
659
|
+
const recordField = meta.fields[0] as RecordField
|
|
646
660
|
if (recordField.type !== "record") {
|
|
647
661
|
throw new Error("Expected record field")
|
|
648
662
|
}
|
|
@@ -754,13 +768,15 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
754
768
|
)
|
|
755
769
|
const meta = visitor.visitFunc(funcType, "updateUser")
|
|
756
770
|
|
|
757
|
-
const argRecord = meta.fields[0]
|
|
771
|
+
const argRecord = meta.fields[0] as RecordField
|
|
758
772
|
if (argRecord.type !== "record") {
|
|
759
773
|
throw new Error("Expected record field")
|
|
760
774
|
}
|
|
761
775
|
expect(argRecord.name).toBe("[0]")
|
|
762
776
|
|
|
763
|
-
const userRecord = argRecord.fields.find(
|
|
777
|
+
const userRecord = argRecord.fields.find(
|
|
778
|
+
(f) => f.label === "user"
|
|
779
|
+
) as RecordField
|
|
764
780
|
if (!userRecord || userRecord.type !== "record") {
|
|
765
781
|
throw new Error("User record not found or not record")
|
|
766
782
|
}
|
|
@@ -783,7 +799,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
783
799
|
const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
|
|
784
800
|
const meta = visitor.visitFunc(funcType, "addTags")
|
|
785
801
|
|
|
786
|
-
const vecField = meta.fields[0]
|
|
802
|
+
const vecField = meta.fields[0] as VectorField
|
|
787
803
|
if (vecField.type !== "vector") {
|
|
788
804
|
throw new Error("Expected vector field")
|
|
789
805
|
}
|
|
@@ -837,7 +853,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
837
853
|
expect(field.fields.length).toBeGreaterThan(5)
|
|
838
854
|
|
|
839
855
|
// Check spender field
|
|
840
|
-
const spenderField = field.fields.find(
|
|
856
|
+
const spenderField = field.fields.find(
|
|
857
|
+
(f) => f.label === "spender"
|
|
858
|
+
) as RecordField
|
|
841
859
|
if (!spenderField || spenderField.type !== "record") {
|
|
842
860
|
throw new Error("Spender field not found or not record")
|
|
843
861
|
}
|
|
@@ -906,7 +924,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
906
924
|
expect(field.options).toContain("UpgradeSnsControlledCanister")
|
|
907
925
|
|
|
908
926
|
// Check Motion variant
|
|
909
|
-
const motionField = field.fields.find(
|
|
927
|
+
const motionField = field.fields.find(
|
|
928
|
+
(f) => f.label === "Motion"
|
|
929
|
+
) as RecordField
|
|
910
930
|
if (!motionField || motionField.type !== "record") {
|
|
911
931
|
throw new Error("Motion field not found or not record")
|
|
912
932
|
}
|
|
@@ -916,7 +936,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
916
936
|
// Check TransferSnsTreasuryFunds variant
|
|
917
937
|
const transferField = field.fields.find(
|
|
918
938
|
(f) => f.label === "TransferSnsTreasuryFunds"
|
|
919
|
-
)
|
|
939
|
+
) as RecordField
|
|
920
940
|
if (!transferField || transferField.type !== "record") {
|
|
921
941
|
throw new Error("Transfer field not found or not record")
|
|
922
942
|
}
|
|
@@ -944,8 +964,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
944
964
|
"status"
|
|
945
965
|
)
|
|
946
966
|
|
|
947
|
-
expect(field.getOptionDefault("Active")).toEqual({
|
|
967
|
+
expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
|
|
948
968
|
expect(field.getOptionDefault("Pending")).toEqual({
|
|
969
|
+
_type: "Pending",
|
|
949
970
|
Pending: { reason: "" },
|
|
950
971
|
})
|
|
951
972
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isQuery } from "../helpers"
|
|
2
|
+
import { checkTextFormat, checkNumberFormat } from "../constants"
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
+
FieldNode,
|
|
4
5
|
RecordField,
|
|
5
6
|
VariantField,
|
|
6
7
|
TupleField,
|
|
@@ -20,6 +21,8 @@ import type {
|
|
|
20
21
|
PrimitiveInputProps,
|
|
21
22
|
BlobLimits,
|
|
22
23
|
BlobValidationResult,
|
|
24
|
+
TextFormat,
|
|
25
|
+
NumberFormat,
|
|
23
26
|
} from "./types"
|
|
24
27
|
|
|
25
28
|
import { IDL } from "@icp-sdk/core/candid"
|
|
@@ -29,6 +32,8 @@ import * as z from "zod"
|
|
|
29
32
|
import { formatLabel } from "./helpers"
|
|
30
33
|
|
|
31
34
|
export * from "./types"
|
|
35
|
+
export * from "./helpers"
|
|
36
|
+
export { checkTextFormat, checkNumberFormat } from "../constants"
|
|
32
37
|
|
|
33
38
|
// ════════════════════════════════════════════════════════════════════════════
|
|
34
39
|
// Render Hint Helpers
|
|
@@ -177,7 +182,7 @@ function validateBlobInput(
|
|
|
177
182
|
*/
|
|
178
183
|
export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
179
184
|
string,
|
|
180
|
-
|
|
185
|
+
FieldNode | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
|
|
181
186
|
> {
|
|
182
187
|
public recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
|
|
183
188
|
|
|
@@ -231,7 +236,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
231
236
|
const fields = t.argTypes.map((arg, index) => {
|
|
232
237
|
return this.withName(`[${index}]`, () =>
|
|
233
238
|
arg.accept(this, `__arg${index}`)
|
|
234
|
-
) as
|
|
239
|
+
) as FieldNode
|
|
235
240
|
})
|
|
236
241
|
|
|
237
242
|
const defaultValues = fields.map((field) => field.defaultValue)
|
|
@@ -272,15 +277,15 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
272
277
|
label: string
|
|
273
278
|
): RecordField {
|
|
274
279
|
const name = this.currentName()
|
|
275
|
-
const fields:
|
|
276
|
-
const fieldMap = new Map<string,
|
|
280
|
+
const fields: FieldNode[] = []
|
|
281
|
+
const fieldMap = new Map<string, FieldNode>()
|
|
277
282
|
const defaultValue: Record<string, unknown> = {}
|
|
278
283
|
const schemaShape: Record<string, z.ZodTypeAny> = {}
|
|
279
284
|
|
|
280
285
|
for (const [key, type] of fields_) {
|
|
281
286
|
const field = this.withName(name ? `.${key}` : key, () =>
|
|
282
287
|
type.accept(this, key)
|
|
283
|
-
) as
|
|
288
|
+
) as FieldNode
|
|
284
289
|
|
|
285
290
|
fields.push(field)
|
|
286
291
|
fieldMap.set(key, field)
|
|
@@ -311,29 +316,44 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
311
316
|
label: string
|
|
312
317
|
): VariantField {
|
|
313
318
|
const name = this.currentName()
|
|
314
|
-
const fields:
|
|
319
|
+
const fields: FieldNode[] = []
|
|
315
320
|
const options: string[] = []
|
|
316
|
-
const optionMap = new Map<string,
|
|
321
|
+
const optionMap = new Map<string, FieldNode>()
|
|
317
322
|
const variantSchemas: z.ZodTypeAny[] = []
|
|
318
323
|
|
|
319
324
|
for (const [key, type] of fields_) {
|
|
320
325
|
const field = this.withName(`.${key}`, () =>
|
|
321
326
|
type.accept(this, key)
|
|
322
|
-
) as
|
|
327
|
+
) as FieldNode
|
|
323
328
|
|
|
324
329
|
fields.push(field)
|
|
325
330
|
options.push(key)
|
|
326
331
|
optionMap.set(key, field)
|
|
327
|
-
|
|
332
|
+
|
|
333
|
+
if (field.type === "null") {
|
|
334
|
+
variantSchemas.push(z.object({ _type: z.literal(key) }))
|
|
335
|
+
} else {
|
|
336
|
+
variantSchemas.push(
|
|
337
|
+
z.object({
|
|
338
|
+
_type: z.literal(key),
|
|
339
|
+
[key]: field.schema,
|
|
340
|
+
})
|
|
341
|
+
)
|
|
342
|
+
}
|
|
328
343
|
}
|
|
329
344
|
|
|
330
345
|
const defaultOption = options[0]
|
|
331
346
|
const firstField = fields[0]
|
|
332
|
-
const defaultValue = {
|
|
333
|
-
[defaultOption]: firstField.defaultValue,
|
|
334
|
-
}
|
|
335
347
|
|
|
336
|
-
const
|
|
348
|
+
const defaultValue =
|
|
349
|
+
firstField.type === "null"
|
|
350
|
+
? { _type: defaultOption }
|
|
351
|
+
: {
|
|
352
|
+
_type: defaultOption,
|
|
353
|
+
[defaultOption]: firstField.defaultValue,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const schema = z.union(variantSchemas)
|
|
337
357
|
|
|
338
358
|
// Helper to get default value for any option
|
|
339
359
|
const getOptionDefault = (option: string): Record<string, unknown> => {
|
|
@@ -341,11 +361,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
341
361
|
if (!optField) {
|
|
342
362
|
throw new Error(`Unknown variant option: ${option}`)
|
|
343
363
|
}
|
|
344
|
-
return
|
|
364
|
+
return optField.type === "null"
|
|
365
|
+
? { _type: option }
|
|
366
|
+
: { _type: option, [option]: optField.defaultValue }
|
|
345
367
|
}
|
|
346
368
|
|
|
347
369
|
// Helper to get field for a specific option
|
|
348
|
-
const getField = (option: string):
|
|
370
|
+
const getField = (option: string): FieldNode => {
|
|
349
371
|
const optField = optionMap.get(option)
|
|
350
372
|
if (!optField) {
|
|
351
373
|
throw new Error(`Unknown variant option: ${option}`)
|
|
@@ -355,12 +377,15 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
355
377
|
|
|
356
378
|
// Helper to get currently selected option from a value
|
|
357
379
|
const getSelectedOption = (value: Record<string, unknown>): string => {
|
|
380
|
+
if (value._type && typeof value._type === "string") {
|
|
381
|
+
return value._type
|
|
382
|
+
}
|
|
358
383
|
const validKeys = Object.keys(value).filter((k) => options.includes(k))
|
|
359
384
|
return validKeys[0] ?? defaultOption
|
|
360
385
|
}
|
|
361
386
|
|
|
362
387
|
// Helper to get selected field from a value
|
|
363
|
-
const getSelectedField = (value: Record<string, unknown>):
|
|
388
|
+
const getSelectedField = (value: Record<string, unknown>): FieldNode => {
|
|
364
389
|
const selectedOption = getSelectedOption(value)
|
|
365
390
|
return getField(selectedOption)
|
|
366
391
|
}
|
|
@@ -392,7 +417,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
392
417
|
label: string
|
|
393
418
|
): TupleField {
|
|
394
419
|
const name = this.currentName()
|
|
395
|
-
const fields:
|
|
420
|
+
const fields: FieldNode[] = []
|
|
396
421
|
const defaultValue: unknown[] = []
|
|
397
422
|
const schemas: z.ZodTypeAny[] = []
|
|
398
423
|
|
|
@@ -400,7 +425,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
400
425
|
const type = components[index]
|
|
401
426
|
const field = this.withName(`[${index}]`, () =>
|
|
402
427
|
type.accept(this, `_${index}_`)
|
|
403
|
-
) as
|
|
428
|
+
) as FieldNode
|
|
404
429
|
|
|
405
430
|
fields.push(field)
|
|
406
431
|
defaultValue.push(field.defaultValue)
|
|
@@ -432,7 +457,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
432
457
|
|
|
433
458
|
// For optional, the inner field keeps the same name path
|
|
434
459
|
// because the value replaces null directly (not nested)
|
|
435
|
-
const innerField = ty.accept(this, label) as
|
|
460
|
+
const innerField = ty.accept(this, label) as FieldNode
|
|
436
461
|
|
|
437
462
|
const schema = z.union([
|
|
438
463
|
innerField.schema,
|
|
@@ -478,7 +503,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
478
503
|
// Item field uses [0] as template path
|
|
479
504
|
const itemField = this.withName("[0]", () =>
|
|
480
505
|
ty.accept(this, `${label}_item`)
|
|
481
|
-
) as
|
|
506
|
+
) as FieldNode
|
|
482
507
|
|
|
483
508
|
if (isBlob) {
|
|
484
509
|
const schema = z.union([
|
|
@@ -517,7 +542,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
517
542
|
const createItemField = (
|
|
518
543
|
index: number,
|
|
519
544
|
overrides?: { label?: string }
|
|
520
|
-
):
|
|
545
|
+
): FieldNode => {
|
|
521
546
|
// Replace [0] in template with actual index
|
|
522
547
|
const itemName = name ? `${name}[${index}]` : `[${index}]`
|
|
523
548
|
const itemLabel = overrides?.label ?? `Item ${index}`
|
|
@@ -559,13 +584,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
559
584
|
if (this.recursiveSchemas.has(typeName)) {
|
|
560
585
|
schema = this.recursiveSchemas.get(typeName)!
|
|
561
586
|
} else {
|
|
562
|
-
schema = z.lazy(() => (ty.accept(this, label) as
|
|
587
|
+
schema = z.lazy(() => (ty.accept(this, label) as FieldNode).schema)
|
|
563
588
|
this.recursiveSchemas.set(typeName, schema)
|
|
564
589
|
}
|
|
565
590
|
|
|
566
591
|
// Lazy extraction to prevent infinite loops
|
|
567
|
-
const extract = ():
|
|
568
|
-
this.withName(name, () => ty.accept(this, label)) as
|
|
592
|
+
const extract = (): FieldNode =>
|
|
593
|
+
this.withName(name, () => ty.accept(this, label)) as FieldNode
|
|
569
594
|
|
|
570
595
|
// Helper to get inner default (evaluates lazily)
|
|
571
596
|
const getInnerDefault = (): unknown => extract().defaultValue
|
|
@@ -628,6 +653,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
628
653
|
defaultValue: "",
|
|
629
654
|
maxLength: 64,
|
|
630
655
|
minLength: 7,
|
|
656
|
+
format: checkTextFormat(label) as TextFormat,
|
|
631
657
|
schema,
|
|
632
658
|
inputProps,
|
|
633
659
|
candidType: "principal",
|
|
@@ -635,11 +661,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
635
661
|
}
|
|
636
662
|
|
|
637
663
|
public visitText(_t: IDL.TextClass, label: string): TextField {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
664
|
+
const format = checkTextFormat(label) as TextFormat
|
|
665
|
+
|
|
666
|
+
// Generate format-specific inputProps
|
|
667
|
+
const inputProps = this.getTextInputProps(format)
|
|
668
|
+
|
|
669
|
+
// Generate format-specific schema
|
|
670
|
+
const schema = this.getTextSchema(format)
|
|
643
671
|
|
|
644
672
|
return {
|
|
645
673
|
type: "text",
|
|
@@ -649,12 +677,109 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
649
677
|
component: "text-input",
|
|
650
678
|
renderHint: TEXT_RENDER_HINT,
|
|
651
679
|
defaultValue: "",
|
|
652
|
-
|
|
680
|
+
format,
|
|
681
|
+
schema,
|
|
653
682
|
inputProps,
|
|
654
683
|
candidType: "text",
|
|
655
684
|
}
|
|
656
685
|
}
|
|
657
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Generate format-specific input props for text fields.
|
|
689
|
+
*/
|
|
690
|
+
private getTextInputProps(format: TextFormat): PrimitiveInputProps {
|
|
691
|
+
switch (format) {
|
|
692
|
+
case "email":
|
|
693
|
+
return {
|
|
694
|
+
type: "email",
|
|
695
|
+
placeholder: "email@example.com",
|
|
696
|
+
inputMode: "email",
|
|
697
|
+
autoComplete: "email",
|
|
698
|
+
spellCheck: false,
|
|
699
|
+
}
|
|
700
|
+
case "url":
|
|
701
|
+
return {
|
|
702
|
+
type: "url",
|
|
703
|
+
placeholder: "https://example.com",
|
|
704
|
+
inputMode: "url",
|
|
705
|
+
autoComplete: "url",
|
|
706
|
+
spellCheck: false,
|
|
707
|
+
}
|
|
708
|
+
case "phone":
|
|
709
|
+
return {
|
|
710
|
+
type: "tel",
|
|
711
|
+
placeholder: "+1 (555) 123-4567",
|
|
712
|
+
inputMode: "tel",
|
|
713
|
+
autoComplete: "tel",
|
|
714
|
+
spellCheck: false,
|
|
715
|
+
}
|
|
716
|
+
case "uuid":
|
|
717
|
+
return {
|
|
718
|
+
type: "text",
|
|
719
|
+
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
720
|
+
pattern:
|
|
721
|
+
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
722
|
+
spellCheck: false,
|
|
723
|
+
autoComplete: "off",
|
|
724
|
+
}
|
|
725
|
+
case "btc":
|
|
726
|
+
return {
|
|
727
|
+
type: "text",
|
|
728
|
+
placeholder: "bc1... or 1... or 3...",
|
|
729
|
+
spellCheck: false,
|
|
730
|
+
autoComplete: "off",
|
|
731
|
+
}
|
|
732
|
+
case "eth":
|
|
733
|
+
return {
|
|
734
|
+
type: "text",
|
|
735
|
+
placeholder: "0x...",
|
|
736
|
+
pattern: "0x[0-9a-fA-F]{40}",
|
|
737
|
+
spellCheck: false,
|
|
738
|
+
autoComplete: "off",
|
|
739
|
+
}
|
|
740
|
+
case "account-id":
|
|
741
|
+
return {
|
|
742
|
+
type: "text",
|
|
743
|
+
placeholder: "64-character hex string",
|
|
744
|
+
pattern: "[0-9a-fA-F]{64}",
|
|
745
|
+
maxLength: 64,
|
|
746
|
+
spellCheck: false,
|
|
747
|
+
autoComplete: "off",
|
|
748
|
+
}
|
|
749
|
+
case "principal":
|
|
750
|
+
return {
|
|
751
|
+
type: "text",
|
|
752
|
+
placeholder: "aaaaa-aa or full principal ID",
|
|
753
|
+
minLength: 7,
|
|
754
|
+
maxLength: 64,
|
|
755
|
+
spellCheck: false,
|
|
756
|
+
autoComplete: "off",
|
|
757
|
+
}
|
|
758
|
+
default:
|
|
759
|
+
return {
|
|
760
|
+
type: "text",
|
|
761
|
+
placeholder: "Enter text...",
|
|
762
|
+
spellCheck: true,
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Generate format-specific zod schema for text fields.
|
|
769
|
+
*/
|
|
770
|
+
private getTextSchema(format: TextFormat): z.ZodTypeAny {
|
|
771
|
+
switch (format) {
|
|
772
|
+
case "email":
|
|
773
|
+
return z.email("Invalid email address")
|
|
774
|
+
case "url":
|
|
775
|
+
return z.url("Invalid URL")
|
|
776
|
+
case "uuid":
|
|
777
|
+
return z.uuid("Invalid UUID")
|
|
778
|
+
default:
|
|
779
|
+
return z.string().min(1, "Required")
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
658
783
|
public visitBool(_t: IDL.BoolClass, label: string): BooleanField {
|
|
659
784
|
const inputProps: PrimitiveInputProps = {
|
|
660
785
|
type: "checkbox",
|
|
@@ -706,6 +831,8 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
706
831
|
max?: string
|
|
707
832
|
}
|
|
708
833
|
): NumberField | TextField {
|
|
834
|
+
const format = checkNumberFormat(label) as NumberFormat
|
|
835
|
+
|
|
709
836
|
let schema = z.string().min(1, "Required")
|
|
710
837
|
|
|
711
838
|
if (options.isFloat) {
|
|
@@ -722,6 +849,11 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
722
849
|
const type = isBigInt ? "text" : "number"
|
|
723
850
|
|
|
724
851
|
if (type === "text") {
|
|
852
|
+
// Propagate timestamp/cycle format if detected, otherwise default to plain
|
|
853
|
+
let textFormat: TextFormat = "plain"
|
|
854
|
+
if (format === "timestamp") textFormat = "timestamp"
|
|
855
|
+
if (format === "cycle") textFormat = "cycle"
|
|
856
|
+
|
|
725
857
|
const inputProps: PrimitiveInputProps = {
|
|
726
858
|
type: "text",
|
|
727
859
|
placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
|
|
@@ -739,6 +871,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
739
871
|
component: "text-input",
|
|
740
872
|
renderHint: TEXT_RENDER_HINT,
|
|
741
873
|
defaultValue: "",
|
|
874
|
+
format: textFormat,
|
|
742
875
|
candidType,
|
|
743
876
|
schema,
|
|
744
877
|
inputProps,
|
|
@@ -763,7 +896,8 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
763
896
|
renderHint: NUMBER_RENDER_HINT,
|
|
764
897
|
defaultValue: "",
|
|
765
898
|
candidType,
|
|
766
|
-
|
|
899
|
+
format,
|
|
900
|
+
schema,
|
|
767
901
|
inputProps,
|
|
768
902
|
...options,
|
|
769
903
|
}
|
|
@@ -838,6 +972,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
838
972
|
isPrimitive: false,
|
|
839
973
|
},
|
|
840
974
|
defaultValue: undefined,
|
|
975
|
+
candidType: "unknown",
|
|
841
976
|
schema: z.any(),
|
|
842
977
|
}
|
|
843
978
|
}
|