@ic-reactor/candid 3.0.14-beta.0 → 3.0.14-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.
@@ -2,21 +2,14 @@ import type { BaseActor, FunctionName, FunctionType } from "@ic-reactor/core"
2
2
  import * as z from "zod"
3
3
  import type { VisitorDataType, TextFormat, NumberFormat } from "../types"
4
4
 
5
- export type { TextFormat, NumberFormat }
5
+ export type { VisitorDataType, TextFormat, NumberFormat }
6
6
 
7
7
  // ════════════════════════════════════════════════════════════════════════════
8
- // Field Type Union
9
- // ════════════════════════════════════════════════════════════════════════════
10
-
11
- export type ArgumentFieldType = VisitorDataType
12
-
13
- // ════════════════════════════════════════════════════════════════════════════
14
- // Component Type Hints
8
+ // Component & UI Types
15
9
  // ════════════════════════════════════════════════════════════════════════════
16
10
 
17
11
  /**
18
12
  * Suggested component type for rendering the field.
19
- * This eliminates the need for switch statements in the frontend.
20
13
  */
21
14
  export type FieldComponentType =
22
15
  | "record-container"
@@ -33,13 +26,8 @@ export type FieldComponentType =
33
26
  | "recursive-lazy"
34
27
  | "unknown-fallback"
35
28
 
36
- // ════════════════════════════════════════════════════════════════════════════
37
- // Render Hints for UI Rendering Strategy
38
- // ════════════════════════════════════════════════════════════════════════════
39
-
40
29
  /**
41
30
  * Input type hints for HTML input elements.
42
- * Used by primitive fields to suggest the appropriate input type.
43
31
  */
44
32
  export type InputType =
45
33
  | "text"
@@ -51,49 +39,28 @@ export type InputType =
51
39
 
52
40
  /**
53
41
  * Rendering hints for the UI.
54
- * Eliminates the need for frontend to maintain COMPLEX_TYPES arrays.
55
42
  */
56
43
  export interface RenderHint {
57
- /** Whether this field has its own container/card styling (compound types) */
58
44
  isCompound: boolean
59
- /** Whether this is a leaf input (primitive types) */
60
45
  isPrimitive: boolean
61
- /** Suggested input type for HTML input elements */
62
46
  inputType?: InputType
63
- /** Description or help text for the field (derived from Candid) */
64
47
  description?: string
65
48
  }
66
49
 
67
- // ════════════════════════════════════════════════════════════════════════════
68
- // Primitive Input Props
69
- // ════════════════════════════════════════════════════════════════════════════
70
-
71
50
  /**
72
51
  * Pre-computed HTML input props for primitive fields.
73
- * Can be spread directly onto an input element.
74
52
  */
75
53
  export interface PrimitiveInputProps {
76
- /** HTML input type - includes format-specific types */
77
54
  type?: "text" | "number" | "checkbox" | "email" | "url" | "tel"
78
- /** Placeholder text */
79
55
  placeholder?: string
80
- /** Minimum value for number inputs */
81
56
  min?: string | number
82
- /** Maximum value for number inputs */
83
57
  max?: string | number
84
- /** Step value for number inputs */
85
58
  step?: string | number
86
- /** Pattern for text inputs (e.g., phone numbers) */
87
59
  pattern?: string
88
- /** Input mode for virtual keyboards */
89
60
  inputMode?: "text" | "numeric" | "decimal" | "email" | "tel" | "url"
90
- /** Autocomplete hint */
91
61
  autoComplete?: string
92
- /** Whether to check spelling */
93
62
  spellCheck?: boolean
94
- /** Minimum length for text inputs */
95
63
  minLength?: number
96
- /** Maximum length for text inputs */
97
64
  maxLength?: number
98
65
  }
99
66
 
