@ic-reactor/candid 3.0.12-beta.0 → 3.0.13-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.
@@ -0,0 +1,104 @@
1
+ import {
2
+ ArgumentFieldType,
3
+ CompoundField,
4
+ Field,
5
+ FieldByType,
6
+ PrimitiveField,
7
+ RecordField,
8
+ TupleField,
9
+ VariantField,
10
+ } from "./types"
11
+
12
+ /**
13
+ * Type guard for checking specific field types.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * function FieldInput({ field }: { field: Field }) {
18
+ * if (isFieldType(field, 'record')) {
19
+ * // field is now typed as RecordField
20
+ * return <RecordInput field={field} />
21
+ * }
22
+ * if (isFieldType(field, 'text')) {
23
+ * // field is now typed as TextField
24
+ * return <TextInput field={field} />
25
+ * }
26
+ * // ...
27
+ * }
28
+ * ```
29
+ */
30
+ export function isFieldType<T extends ArgumentFieldType>(
31
+ field: Field,
32
+ type: T
33
+ ): field is FieldByType<T> {
34
+ return field.type === type
35
+ }
36
+
37
+ /** Check if a field is a compound type (contains other fields) */
38
+ export function isCompoundField(field: Field): field is CompoundField {
39
+ return [
40
+ "record",
41
+ "variant",
42
+ "tuple",
43
+ "optional",
44
+ "vector",
45
+ "recursive",
46
+ ].includes(field.type)
47
+ }
48
+
49
+ /** Check if a field is a primitive type */
50
+ export function isPrimitiveField(field: Field): field is PrimitiveField {
51
+ return ["principal", "number", "text", "boolean", "null"].includes(field.type)
52
+ }
53
+
54
+ /** Check if a field has children (for iteration) */
55
+ export function hasChildFields(
56
+ field: Field
57
+ ): field is RecordField | VariantField | TupleField {
58
+ return "fields" in field && Array.isArray((field as RecordField).fields)
59
+ }
60
+
61
+ // ════════════════════════════════════════════════════════════════════════════
62
+ // Label Formatting Utilities
63
+ // ════════════════════════════════════════════════════════════════════════════
64
+
65
+ /**
66
+ * Format a raw Candid label into a human-readable display label.
67
+ * Handles common patterns like "__arg0", "_0_", "snake_case", etc.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * formatLabel("__arg0") // "Arg 0"
72
+ * formatLabel("_0_") // "Item 0"
73
+ * formatLabel("created_at") // "Created At"
74
+ * formatLabel("userAddress") // "User Address"
75
+ * ```
76
+ */
77
+ export function formatLabel(label: string): string {
78
+ // Handle argument labels: __arg0 -> Arg 0
79
+ if (label.startsWith("__arg")) {
80
+ const num = label.slice(5)
81
+ return `Arg ${num}`
82
+ }
83
+
84
+ // Handle tuple index labels: _0_ -> Item 0
85
+ if (/^_\d+_$/.test(label)) {
86
+ const num = label.slice(1, -1)
87
+ return `Item ${num}`
88
+ }
89
+
90
+ // Handle item labels for vectors: label_item -> Item
91
+ if (label.endsWith("_item")) {
92
+ return "Item"
93
+ }
94
+
95
+ // Convert snake_case or just clean up underscores
96
+ // and capitalize each word
97
+ return label
98
+ .replace(/^_+|_+$/g, "") // Remove leading/trailing underscores
99
+ .replace(/_/g, " ") // Replace underscores with spaces
100
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // Add space before capitals (camelCase)
101
+ .split(" ")
102
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
103
+ .join(" ")
104
+ }
@@ -1,9 +1,9 @@
1
1
  import { describe, it, expect } from "vitest"
2
2
  import { IDL } from "@icp-sdk/core/candid"
3
- import { ArgumentFieldVisitor, VectorField } from "./index"
3
+ import { FieldVisitor, VectorField } from "./index"
4
4
 
5
5
  describe("ArgumentFieldVisitor", () => {
6
- const visitor = new ArgumentFieldVisitor()
6
+ const visitor = new FieldVisitor()
7
7
 
8
8
  // ════════════════════════════════════════════════════════════════════════
9
9
  // Primitive Types
@@ -972,4 +972,403 @@ describe("ArgumentFieldVisitor", () => {
972
972
  expect(field.getInnerDefault()).toEqual({ value: "" })
973
973
  })
974
974
  })
