@ic-reactor/candid 3.0.14-beta.1 → 3.0.14-beta.2

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.
@@ -4,12 +4,32 @@ import type { VisitorDataType, TextFormat, NumberFormat } from "../types"
4
4
 
5
5
  export type { VisitorDataType, TextFormat, NumberFormat }
6
6
 
7
+ // ════════════════════════════════════════════════════════════════════════════
8
+ // Custom Error Class
9
+ // ════════════════════════════════════════════════════════════════════════════
10
+
11
+ /**
12
+ * Custom error class for metadata-related errors.
13
+ * Provides additional context about the field path and Candid type.
14
+ */
15
+ export class MetadataError extends Error {
16
+ constructor(
17
+ message: string,
18
+ public readonly fieldPath?: string,
19
+ public readonly candidType?: string
20
+ ) {
21
+ super(message)
22
+ this.name = "MetadataError"
23
+ }
24
+ }
25
+
7
26
  // ════════════════════════════════════════════════════════════════════════════
8
27
  // Component & UI Types
9
28
  // ════════════════════════════════════════════════════════════════════════════
10
29
 
11
30
  /**
12
31
  * Suggested component type for rendering the field.
32
+ * Use this to map field types to your UI components.
13
33
  */
14
34
  export type FieldComponentType =
15
35
  | "record-container"