@@ -102,196 +69,156 @@ export interface PrimitiveInputProps {
102
69
  // ════════════════════════════════════════════════════════════════════════════
103
70
 
104
71
  interface FieldBase<T extends VisitorDataType = VisitorDataType> {
105
- /** The field type */
106
72
  type: T
107
- /** Raw label from Candid: "__arg0", "_0_" */
108
73
  label: string
109
- /** Pre-formatted display label for UI rendering */
110
74
  displayLabel: string
111
- /** Form field name path for binding */
112
75
  name: string
113
- /** Suggested component type for rendering this field */
114
76
  component: FieldComponentType
115
- /** Rendering hints for UI strategy */
116
77
  renderHint: RenderHint
117
- /** Zod schema for field validation */
118
78
  schema: z.ZodTypeAny
119
- /** Original Candid type name for reference */
120
79
  candidType: string
80
+ defaultValue: unknown
121
81
  }
122
82
 
123
83
  // ════════════════════════════════════════════════════════════════════════════
124
- // Type-Specific Extras
84
+ // Field Extras
125
85
  // ════════════════════════════════════════════════════════════════════════════
126
86
 
127
- /**
128
- * Blob field size limits.
129
- */
130
87
  export interface BlobLimits {
131
- /** Maximum bytes when entering as hex (e.g., 512 bytes) */
132
88
  maxHexBytes: number
133
- /** Maximum file size in bytes (e.g., 2MB ICP limit) */
134
89
  maxFileBytes: number
135
- /** Maximum hex display length before truncation */
136
90
  maxHexDisplayLength: number
137
91
  }
138
92
 
139
- /**
140
- * Validation result for blob input.
141
- */
142
93
  export interface BlobValidationResult {
143
- /** Whether the input is valid */
144
94
  valid: boolean
145
- /** Error message if invalid */
146
95
  error?: string
147
96
  }
148
97
 
98
+ interface RecordExtras {
99
+ fields: FieldNode[]
100
+ defaultValue: Record<string, unknown>
101
+ }
102
+
103
+ interface VariantExtras {
104
+ fields: FieldNode[]
105
+ defaultOption: string
106
+ defaultValue: Record<string, unknown>
107
+ 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
111
+ }
112
+
113
+ interface TupleExtras {
114
+ fields: FieldNode[]
115
+ defaultValue: unknown[]
116
+ }
117
+
118
+ interface OptionalExtras {
119
+ innerField: FieldNode
120
+ defaultValue: null
121
+ getInnerDefault: () => unknown
122
+ isEnabled: (value: unknown) => boolean
123
+ }
124
+
125
+ interface VectorExtras {
126
+ itemField: FieldNode
127
+ defaultValue: unknown[]
128
+ getItemDefault: () => unknown
129
+ createItemField: (index: number, overrides?: { label?: string }) => FieldNode
130
+ }
131
+
132
+ interface BlobExtras {
133
+ itemField: FieldNode
134
+ acceptedFormats: ("hex" | "base64" | "file")[]
135
+ limits: BlobLimits
136
+ normalizeHex: (input: string) => string
137
+ validateInput: (value: string | Uint8Array) => BlobValidationResult
138
+ defaultValue: string
139
+ }
140
+
141
+ interface RecursiveExtras {
142
+ typeName: string
143
+ extract: () => FieldNode
144
+ getInnerDefault: () => unknown
145
+ defaultValue: undefined
146
+ }
147
+
148
+ interface PrincipalExtras {
149
+ maxLength: number
150
+ minLength: number
151
+ format: TextFormat
152
+ inputProps: PrimitiveInputProps
153
+ defaultValue: string
154
+ }
155
+
156
+ interface NumberExtras {
157
+ unsigned: boolean
158
+ isFloat: boolean
159
+ bits?: number
160
+ min?: string
161
+ max?: string
162
+ format: NumberFormat
163
+ inputProps: PrimitiveInputProps
164
+ defaultValue: string
165
+ }
166
+
167
+ interface TextExtras {
168
+ minLength?: number
169
+ maxLength?: number
170
+ multiline?: boolean
171
+ format: TextFormat
172
+ inputProps: PrimitiveInputProps
173
+ defaultValue: string
174
+ }
175
+
176
+ interface BooleanExtras {
177
+ inputProps: PrimitiveInputProps
178
+ defaultValue: boolean
179
+ }
180
+
181
+ interface NullExtras {
182
+ defaultValue: null
183
+ }
184
+
185
+ interface UnknownExtras {
186
+ defaultValue: undefined
187
+ }
188
+
149
189
  type FieldExtras<T extends VisitorDataType> = T extends "record"
150
- ? {
151
- /** Child fields in the record */
152
- fields: FieldNode[]
153
- /** Map of field label to its metadata for quick lookup */
154
- fieldMap: Map<string, FieldNode>
155
- defaultValue: Record<string, unknown>
156
- }
190
+ ? RecordExtras
157
191
  : T extends "variant"
158
- ? {
159
- /** All variant option fields */
160
- fields: FieldNode[]
161
- /** List of variant option names */
162
- options: string[]
163
- /** Default selected option */
164
- defaultOption: string
165
- /** Map of option name to its field metadata */
166
- optionMap: Map<string, FieldNode>
167
- defaultValue: Record<string, unknown>
168
- /** Get default value for a specific option */
169
- getOptionDefault: (option: string) => Record<string, unknown>
170
- /** Get the field for a specific option */
171
- getField: (option: string) => FieldNode
172
- /** Get the currently selected option from a value */
173
- getSelectedOption: (value: Record<string, unknown>) => string
174
- /** Get the selected field from a value */
175
- getSelectedField: (value: Record<string, unknown>) => FieldNode
176
- }
192
+ ? VariantExtras
177
193
  : T extends "tuple"
178
- ? {
179
- /** Tuple element fields in order */
180
- fields: FieldNode[]
181
- defaultValue: unknown[]
182
- }
194
+ ? TupleExtras
183
195
  : T extends "optional"
184
- ? {
185
- /** The inner field when value is present */
186
- innerField: FieldNode
187
- defaultValue: null
188
- /** Get default value when enabling the optional */
189
- getInnerDefault: () => unknown
190
- /** Check if a value represents an enabled optional */
191
- isEnabled: (value: unknown) => boolean
192
- }
196
+ ? OptionalExtras
193
197
  : T extends "vector"
194
- ? {
195
- /** Template field for vector items */
196
- itemField: FieldNode
197
- defaultValue: unknown[]
198
- /** Get a new item with default values */
199
- getItemDefault: () => unknown
200
- /** Create a properly configured item field for a specific index */
201
- createItemField: (
202
- index: number,
203
- overrides?: { label?: string }
204
- ) => FieldNode
205
- }
198
+ ? VectorExtras
206
199
  : T extends "blob"
207
- ? {
208
- /** Item field for individual bytes (nat8) */
209
- itemField: FieldNode
210
- /** Accepted input formats */
211
- acceptedFormats: ("hex" | "base64" | "file")[]
212
- /** Size limits for blob input */
213
- limits: BlobLimits
214
- /** Normalize hex input */
215
- normalizeHex: (input: string) => string
216
- /** Validate blob input value */
217
- validateInput: (
218
- value: string | Uint8Array
219
- ) => BlobValidationResult
220
- defaultValue: string
221
- }
200
+ ? BlobExtras
222
201
  : T extends "recursive"
223
- ? {
224
- /** Type name for the recursive type */
225
- typeName: string
226
- /** Lazily extract the inner field to prevent infinite loops */
227
- extract: () => FieldNode
228
- /** Get default value for the recursive type (evaluates lazily) */
229
- getInnerDefault: () => unknown
230
- defaultValue: undefined
231
- }
202
+ ? RecursiveExtras
232
203
  : T extends "principal"
233
- ? {
234
- maxLength: number
235
- minLength: number
236
- /** Detected format based on label heuristics */
237
- format: TextFormat
238
- /** Pre-computed HTML input props */
239
- inputProps: PrimitiveInputProps
240
- defaultValue: string
241
- }
204
+ ? PrincipalExtras
242
205
  : T extends "number"
243
- ? {
244
- /** Whether this is an unsigned type */
245
- unsigned: boolean
246
- /** Whether this is a floating point type */
247
- isFloat: boolean
248
- /** Bit width if applicable (8, 16, 32, 64, or undefined for unbounded) */
249
- bits?: number
250
- /** Minimum value constraint (for bounded types) */
251
- min?: string
252
- /** Maximum value constraint (for bounded types) */
253
- max?: string
254
- /** Detected format based on label heuristics */
255
- format: NumberFormat
256
- /** Pre-computed HTML input props */
257
- inputProps: PrimitiveInputProps
258
- defaultValue: string
259
- }
206
+ ? NumberExtras
260
207
  : T extends "text"
261
- ? {
262
- /** Minimum length constraint */
263
- minLength?: number
264
- /** Maximum length constraint */
265
- maxLength?: number
266
- /** Whether to render as multiline textarea */
267
- multiline?: boolean
268
- /** Detected format based on label heuristics */
269
- format: TextFormat
270
- /** Pre-computed HTML input props */
271
- inputProps: PrimitiveInputProps
272
- defaultValue: string
273
- }
208
+ ? TextExtras
274
209
  : T extends "boolean"
275
- ? {
276
- /** Pre-computed HTML input props */
277
- inputProps: PrimitiveInputProps
278
- defaultValue: boolean
279
- }
210
+ ? BooleanExtras
280
211
  : T extends "null"
281
- ? {
282
- defaultValue: null
283
- }
212
+ ? NullExtras
284
213
  : T extends "unknown"
285
- ? {
286
- defaultValue: undefined
287
- }
214
+ ? UnknownExtras
288
215
  : {}
289
216
 
290
217
  /**
291
218
  * A unified field node that contains all metadata needed for rendering.
292
219
  */
293
220
  export type FieldNode<T extends VisitorDataType = VisitorDataType> =
294
- FieldBase<T> & FieldExtras<T>
221
+ T extends any ? FieldBase<T> & FieldExtras<T> : never
295
222
 
296
223
  export type RecordField = FieldNode<"record">
297
224
  export type VariantField = FieldNode<"variant">
@@ -308,75 +235,48 @@ export type NullField = FieldNode<"null">
308
235
  export type UnknownField = FieldNode<"unknown">
309
236
 
310
237
  // ════════════════════════════════════════════════════════════════════════════
311
- // Form Metadata - TanStack Form Integration
238
+ // Form Metadata
312
239
  // ════════════════════════════════════════════════════════════════════════════
313
240
 
314
- /**
315
- * Form metadata for a Candid method.
316
- * Contains all information needed to create a TanStack Form instance.
317
- */
318
241
  export interface ArgumentsMeta<
319
242
  A = BaseActor,
320
243
  Name extends FunctionName<A> = FunctionName<A>,
321
244
  > {
322
- /** Whether this is a "query" or "update" function */
323
245
  functionType: FunctionType
324
- /** The function name */
325
246
  functionName: Name
326
- /** Argument field definitions for rendering */
327
247
  fields: FieldNode[]
328
- /** Default values for all arguments (as a tuple) */
329
248
  defaultValues: unknown[]
330
- /** Combined Zod schema for all arguments */
331
249
  schema: z.ZodTuple<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
332
- /** Number of arguments */
333
250
  argCount: number
334
- /** Whether the function takes no arguments */
335
251
  isNoArgs: boolean
336
252
  }
337
253
 
338
- /**
339
- * Options that can be spread into useForm().
340
- * Pre-configured with defaultValues and validators.
341
- */
342
254
  export interface FormOptions {
343
- /** Initial form values */
344
255
  defaultValues: unknown[]
345
- /** Validators using the Zod schema */
346
256
  validators: {
347
257
  onChange: z.ZodTypeAny
348
258
  onBlur: z.ZodTypeAny
349
259
  }
350
260
  }
351
261
 
352
- /**
353
- * Service-level form metadata.
354
- * Maps each method name to its FormMeta.
355
- */
356
262
  export type ArgumentsServiceMeta<A = BaseActor> = {
357
263
  [K in FunctionName<A>]: ArgumentsMeta<A, K>
358
264
  }
359
265
 
360
266
  // ════════════════════════════════════════════════════════════════════════════
361
- // Type Utilities & Guards
267
+ // Type Utilities
362
268
  // ════════════════════════════════════════════════════════════════════════════
363
269
 
364
- /** Extract a specific field type */
365
- export type FieldByType<T extends ArgumentFieldType> = Extract<
270
+ export type FieldByType<T extends VisitorDataType> = Extract<
366
271
  FieldNode,
367
272
  { type: T }
368
273
  >
369
274
 
370
- /**
371
- * Props type helper for field components.
372
- * Use this to type your field components for better DX.
373
- */
374
- export type FieldProps<T extends ArgumentFieldType> = {
275
+ export type FieldProps<T extends VisitorDataType> = {
375
276
  field: FieldByType<T>
376
277
  renderField?: (child: FieldNode) => React.ReactNode
377
278
  }
378
279
 
379
- /** Compound field types that contain other fields */
380
280
  export type CompoundField =
381
281
  | RecordField
382
282
  | VariantField
@@ -385,7 +285,6 @@ export type CompoundField =
385
285
  | VectorField
386
286
  | RecursiveField
387
287
 
388
- /** Primitive field types */
389
288
  export type PrimitiveField =
390
289
  | PrincipalField
391
290
  | NumberField
@@ -30,27 +30,36 @@ const CYCLE_KEYS_REGEX = new RegExp(
30
30
  "i"
31
31
  )
32
32
 
33
- const EMAIL_KEYS_REGEX = /email|mail/i
34
- const PHONE_KEYS_REGEX = /phone|tel|mobile/i
35
- const URL_KEYS_REGEX = /url|link|website/i
36
- const UUID_KEYS_REGEX = /uuid|guid/i
37
- const BITCOIN_KEYS_REGEX = /bitcoin|btc/i
38
- const ETHEREUM_KEYS_REGEX = /ethereum|eth/i
39
33
  const ACCOUNT_ID_KEYS_REGEX =
40
- /account_id|account_identifier|ledger_account|block_hash|transaction_hash|tx_hash/i
41
- const PRINCIPAL_KEYS_REGEX = /canister|principal/i
34
+ /account_identifier|ledger_account|block_hash|transaction_hash|tx_hash/i
35
+
36
+ const tokenize = (label: string): Set<string> => {
37
+ const parts = label
38
+ .replace(/_/g, " ")
39
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
40
+ .toLowerCase()
41
+ .split(/[\s-]+/)
42
+ return new Set(parts)
43
+ }
42
44
 
43
45
  export const checkTextFormat = (label?: string): TextFormat => {
44
46
  if (!label) return "plain"
47
+
45
48
  if (TAMESTAMP_KEYS_REGEX.test(label)) return "timestamp"
46
- if (EMAIL_KEYS_REGEX.test(label)) return "email"
47
- if (PHONE_KEYS_REGEX.test(label)) return "phone"
48
- if (URL_KEYS_REGEX.test(label)) return "url"
49
- if (UUID_KEYS_REGEX.test(label)) return "uuid"
50
- if (BITCOIN_KEYS_REGEX.test(label)) return "btc"
51
- if (ETHEREUM_KEYS_REGEX.test(label)) return "eth"
52
49
  if (ACCOUNT_ID_KEYS_REGEX.test(label)) return "account-id"
53
- if (PRINCIPAL_KEYS_REGEX.test(label)) return "principal"
50
+
51
+ const tokens = tokenize(label)
52
+
53
+ if (tokens.has("email") || tokens.has("mail")) return "email"
54
+ if (tokens.has("phone") || tokens.has("tel") || tokens.has("mobile"))
55
+ return "phone"
56
+ if (tokens.has("url") || tokens.has("link") || tokens.has("website"))
57
+ return "url"
58
+ if (tokens.has("uuid") || tokens.has("guid")) return "uuid"
59
+ if (tokens.has("btc") || tokens.has("bitcoin")) return "btc"
60
+ if (tokens.has("eth") || tokens.has("ethereum")) return "eth"
61
+ if (tokens.has("principal") || tokens.has("canister")) return "principal"
62
+
54
63
  return "plain"
55
64
  }
56
65
 
@@ -58,7 +58,7 @@ describe("ResultFieldVisitor", () => {
58
58
  const ethField = visitor.visitText(IDL.Text, "ethereum_address")
59
59
  expect(ethField.format).toBe("eth")
60
60
 
61
- const accountIdField = visitor.visitText(IDL.Text, "account_id")
61
+ const accountIdField = visitor.visitText(IDL.Text, "account_identifier")
62
62
  expect(accountIdField.format).toBe("account-id")
63
63
 
64
64
  const principalField = visitor.visitText(IDL.Text, "canister_id")