975
+
976
+ // ════════════════════════════════════════════════════════════════════════
977
+ // New Features - displayLabel, component, renderHint
978
+ // ════════════════════════════════════════════════════════════════════════
979
+
980
+ describe("displayLabel formatting", () => {
981
+ it("should format __arg labels correctly", () => {
982
+ const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
983
+ const meta = visitor.visitFunc(funcType, "test")
984
+
985
+ expect(meta.fields[0].displayLabel).toBe("Arg 0")
986
+ expect(meta.fields[1].displayLabel).toBe("Arg 1")
987
+ })
988
+
989
+ it("should format tuple index labels correctly", () => {
990
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat, IDL.Bool)
991
+ const field = visitor.visitTuple(
992
+ tupleType,
993
+ [IDL.Text, IDL.Nat, IDL.Bool],
994
+ "triple"
995
+ )
996
+
997
+ expect(field.fields[0].displayLabel).toBe("Item 0")
998
+ expect(field.fields[1].displayLabel).toBe("Item 1")
999
+ expect(field.fields[2].displayLabel).toBe("Item 2")
1000
+ })
1001
+
1002
+ it("should format snake_case labels correctly", () => {
1003
+ const recordType = IDL.Record({
1004
+ created_at_time: IDL.Nat,
1005
+ user_address: IDL.Text,
1006
+ })
1007
+ const field = visitor.visitRecord(
1008
+ recordType,
1009
+ [
1010
+ ["created_at_time", IDL.Nat],
1011
+ ["user_address", IDL.Text],
1012
+ ],
1013
+ "record"
1014
+ )
1015
+
1016
+ const createdField = field.fields.find(
1017
+ (f) => f.label === "created_at_time"
1018
+ )
1019
+ const userField = field.fields.find((f) => f.label === "user_address")
1020
+
1021
+ expect(createdField?.displayLabel).toBe("Created At Time")
1022
+ expect(userField?.displayLabel).toBe("User Address")
1023
+ })
1024
+ })
1025
+
1026
+ describe("component hints", () => {
1027
+ it("should have correct component for record", () => {
1028
+ const recordType = IDL.Record({ name: IDL.Text })
1029
+ const field = visitor.visitRecord(
1030
+ recordType,
1031
+ [["name", IDL.Text]],
1032
+ "person"
1033
+ )
1034
+
1035
+ expect(field.component).toBe("record-container")
1036
+ })
1037
+
1038
+ it("should have correct component for variant", () => {
1039
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text })
1040
+ const field = visitor.visitVariant(
1041
+ variantType,
1042
+ [
1043
+ ["A", IDL.Null],
1044
+ ["B", IDL.Text],
1045
+ ],
1046
+ "choice"
1047
+ )
1048
+
1049
+ expect(field.component).toBe("variant-select")
1050
+ })
1051
+
1052
+ it("should have correct component for optional", () => {
1053
+ const optType = IDL.Opt(IDL.Text)
1054
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1055
+
1056
+ expect(field.component).toBe("optional-toggle")
1057
+ })
1058
+
1059
+ it("should have correct component for vector", () => {
1060
+ const vecType = IDL.Vec(IDL.Text)
1061
+ const field = visitor.visitVec(vecType, IDL.Text, "vec") as VectorField
1062
+
1063
+ expect(field.component).toBe("vector-list")
1064
+ })
1065
+
1066
+ it("should have correct component for blob", () => {
1067
+ const blobType = IDL.Vec(IDL.Nat8)
1068
+ const field = visitor.visitVec(blobType, IDL.Nat8, "blob")
1069
+
1070
+ expect(field.component).toBe("blob-upload")
1071
+ })
1072
+
1073
+ it("should have correct component for text", () => {
1074
+ const field = visitor.visitText(IDL.Text, "text")
1075
+
1076
+ expect(field.component).toBe("text-input")
1077
+ })
1078
+
1079
+ it("should have correct component for number", () => {
1080
+ const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "num")
1081
+
1082
+ expect(field.component).toBe("number-input")
1083
+ })
1084
+
1085
+ it("should have correct component for boolean", () => {
1086
+ const field = visitor.visitBool(IDL.Bool, "bool")
1087
+
1088
+ expect(field.component).toBe("boolean-checkbox")
1089
+ })
1090
+
1091
+ it("should have correct component for principal", () => {
1092
+ const field = visitor.visitPrincipal(IDL.Principal, "principal")
1093
+
1094
+ expect(field.component).toBe("principal-input")
1095
+ })
1096
+
1097
+ it("should have correct component for null", () => {
1098
+ const field = visitor.visitNull(IDL.Null, "null")
1099
+
1100
+ expect(field.component).toBe("null-hidden")
1101
+ })
1102
+ })
1103
+
1104
+ describe("renderHint properties", () => {
1105
+ it("compound types should have isCompound: true", () => {
1106
+ const recordField = visitor.visitRecord(
1107
+ IDL.Record({ x: IDL.Text }),
1108
+ [["x", IDL.Text]],
1109
+ "rec"
1110
+ )
1111
+ const variantField = visitor.visitVariant(
1112
+ IDL.Variant({ A: IDL.Null }),
1113
+ [["A", IDL.Null]],
1114
+ "var"
1115
+ )
1116
+ const optionalField = visitor.visitOpt(IDL.Opt(IDL.Text), IDL.Text, "opt")
1117
+ const vectorField = visitor.visitVec(IDL.Vec(IDL.Text), IDL.Text, "vec")
1118
+
1119
+ expect(recordField.renderHint.isCompound).toBe(true)
1120
+ expect(recordField.renderHint.isPrimitive).toBe(false)
1121
+
1122
+ expect(variantField.renderHint.isCompound).toBe(true)
1123
+ expect(optionalField.renderHint.isCompound).toBe(true)
1124
+ expect((vectorField as VectorField).renderHint.isCompound).toBe(true)
1125
+ })
1126
+
1127
+ it("primitive types should have isPrimitive: true", () => {
1128
+ const textField = visitor.visitText(IDL.Text, "text")
1129
+ const boolField = visitor.visitBool(IDL.Bool, "bool")
1130
+ const principalField = visitor.visitPrincipal(IDL.Principal, "principal")
1131
+
1132
+ expect(textField.renderHint.isPrimitive).toBe(true)
1133
+ expect(textField.renderHint.isCompound).toBe(false)
1134
+
1135
+ expect(boolField.renderHint.isPrimitive).toBe(true)
1136
+ expect(principalField.renderHint.isPrimitive).toBe(true)
1137
+ })
1138
+
1139
+ it("should have correct inputType hints", () => {
1140
+ const textField = visitor.visitText(IDL.Text, "text")
1141
+ const boolField = visitor.visitBool(IDL.Bool, "bool")
1142
+ const numField = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "num")
1143
+
1144
+ expect(textField.renderHint.inputType).toBe("text")
1145
+ expect(boolField.renderHint.inputType).toBe("checkbox")
1146
+ expect(numField.renderHint.inputType).toBe("number")
1147
+ })
1148
+ })
1149
+
1150
+ describe("inputProps for primitive types", () => {
1151
+ it("text field should have inputProps", () => {
1152
+ const field = visitor.visitText(IDL.Text, "text")
1153
+
1154
+ expect(field.inputProps).toBeDefined()
1155
+ expect(field.inputProps.type).toBe("text")
1156
+ expect(field.inputProps.placeholder).toBeDefined()
1157
+ })
1158
+
1159
+ it("boolean field should have checkbox inputProps", () => {
1160
+ const field = visitor.visitBool(IDL.Bool, "bool")
1161
+
1162
+ expect(field.inputProps).toBeDefined()
1163
+ expect(field.inputProps.type).toBe("checkbox")
1164
+ })
1165
+
1166
+ it("number field should have number inputProps with min/max", () => {
1167
+ const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
1168
+
1169
+ if (field.type === "number") {
1170
+ expect(field.inputProps).toBeDefined()
1171
+ expect(field.inputProps.type).toBe("number")
1172
+ expect(field.inputProps.min).toBe("0")
1173
+ expect(field.inputProps.max).toBe("255")
1174
+ }
1175
+ })
1176
+
1177
+ it("principal field should have inputProps with spellCheck disabled", () => {
1178
+ const field = visitor.visitPrincipal(IDL.Principal, "principal")
1179
+
1180
+ expect(field.inputProps).toBeDefined()
1181
+ expect(field.inputProps.spellCheck).toBe(false)
1182
+ expect(field.inputProps.autoComplete).toBe("off")
1183
+ })
1184
+ })
1185
+
1186
+ // ════════════════════════════════════════════════════════════════════════
1187
+ // Enhanced Variant Helpers
1188
+ // ════════════════════════════════════════════════════════════════════════
1189
+
1190
+ describe("Variant helper methods", () => {
1191
+ it("getField should return the correct field for an option", () => {
1192
+ const variantType = IDL.Variant({
1193
+ Transfer: IDL.Record({ to: IDL.Principal, amount: IDL.Nat }),
1194
+ Burn: IDL.Nat,
1195
+ })
1196
+ const field = visitor.visitVariant(
1197
+ variantType,
1198
+ [
1199
+ ["Transfer", IDL.Record({ to: IDL.Principal, amount: IDL.Nat })],
1200
+ ["Burn", IDL.Nat],
1201
+ ],
1202
+ "action"
1203
+ )
1204
+
1205
+ const transferField = field.getField("Transfer")
1206
+ expect(transferField.type).toBe("record")
1207
+
1208
+ const burnField = field.getField("Burn")
1209
+ expect(burnField.type).toBe("text") // nat is rendered as text for large numbers
1210
+ })
1211
+
1212
+ it("getSelectedOption should return the selected option from a value", () => {
1213
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text, C: IDL.Nat })
1214
+ const field = visitor.visitVariant(
1215
+ variantType,
1216
+ [
1217
+ ["A", IDL.Null],
1218
+ ["B", IDL.Text],
1219
+ ["C", IDL.Nat],
1220
+ ],
1221
+ "choice"
1222
+ )
1223
+
1224
+ expect(field.getSelectedOption({ A: null })).toBe("A")
1225
+ expect(field.getSelectedOption({ B: "hello" })).toBe("B")
1226
+ expect(field.getSelectedOption({ C: "100" })).toBe("C")
1227
+ // Falls back to default option for unknown values
1228
+ expect(field.getSelectedOption({})).toBe("A")
1229
+ })
1230
+
1231
+ it("getSelectedField should return the field for the selected option", () => {
1232
+ const variantType = IDL.Variant({
1233
+ Ok: IDL.Nat,
1234
+ Err: IDL.Text,
1235
+ })
1236
+ const field = visitor.visitVariant(
1237
+ variantType,
1238
+ [
1239
+ ["Ok", IDL.Nat],
1240
+ ["Err", IDL.Text],
1241
+ ],
1242
+ "result"
1243
+ )
1244
+
1245
+ const okField = field.getSelectedField({ Ok: "100" })
1246
+ expect(okField.label).toBe("Ok")
1247
+
1248
+ const errField = field.getSelectedField({ Err: "error" })
1249
+ expect(errField.label).toBe("Err")
1250
+ })
1251
+ })
1252
+
1253
+ // ════════════════════════════════════════════════════════════════════════
1254
+ // Optional isEnabled Helper
1255
+ // ════════════════════════════════════════════════════════════════════════
1256
+
1257
+ describe("Optional isEnabled helper", () => {
1258
+ it("should return true for non-null values", () => {
1259
+ const optType = IDL.Opt(IDL.Text)
1260
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1261
+
1262
+ expect(field.isEnabled("hello")).toBe(true)
1263
+ expect(field.isEnabled("")).toBe(true)
1264
+ expect(field.isEnabled(0)).toBe(true)
1265
+ expect(field.isEnabled(false)).toBe(true)
1266
+ expect(field.isEnabled({})).toBe(true)
1267
+ })
1268
+
1269
+ it("should return false for null and undefined", () => {
1270
+ const optType = IDL.Opt(IDL.Text)
1271
+ const field = visitor.visitOpt(optType, IDL.Text, "optional")
1272
+
1273
+ expect(field.isEnabled(null)).toBe(false)
1274
+ expect(field.isEnabled(undefined)).toBe(false)
1275
+ })
1276
+ })
1277
+
1278
+ // ════════════════════════════════════════════════════════════════════════
1279
+ // Vector createItemField Helper
1280
+ // ════════════════════════════════════════════════════════════════════════
1281
+
1282
+ describe("Vector createItemField helper", () => {
1283
+ it("should create item field with correct index in name path", () => {
1284
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
1285
+ const meta = visitor.visitFunc(funcType, "addItems")
1286
+ const vecField = meta.fields[0] as VectorField
1287
+
1288
+ const item0 = vecField.createItemField(0)
1289
+ expect(item0.name).toBe("[0][0]")
1290
+
1291
+ const item5 = vecField.createItemField(5)
1292
+ expect(item5.name).toBe("[0][5]")
1293
+ })
1294
+
1295
+ it("should use custom label when provided", () => {
1296
+ const funcType = IDL.Func(
1297
+ [IDL.Vec(IDL.Record({ name: IDL.Text }))],
1298
+ [],
1299
+ []
1300
+ )
1301
+ const meta = visitor.visitFunc(funcType, "addItems")
1302
+ const vecField = meta.fields[0] as VectorField
1303
+
1304
+ const item = vecField.createItemField(3, { label: "Person 3" })
1305
+ expect(item.label).toBe("Person 3")
1306
+ expect(item.displayLabel).toBe("Person 3")
1307
+ })
1308
+
1309
+ it("should use default label when not provided", () => {
1310
+ const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
1311
+ const meta = visitor.visitFunc(funcType, "addTags")
1312
+ const vecField = meta.fields[0] as VectorField
1313
+
1314
+ const item = vecField.createItemField(2)
1315
+ expect(item.label).toBe("Item 2")
1316
+ expect(item.displayLabel).toBe("Item 2")
1317
+ })
1318
+ })
1319
+
1320
+ // ════════════════════════════════════════════════════════════════════════
1321
+ // Blob Field Utilities
1322
+ // ════════════════════════════════════════════════════════════════════════
1323
+
1324
+ describe("Blob field utilities", () => {
1325
+ it("should have limits defined", () => {
1326
+ const blobType = IDL.Vec(IDL.Nat8)
1327
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1328
+
1329
+ if (field.type === "blob") {
1330
+ expect(field.limits).toBeDefined()
1331
+ expect(field.limits.maxHexBytes).toBeGreaterThan(0)
1332
+ expect(field.limits.maxFileBytes).toBeGreaterThan(0)
1333
+ expect(field.limits.maxHexDisplayLength).toBeGreaterThan(0)
1334
+ }
1335
+ })
1336
+
1337
+ it("normalizeHex should remove 0x prefix and lowercase", () => {
1338
+ const blobType = IDL.Vec(IDL.Nat8)
1339
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1340
+
1341
+ if (field.type === "blob") {
1342
+ expect(field.normalizeHex("0xDEADBEEF")).toBe("deadbeef")
1343
+ expect(field.normalizeHex("DEADBEEF")).toBe("deadbeef")
1344
+ expect(field.normalizeHex("0x")).toBe("")
1345
+ expect(field.normalizeHex("abc123")).toBe("abc123")
1346
+ }
1347
+ })
1348
+
1349
+ it("validateInput should validate hex strings", () => {
1350
+ const blobType = IDL.Vec(IDL.Nat8)
1351
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1352
+
1353
+ if (field.type === "blob") {
1354
+ // Valid inputs
1355
+ expect(field.validateInput("").valid).toBe(true)
1356
+ expect(field.validateInput("deadbeef").valid).toBe(true)
1357
+ expect(field.validateInput("0x1234").valid).toBe(true)
1358
+
1359
+ // Invalid inputs
1360
+ expect(field.validateInput("xyz").valid).toBe(false)
1361
+ expect(field.validateInput("abc").valid).toBe(false) // odd length
1362
+ }
1363
+ })
1364
+
1365
+ it("validateInput should validate Uint8Array", () => {
1366
+ const blobType = IDL.Vec(IDL.Nat8)
1367
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1368
+
1369
+ if (field.type === "blob") {
1370
+ expect(field.validateInput(new Uint8Array([1, 2, 3])).valid).toBe(true)
1371
+ }
1372
+ })
1373
+ })
975
1374
  })