@@ -68,121 +88,187 @@ export interface PrimitiveInputProps {
68
88
  // Base Field Interface
69
89
  // ════════════════════════════════════════════════════════════════════════════
70
90
 
91
+ /**
92
+ * Base properties shared by all field nodes.
93
+ */
71
94
  interface FieldBase<T extends VisitorDataType = VisitorDataType> {
95
+ /** The Candid type category (record, variant, text, number, etc.) */
72
96
  type: T
97
+ /** Raw label from Candid definition */
73
98
  label: string
99
+ /** Human-readable formatted label for display */
74
100
  displayLabel: string
101
+ /** Form path compatible with TanStack Form (e.g., "[0]", "[0].owner") */
75
102
  name: string
103
+ /** Suggested component type for rendering */
76
104
  component: FieldComponentType
105
+ /** UI rendering hints */
77
106
  renderHint: RenderHint
107
+ /** Zod validation schema */
78
108
  schema: z.ZodTypeAny
109
+ /** Original Candid type name */
79
110
  candidType: string
111
+ /** Default value for form initialization */
80
112
  defaultValue: unknown
81
113
  }
82
114
 
83
115
  // ════════════════════════════════════════════════════════════════════════════
84
- // Field Extras
116
+ // Field Extras - Type-Specific Properties
85
117
  // ════════════════════════════════════════════════════════════════════════════
86
118
 
119
+ /** Blob upload limits configuration */
87
120
  export interface BlobLimits {
88
121
  maxHexBytes: number
89
122
  maxFileBytes: number
90
123
  maxHexDisplayLength: number
91
124
  }
92
125
 
126
+ /** Blob validation result */
93
127
  export interface BlobValidationResult {
94
128
  valid: boolean
95
129
  error?: string
96
130
  }
97
131
 
98
132
  interface RecordExtras {
133
+ /** Child fields of the record */
99
134
  fields: FieldNode[]
135
+ /** Default value as object with all field defaults */
100
136
  defaultValue: Record<string, unknown>
101
137
  }
102
138
 
103
139
  interface VariantExtras {
104
- fields: FieldNode[]
140
+ /** All variant options as fields */
141
+ options: FieldNode[]
142
+ /** The default selected option key */
105
143
  defaultOption: string
144
+ /** Default value with the first option selected */
106
145
  defaultValue: Record<string, unknown>
146
+ /** Get default value for a specific option */
107
147
  getOptionDefault: (option: string) => Record<string, unknown>
108
- getField: (option: string) => FieldNode
109
- getSelectedOption: (value: Record<string, unknown>) => string
110
- getSelectedField: (value: Record<string, unknown>) => FieldNode
148
+ /** Get field descriptor for a specific option */
149
+ getOption: (option: string) => FieldNode
150
+ /** Get currently selected option key from a value */
151
+ getSelectedKey: (value: Record<string, unknown>) => string
152
+ /** Get the field for the currently selected option */
153
+ getSelectedOption: (value: Record<string, unknown>) => FieldNode
111
154
  }
112
155
 
113
156
  interface TupleExtras {
157
+ /** Tuple element fields */
114
158
  fields: FieldNode[]
159
+ /** Default value as array of element defaults */
115
160
  defaultValue: unknown[]
116
161
  }
117
162
 
118
163
  interface OptionalExtras {
164
+ /** The inner type field */
119
165
  innerField: FieldNode
166
+ /** Default value is always null */
120
167
  defaultValue: null
168
+ /** Get default value of the inner type when enabling */
121
169
  getInnerDefault: () => unknown
170
+ /** Check if a value represents an enabled optional */
122
171
  isEnabled: (value: unknown) => boolean
123
172
  }
124
173
 
125
174
  interface VectorExtras {
175
+ /** Template field for vector items */
126
176
  itemField: FieldNode
177
+ /** Default value is empty array */
127
178
  defaultValue: unknown[]
179
+ /** Get default value for a new item */
128
180
  getItemDefault: () => unknown
181
+ /** Create a field node for an item at a specific index */
129
182
  createItemField: (index: number, overrides?: { label?: string }) => FieldNode
130
183
  }
131
184
 
132
185
  interface BlobExtras {
186
+ /** Template field for blob bytes */
133
187
  itemField: FieldNode
188
+ /** Accepted input formats */
134
189
  acceptedFormats: ("hex" | "base64" | "file")[]
190
+ /** Upload limits */
135
191
  limits: BlobLimits
192
+ /** Normalize hex input string */
136
193
  normalizeHex: (input: string) => string
194
+ /** Validate blob input */
137
195
  validateInput: (value: string | Uint8Array) => BlobValidationResult
196
+ /** Default value is empty string */
138
197
  defaultValue: string
139
198
  }
140
199
 
141
200
  interface RecursiveExtras {
201
+ /** The recursive type name */
142
202
  typeName: string
203
+ /** Lazily extract the inner type */
143
204
  extract: () => FieldNode
205
+ /** Get default value of the inner type */
144
206
  getInnerDefault: () => unknown
207
+ /** Default value is undefined */
145
208
  defaultValue: undefined
146
209
  }
147
210
 
148
211
  interface PrincipalExtras {
212
+ /** Maximum Principal string length */
149
213
  maxLength: number
214
+ /** Minimum Principal string length */
150
215
  minLength: number
216
+ /** Detected text format */
151
217
  format: TextFormat
218
+ /** Pre-computed HTML input props */
152
219
  inputProps: PrimitiveInputProps
220
+ /** Default value is empty string */
153
221
  defaultValue: string
154
222
  }
155
223
 
156
224
  interface NumberExtras {
225
+ /** Whether the number is unsigned (nat vs int) */
157
226
  unsigned: boolean
227
+ /** Whether the number is a float */
158
228
  isFloat: boolean
229
+ /** Bit width (8, 16, 32, 64) */
159
230
  bits?: number
231
+ /** Minimum value as string */
160
232
  min?: string
233
+ /** Maximum value as string */
161
234
  max?: string
235
+ /** Detected number format */
162
236
  format: NumberFormat
237
+ /** Pre-computed HTML input props */
163
238
  inputProps: PrimitiveInputProps
239
+ /** Default value is empty string */
164
240
  defaultValue: string
165
241
  }
166
242
 
167
243
  interface TextExtras {
244
+ /** Minimum text length */
168
245
  minLength?: number
246
+ /** Maximum text length */
169
247
  maxLength?: number
248
+ /** Whether to use multiline input */
170
249
  multiline?: boolean
250
+ /** Detected text format */
171
251
  format: TextFormat
252
+ /** Pre-computed HTML input props */
172
253
  inputProps: PrimitiveInputProps
254
+ /** Default value is empty string */
173
255
  defaultValue: string
174
256
  }
175
257
 
176
258
  interface BooleanExtras {
259
+ /** Pre-computed HTML input props */
177
260
  inputProps: PrimitiveInputProps
261
+ /** Default value is false */
178
262
  defaultValue: boolean
179
263
  }
180
264
 
181
265
  interface NullExtras {
266
+ /** Default value is null */
182
267
  defaultValue: null
183
268
  }
184
269
 
185
270
  interface UnknownExtras {
271
+ /** Default value is undefined */
186
272
  defaultValue: undefined
187
273
  }
188
274
 
@@ -238,27 +324,33 @@ export type UnknownField = FieldNode<"unknown">
238
324
  // Form Metadata
239
325
  // ════════════════════════════════════════════════════════════════════════════
240
326
 
327
+ /**
328
+ * Metadata for a single method's input arguments.
329
+ * Use this to generate dynamic forms for calling canister methods.
330
+ */
241
331
  export interface ArgumentsMeta<
242
332
  A = BaseActor,
243
333
  Name extends FunctionName<A> = FunctionName<A>,
244
334
  > {
335
+ /** Whether this is a "query" or "update" call */
245
336
  functionType: FunctionType
337
+ /** The method name as defined in the Candid interface */
246
338
  functionName: Name
247
- fields: FieldNode[]
248
- defaultValues: unknown[]
339
+ /** Array of field descriptors, one per argument */
340
+ args: FieldNode[]
341
+ /** Default values suitable for form initialization */
342
+ defaults: unknown[]
343
+ /** Zod schema for validating all arguments together */
249
344
  schema: z.ZodTuple<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
345
+ /** Number of arguments (0 for no-arg methods) */
250
346
  argCount: number
251
- isNoArgs: boolean
252
- }
253
-
254
- export interface FormOptions {
255
- defaultValues: unknown[]
256
- validators: {
257
- onChange: z.ZodTypeAny
258
- onBlur: z.ZodTypeAny
259
- }
347
+ /** True if this method takes no arguments */
348
+ isEmpty: boolean
260
349
  }
261
350
 
351
+ /**
352
+ * Service-level metadata mapping method names to their argument metadata.
353
+ */
262
354
  export type ArgumentsServiceMeta<A = BaseActor> = {
263
355
  [K in FunctionName<A>]: ArgumentsMeta<A, K>
264
356
  }
@@ -267,16 +359,15 @@ export type ArgumentsServiceMeta<A = BaseActor> = {
267
359
  // Type Utilities
268
360
  // ════════════════════════════════════════════════════════════════════════════
269
361
 
362
+ /**
363
+ * Extract field type by VisitorDataType.
364
+ */
270
365
  export type FieldByType<T extends VisitorDataType> = Extract<
271
366
  FieldNode,
272
367
  { type: T }
273
368
  >
274
369
 
275
- export type FieldProps<T extends VisitorDataType> = {
276
- field: FieldByType<T>
277
- renderField?: (child: FieldNode) => React.ReactNode
278
- }
279
-
370
+ /** Compound field types that contain other fields */
280
371
  export type CompoundField =
281
372
  | RecordField
282
373
  | VariantField
@@ -285,6 +376,7 @@ export type CompoundField =
285
376
  | VectorField
286
377
  | RecursiveField
287
378
 
379
+ /** Primitive field types with direct values */
288
380
  export type PrimitiveField =
289
381
  | PrincipalField
290
382
  | NumberField
@@ -347,7 +347,7 @@ describe("ResultFieldVisitor", () => {
347
347
  // Validate options by resolving each option
348
348
  const resolvedActive = field.resolve({ Active: null })
349
349
  expect(resolvedActive.selected).toBe("Active")
350
- expect(resolvedActive.selectedOption.type).toBe("null")
350
+ expect(resolvedActive.selectedValue.type).toBe("null")
351
351
  const resolvedInactive = field.resolve({ Inactive: null })
352
352
  expect(resolvedInactive.selected).toBe("Inactive")
353
353
  const resolvedPending = field.resolve({ Pending: null })
@@ -396,14 +396,14 @@ describe("ResultFieldVisitor", () => {
396
396
  Transfer: { from: "aaaaa-aa", to: "bbbbb-bb", amount: BigInt(1) },
397
397
  })
398
398
  expect(transferResolved.selected).toBe("Transfer")
399
- expect(transferResolved.selectedOption.type).toBe("record")
399
+ expect(transferResolved.selectedValue.type).toBe("record")
400
400
  expect(
401
- Object.keys((transferResolved.selectedOption as RecordNode).fields)
401
+ Object.keys((transferResolved.selectedValue as RecordNode).fields)
402
402
  ).toHaveLength(3)
403
403
 
404
404
  const approveResolved = field.resolve({ Approve: BigInt(5) })
405
405
  expect(approveResolved.selected).toBe("Approve")
406
- expect(approveResolved.selectedOption.type).toBe("number")
406
+ expect(approveResolved.selectedValue.type).toBe("number")
407
407
  })
408
408
 
409
409
  it("should detect Result variant (Ok/Err)", () => {
@@ -425,13 +425,13 @@ describe("ResultFieldVisitor", () => {
425
425
 
426
426
  const okResolved = field.resolve({ Ok: BigInt(1) })
427
427
  expect(okResolved.selected).toBe("Ok")
428
- expect(okResolved.selectedOption.type).toBe("number")
429
- expect(okResolved.selectedOption.displayType).toBe("string")
428
+ expect(okResolved.selectedValue.type).toBe("number")
429
+ expect(okResolved.selectedValue.displayType).toBe("string")
430
430
 
431
431
  const errResolved = field.resolve({ Err: "error" })
432
432
  expect(errResolved.selected).toBe("Err")
433
- expect(errResolved.selectedOption.type).toBe("text")
434
- expect(errResolved.selectedOption.displayType).toBe("string")
433
+ expect(errResolved.selectedValue.type).toBe("text")
434
+ expect(errResolved.selectedValue.displayType).toBe("string")
435
435
  })
436
436
 
437
437
  it("should detect complex Result variant", () => {
@@ -474,11 +474,11 @@ describe("ResultFieldVisitor", () => {
474
474
  Ok: { id: BigInt(1), data: new Uint8Array([1, 2, 3]) },
475
475
  })
476
476
  expect(okResolved.selected).toBe("Ok")
477
- expect(okResolved.selectedOption.type).toBe("record")
477
+ expect(okResolved.selectedValue.type).toBe("record")
478
478
 
479
479
  const errResolved = field.resolve({ Err: { NotFound: null } })
480
480
  expect(errResolved.selected).toBe("Err")
481
- expect(errResolved.selectedOption.type).toBe("variant")
481
+ expect(errResolved.selectedValue.type).toBe("variant")
482
482
  })
483
483
 
484
484
  it("should not detect non-Result variant with Ok and other options", () => {
@@ -944,18 +944,18 @@ describe("ResultFieldVisitor", () => {
944
944
  expect(field.displayType).toBe("result")
945
945
 
946
946
  const okResolved = field.resolve({ Ok: BigInt(123) })
947
- if ((okResolved.selectedOption as ResolvedNode).type !== "number") {
947
+ if ((okResolved.selectedValue as ResolvedNode).type !== "number") {
948
948
  throw new Error("Ok field is not number")
949
949
  }
950
- expect((okResolved.selectedOption as ResolvedNode).candidType).toBe("nat")
951
- expect((okResolved.selectedOption as ResolvedNode).displayType).toBe(
950
+ expect((okResolved.selectedValue as ResolvedNode).candidType).toBe("nat")
951
+ expect((okResolved.selectedValue as ResolvedNode).displayType).toBe(
952
952
  "string"
953
953
  )
954
954
 
955
955
  const errResolved = field.resolve({
956
956
  Err: { InsufficientFunds: { balance: BigInt(0) } },
957
957
  })
958
- const innerErr = errResolved.selectedOption as ResolvedNode
958
+ const innerErr = errResolved.selectedValue as ResolvedNode
959
959
  expect(innerErr.type).toBe("variant")
960
960
  const insufficient = (innerErr as any).resolve({
961
961
  InsufficientFunds: { balance: BigInt(0) },
@@ -1269,7 +1269,7 @@ describe("ResultFieldVisitor", () => {
1269
1269
  )
1270
1270
 
1271
1271
  expect(() => field.resolve(null)).toThrow(
1272
- "Expected record for field user, but got null"
1272
+ "Expected record, but got null"
1273
1273
  )
1274
1274
  })
1275
1275
  })
@@ -1292,7 +1292,7 @@ describe("ResultFieldVisitor", () => {
1292
1292
  const resolved = field.resolve({ Ok: "Success" })
1293
1293
  expect(resolved.type).toBe(field.type)
1294
1294
  expect(resolved.selected).toBe("Ok")
1295
- const data = resolved.selectedOption as ResolvedNode
1295
+ const data = resolved.selectedValue as ResolvedNode
1296
1296
  expect(data.value).toBe("Success")
1297
1297
  })
1298
1298
 
@@ -1313,7 +1313,7 @@ describe("ResultFieldVisitor", () => {
1313
1313
  const resolved = field.resolve({ Err: "Something went wrong" })
1314
1314
 
1315
1315
  expect(resolved.selected).toBe("Err")
1316
- const data = resolved.selectedOption as ResolvedNode
1316
+ const data = resolved.selectedValue as ResolvedNode
1317
1317
  expect(data.value).toBe("Something went wrong")
1318
1318
  })
1319
1319
 
@@ -1329,7 +1329,7 @@ describe("ResultFieldVisitor", () => {
1329
1329
  )
1330
1330
 
1331
1331
  expect(() => field.resolve(null)).toThrow(
1332
- "Expected variant for field choice, but got null"
1332
+ "Expected variant, but got null"
1333
1333
  )
1334
1334
  })
1335
1335
  })
@@ -1358,7 +1358,7 @@ describe("ResultFieldVisitor", () => {
1358
1358
  const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
1359
1359
 
1360
1360
  expect(() => field.resolve(null)).toThrow(
1361
- "Expected tuple for field pair, but got null"
1361
+ "Expected tuple, but got null"
1362
1362
  )
1363
1363
  })
1364
1364
  })
@@ -1416,7 +1416,7 @@ describe("ResultFieldVisitor", () => {
1416
1416
  const field = visitor.visitVec(vecType, IDL.Text, "items")
1417
1417
 
1418
1418
  expect(() => field.resolve(null)).toThrow(
1419
- "Expected vector for field items, but got null"
1419
+ "Expected vector, but got null"
1420
1420
  )
1421
1421
  })
1422
1422
  })
@@ -1612,7 +1612,7 @@ describe("ResultFieldVisitor", () => {
1612
1612
 
1613
1613
  const okValue = okResult.results[0] as ResolvedNode
1614
1614
  expect((okValue as any).selected).toBe("Ok")
1615
- expect(((okValue as any).selectedOption as ResolvedNode).value).toBe(
1615
+ expect(((okValue as any).selectedValue as ResolvedNode).value).toBe(
1616
1616
  "12345"
1617
1617
  )
1618
1618
 
@@ -1622,7 +1622,7 @@ describe("ResultFieldVisitor", () => {
1622
1622
  })
1623
1623
  const errValue = errResult.results[0] as ResolvedNode
1624
1624
  expect((errValue as any).selected).toBe("Err")
1625
- expect(((errValue as any).selectedOption as ResolvedNode).value).toBe(
1625
+ expect(((errValue as any).selectedValue as ResolvedNode).value).toBe(
1626
1626
  "Insufficient funds"
1627
1627
  )
1628
1628
  })
@@ -1727,7 +1727,7 @@ describe("ResultFieldVisitor", () => {
1727
1727
 
1728
1728
  const successValue = successResult.results[0] as ResolvedNode
1729
1729
  expect((successValue as any).selected).toBe("Ok")
1730
- expect(((successValue as any).selectedOption as ResolvedNode).value).toBe(
1730
+ expect(((successValue as any).selectedValue as ResolvedNode).value).toBe(
1731
1731
  "1000"
1732
1732
  )
1733
1733
 
@@ -1738,7 +1738,7 @@ describe("ResultFieldVisitor", () => {
1738
1738
  const errorValue = errorResult.results[0] as ResolvedNode
1739
1739
  expect((errorValue as any).selected).toBe("Err")
1740
1740
 
1741
- const val = (errorValue as any).selectedOption as ResolvedNode
1741
+ const val = (errorValue as any).selectedValue as ResolvedNode
1742
1742
  if (typeof val !== "object" || val === null || !("selected" in val)) {
1743
1743
  throw new Error("Expected variant value object")
1744
1744
  }
@@ -1876,7 +1876,7 @@ describe("ResultFieldVisitor", () => {
1876
1876
 
1877
1877
  const variantValue = result.results[0] as ResolvedNode
1878
1878
  expect((variantValue as any).selected).toBe("Ok")
1879
- const innerVal = (variantValue as any).selectedOption as ResolvedNode
1879
+ const innerVal = (variantValue as any).selectedValue as ResolvedNode
1880
1880
  expect(innerVal.value).toBe("12345")
1881
1881
  })
1882
1882
 
@@ -2042,7 +2042,7 @@ describe("ResultFieldVisitor Reproduction - User reported issue", () => {
2042
2042
  const resolved = field.resolve(transformedData)
2043
2043
 
2044
2044
  expect(resolved.selected).toBe("Quorum")
2045
- expect(resolved.selectedOption.value).toBe("some text")
2045
+ expect(resolved.selectedValue.value).toBe("some text")
2046
2046
  })
2047
2047
 
2048
2048
  it("should handle already transformed optional data (unwrapped)", () => {
@@ -2071,7 +2071,7 @@ describe("ResultFieldVisitor Reproduction - User reported issue", () => {
2071
2071
  )
2072
2072
 
2073
2073
  expect(() => field.resolve({ C: null })).toThrow(
2074
- /Option C not found in variant MyVariant. Available options: A, B/
2074
+ /Option "C" not found. Available: A, B/
2075
2075
  )
2076
2076
  })
2077
2077
 
@@ -2101,7 +2101,7 @@ describe("ResultFieldVisitor Reproduction - User reported issue", () => {
2101
2101
  const variantNode = resolved.inner as VariantNode
2102
2102
 
2103
2103
  expect(variantNode.selected).toBe("Quorum")
2104
- expect(variantNode.selectedOption.type).toBe("record")
2104
+ expect(variantNode.selectedValue.type).toBe("record")
2105
2105
  })
2106
2106
 
2107
2107
  it("should handle deeply nested recursive variant with transformed data", () => {
@@ -2130,7 +2130,7 @@ describe("ResultFieldVisitor Reproduction - User reported issue", () => {
2130
2130
  const variantNode = resolved.inner as VariantNode
2131
2131
  expect(variantNode.selected).toBe("Nested")
2132
2132
 
2133
- const nestedResolved = variantNode.selectedOption as RecursiveNode
2133
+ const nestedResolved = variantNode.selectedValue as RecursiveNode
2134
2134
  const innerVariant = nestedResolved.inner as VariantNode
2135
2135
  expect(innerVariant.selected).toBe("Quorum")
2136
2136
  })