@ic-reactor/candid 3.0.11-beta.1 → 3.0.12-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.
Files changed (40) hide show
  1. package/dist/display-reactor.d.ts +1 -2
  2. package/dist/display-reactor.d.ts.map +1 -1
  3. package/dist/display-reactor.js +1 -1
  4. package/dist/display-reactor.js.map +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/metadata-display-reactor.d.ts +3 -3
  9. package/dist/metadata-display-reactor.d.ts.map +1 -1
  10. package/dist/visitor/arguments/index.d.ts +58 -39
  11. package/dist/visitor/arguments/index.d.ts.map +1 -1
  12. package/dist/visitor/arguments/index.js +273 -81
  13. package/dist/visitor/arguments/index.js.map +1 -1
  14. package/dist/visitor/arguments/types.d.ts +228 -45
  15. package/dist/visitor/arguments/types.d.ts.map +1 -1
  16. package/dist/visitor/arguments/types.js +40 -1
  17. package/dist/visitor/arguments/types.js.map +1 -1
  18. package/dist/visitor/helpers.d.ts +1 -1
  19. package/dist/visitor/helpers.d.ts.map +1 -1
  20. package/dist/visitor/returns/index.d.ts +3 -3
  21. package/dist/visitor/returns/index.d.ts.map +1 -1
  22. package/dist/visitor/returns/index.js +54 -15
  23. package/dist/visitor/returns/index.js.map +1 -1
  24. package/dist/visitor/types.d.ts +2 -3
  25. package/dist/visitor/types.d.ts.map +1 -1
  26. package/dist/visitor/types.js +1 -2
  27. package/dist/visitor/types.js.map +1 -1
  28. package/package.json +6 -3
  29. package/src/display-reactor.ts +4 -6
  30. package/src/index.ts +0 -1
  31. package/src/metadata-display-reactor.ts +6 -6
  32. package/src/visitor/arguments/README.md +230 -0
  33. package/src/visitor/arguments/index.test.ts +144 -51
  34. package/src/visitor/arguments/index.ts +351 -146
  35. package/src/visitor/arguments/schema.test.ts +215 -0
  36. package/src/visitor/arguments/types.ts +287 -61
  37. package/src/visitor/helpers.ts +1 -1
  38. package/src/visitor/returns/index.test.ts +163 -1
  39. package/src/visitor/returns/index.ts +62 -16
  40. package/src/visitor/types.ts +2 -3
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { IDL } from "@icp-sdk/core/candid"
3
+ import { Principal } from "@icp-sdk/core/principal"
4
+ import { ArgumentFieldVisitor } from "./index"
5
+ import * as z from "zod"
6
+
7
+ describe("ArgumentFieldVisitor Schema Generation", () => {
8
+ const visitor = new ArgumentFieldVisitor()
9
+
10
+ // ════════════════════════════════════════════════════════════════════════
11
+ // Primitive Types
12
+ // ════════════════════════════════════════════════════════════════════════
13
+
14
+ describe("Primitive Types", () => {
15
+ it("should generate string schema for text", () => {
16
+ const field = visitor.visitText(IDL.Text, "username")
17
+ const schema = field.schema
18
+
19
+ expect(schema).toBeDefined()
20
+ expect(schema.parse("hello")).toBe("hello")
21
+ expect(() => schema.parse(123)).toThrow()
22
+ })
23
+
24
+ it("should generate boolean schema for bool", () => {
25
+ const field = visitor.visitBool(IDL.Bool, "isActive")
26
+ const schema = field.schema
27
+
28
+ expect(schema.parse(true)).toBe(true)
29
+ expect(schema.parse(false)).toBe(false)
30
+ expect(() => schema.parse("true")).toThrow()
31
+ })
32
+
33
+ it("should generate null schema for null", () => {
34
+ const field = visitor.visitNull(IDL.Null, "void")
35
+ const schema = field.schema
36
+
37
+ expect(schema.parse(null)).toBe(null)
38
+ expect(() => schema.parse(undefined)).toThrow()
39
+ })
40
+
41
+ it("should generate string schema for numbers (form input matching)", () => {
42
+ // The visitor currently generates string schemas for numbers to match form inputs
43
+ const natField = visitor.visitNat(IDL.Nat, "amount")
44
+ expect(natField.schema.parse("100")).toBe("100")
45
+
46
+ const intField = visitor.visitInt(IDL.Int, "balance")
47
+ expect(intField.schema.parse("-50")).toBe("-50")
48
+ })
49
+
50
+ it("should generate principal schema", () => {
51
+ const field = visitor.visitPrincipal(IDL.Principal, "owner")
52
+ const schema = field.schema
53
+
54
+ const p = Principal.fromText("2vxsx-fae")
55
+ // Should accept Principal instance
56
+ expect(schema.parse(p)).toEqual(p)
57
+ // Should accept valid string representation
58
+ expect(schema.parse("2vxsx-fae")).toBe("2vxsx-fae")
59
+
60
+ // Should reject invalid
61
+ expect(() => schema.parse("invalid-principal")).toThrow()
62
+ })
63
+ })
64
+
65
+ // ════════════════════════════════════════════════════════════════════════
66
+ // Compound Types
67
+ // ════════════════════════════════════════════════════════════════════════
68
+
69
+ describe("Record Types", () => {
70
+ it("should generate object schema for record", () => {
71
+ const recordType = IDL.Record({
72
+ name: IDL.Text,
73
+ age: IDL.Nat,
74
+ })
75
+ const field = visitor.visitRecord(
76
+ recordType,
77
+ [
78
+ ["name", IDL.Text],
79
+ ["age", IDL.Nat],
80
+ ],
81
+ "person"
82
+ )
83
+ const schema = field.schema as z.ZodObject<any>
84
+
85
+ const validData = { name: "John", age: "30" }
86
+ expect(schema.parse(validData)).toEqual(validData)
87
+
88
+ expect(() => schema.parse({ name: "John" })).toThrow() // missing age
89
+ expect(() => schema.parse({ name: 123, age: "30" })).toThrow() // invalid type
90
+ })
91
+ })
92
+
93
+ describe("Variant Types", () => {
94
+ it("should generate union schema for variant", () => {
95
+ const variantType = IDL.Variant({
96
+ Ok: IDL.Text,
97
+ Err: IDL.Text,
98
+ })
99
+ const field = visitor.visitVariant(
100
+ variantType,
101
+ [
102
+ ["Ok", IDL.Text],
103
+ ["Err", IDL.Text],
104
+ ],
105
+ "result"
106
+ )
107
+ const schema = field.schema as z.ZodUnion<any>
108
+
109
+ expect(schema.parse({ Ok: "Success" })).toEqual({ Ok: "Success" })
110
+ expect(schema.parse({ Err: "Error" })).toEqual({ Err: "Error" })
111
+
112
+ expect(() => schema.parse({ Other: "value" })).toThrow()
113
+ })
114
+ })
115
+
116
+ describe("Tuple Types", () => {
117
+ it("should generate tuple schema", () => {
118
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
119
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
120
+ const schema = field.schema as z.ZodTuple<any>
121
+
122
+ expect(schema.parse(["key", "100"])).toEqual(["key", "100"])
123
+ expect(() => schema.parse(["key"])).toThrow()
124
+ })
125
+ })
126
+
127
+ describe("Optional Types", () => {
128
+ it("should generate nullable/optional schema", () => {
129
+ const optType = IDL.Opt(IDL.Text)
130
+ const field = visitor.visitOpt(optType, IDL.Text, "maybe")
131
+ const schema = field.schema
132
+
133
+ expect(schema.parse("value")).toBe("value")
134
+ expect(schema.parse(null)).toBe(null)
135
+ expect(schema.parse(undefined)).toBe(null) // nullish() allows undefined -> null
136
+ })
137
+ })
138
+
139
+ describe("Vector Types", () => {
140
+ it("should generate array schema", () => {
141
+ const vecType = IDL.Vec(IDL.Text)
142
+ const field = visitor.visitVec(vecType, IDL.Text, "tags")
143
+ const schema = field.schema
144
+
145
+ expect(schema.parse(["a", "b"])).toEqual(["a", "b"])
146
+ expect(schema.parse([])).toEqual([])
147
+ expect(() => schema.parse("not array")).toThrow()
148
+ })
149
+
150
+ it("should generate special schema for blob", () => {
151
+ const blobType = IDL.Vec(IDL.Nat8)
152
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
153
+ const schema = field.schema
154
+
155
+ // Blob accepts string (hex) or array of numbers
156
+ expect(schema.parse("deadbeef")).toBe("deadbeef")
157
+ expect(schema.parse([1, 2, 3])).toEqual([1, 2, 3])
158
+ expect(() => schema.parse(123)).toThrow()
159
+ })
160
+ })
161
+
162
+ // ════════════════════════════════════════════════════════════════════════
163
+ // Recursive Types
164
+ // ════════════════════════════════════════════════════════════════════════
165
+
166
+ describe("Recursive Types", () => {
167
+ it("should handle recursive schemas", () => {
168
+ const List = IDL.Rec()
169
+ const ListVariant = IDL.Variant({
170
+ Nil: IDL.Null,
171
+ Cons: IDL.Record({
172
+ head: IDL.Nat,
173
+ tail: List,
174
+ }),
175
+ })
176
+ List.fill(ListVariant)
177
+
178
+ const field = visitor.visitRec(List, ListVariant, "list")
179
+ const schema = field.schema
180
+
181
+ const validList = {
182
+ Cons: {
183
+ head: "1",
184
+ tail: {
185
+ Cons: {
186
+ head: "2",
187
+ tail: { Nil: null },
188
+ },
189
+ },
190
+ },
191
+ }
192
+
193
+ expect(schema.parse(validList)).toEqual(validList)
194
+ })
195
+ })
196
+
197
+ // ════════════════════════════════════════════════════════════════════════
198
+ // Method Schema
199
+ // ════════════════════════════════════════════════════════════════════════
200
+
201
+ describe("Method Schema", () => {
202
+ it("should generate tuple schema for function arguments", () => {
203
+ const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
204
+ const meta = visitor.visitFunc(funcType, "myMethod")
205
+
206
+ const schema = meta.schema
207
+ expect(schema).toBeDefined()
208
+
209
+ const validArgs = ["hello", "123"]
210
+ expect(schema.parse(validArgs)).toEqual(validArgs)
211
+
212
+ expect(() => schema.parse(["hello"])).toThrow()
213
+ })
214
+ })
215
+ })
@@ -1,4 +1,5 @@
1
1
  import type { BaseActor, FunctionName, FunctionType } from "@ic-reactor/core"
2
+ import * as z from "zod"
2
3
 
3
4
  // ════════════════════════════════════════════════════════════════════════════
4
5
  // Field Type Union
@@ -19,150 +20,375 @@ export type ArgumentFieldType =
19
20
  | "null"
20
21
  | "unknown"
21
22
 
23
+ // ════════════════════════════════════════════════════════════════════════════
24
+ // UI Hints for Form Rendering
25
+ // ════════════════════════════════════════════════════════════════════════════
26
+
27
+ export interface FieldUIHints {
28
+ /** Placeholder text for the input */
29
+ placeholder?: string
30
+ /** Description or help text for the field */
31
+ description?: string
32
+ /** Whether the field is required */
33
+ required?: boolean
34
+ /** Whether the field should be disabled */
35
+ disabled?: boolean
36
+ /** Additional CSS class names */
37
+ className?: string
38
+ }
39
+
22
40
  // ════════════════════════════════════════════════════════════════════════════
23
41
  // Base Field Interface
24
42
  // ════════════════════════════════════════════════════════════════════════════
25
43
 
26
- export interface ArgumentFieldBase {
44
+ export interface FieldBase<TValue = unknown> {
27
45
  /** The field type */
28
46
  type: ArgumentFieldType
29
47
  /** Human-readable label from Candid */
30
48
  label: string
31
- /** Dot-notation path for form binding (e.g., "0.owner", "[0].to") */
32
- path: string
49
+ /**
50
+ * Form field name path for binding.
51
+ * Uses bracket notation for array indices: `[0]`, `args[0].owner`, `tags[1]`
52
+ * Compatible with TanStack Form's `form.Field` name prop.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * <form.Field name={field.name}>
57
+ * {(fieldApi) => <input {...} />}
58
+ * </form.Field>
59
+ * ```
60
+ */
61
+ name: string
62
+ /** Zod schema for field validation */
63
+ schema: z.ZodTypeAny
64
+ /** Default value for the field */
65
+ defaultValue: TValue
66
+ /** Original Candid type name for reference */
67
+ candidType?: string
68
+ /** UI rendering hints */
69
+ ui?: FieldUIHints
33
70
  }
34
71
 
35
72
  // ════════════════════════════════════════════════════════════════════════════
36
73
  // Compound Types
37
74
  // ════════════════════════════════════════════════════════════════════════════
38
75
 
39
- export interface RecordArgumentField extends ArgumentFieldBase {
76
+ export interface RecordField extends FieldBase<Record<string, unknown>> {
40
77
  type: "record"
41
- fields: ArgumentField[]
42
- defaultValues: Record<string, unknown>
78
+ /** Child fields in the record */
79
+ fields: Field[]
80
+ /** Map of field label to its metadata for quick lookup */
81
+ fieldMap: Map<string, Field>
43
82
  }
44
83
 
45
- export interface VariantArgumentField extends ArgumentFieldBase {
84
+ export interface VariantField extends FieldBase<Record<string, unknown>> {
46
85
  type: "variant"
47
- fields: ArgumentField[]
86
+ /** All variant option fields */
87
+ fields: Field[]
88
+ /** List of variant option names */
48
89
  options: string[]
90
+ /** Default selected option */
49
91
  defaultOption: string
50
- defaultValues: Record<string, unknown>
92
+ /** Map of option name to its field metadata */
93
+ optionMap: Map<string, Field>
94
+ /**
95
+ * Get default value for a specific option.
96
+ * Useful when switching between variant options.
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * const handleOptionChange = (newOption: string) => {
101
+ * const newDefault = field.getOptionDefault(newOption)
102
+ * fieldApi.handleChange(newDefault)
103
+ * }
104
+ * ```
105
+ */
106
+ getOptionDefault: (option: string) => Record<string, unknown>
51
107
  }
52
108
 
53
- export interface TupleArgumentField extends ArgumentFieldBase {
109
+ export interface TupleField extends FieldBase<unknown[]> {
54
110
  type: "tuple"
55
- fields: ArgumentField[]
56
- defaultValues: unknown[]
111
+ /** Tuple element fields in order */
112
+ fields: Field[]
57
113
  }
58
114
 
59
- export interface OptionalArgumentField extends ArgumentFieldBase {
115
+ export interface OptionalField extends FieldBase<null> {
60
116
  type: "optional"
61
- innerField: ArgumentField
62
- defaultValue: null
117
+ /** The inner field when value is present */
118
+ innerField: Field
119
+ /**
120
+ * Get default value when enabling the optional.
121
+ * Returns the inner field's default value.
122
+ *
123
+ * @example
124
+ * ```tsx
125
+ * const handleToggle = (enabled: boolean) => {
126
+ * if (enabled) {
127
+ * fieldApi.handleChange(field.getInnerDefault())
128
+ * } else {
129
+ * fieldApi.handleChange(null)
130
+ * }
131
+ * }
132
+ * ```
133
+ */
134
+ getInnerDefault: () => unknown
63
135
  }
64
136
 
65
- export interface VectorArgumentField extends ArgumentFieldBase {
137
+ export interface VectorField extends FieldBase<unknown[]> {
66
138
  type: "vector"
67
- itemField: ArgumentField
68
- defaultValue: []
139
+ /** Template field for vector items */
140
+ itemField: Field
141
+ /**
142
+ * Get a new item with default values.
143
+ * Used when adding items to the vector.
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * <button onClick={() => fieldApi.pushValue(field.getItemDefault())}>
148
+ * Add Item
149
+ * </button>
150
+ * ```
151
+ */
152
+ getItemDefault: () => unknown
69
153
  }
70
154
 
71
- export interface BlobArgumentField extends ArgumentFieldBase {
155
+ export interface BlobField extends FieldBase<string> {
72
156
  type: "blob"
73
- itemField: ArgumentField
74
- /** Default is empty hex string */
75
- defaultValue: string
157
+ /** Item field for individual bytes (nat8) */
158
+ itemField: Field
159
+ /** Accepted input formats */
160
+ acceptedFormats: ("hex" | "base64" | "file")[]
76
161
  }
77
162
 
78
- export interface RecursiveArgumentField extends ArgumentFieldBase {
163
+ export interface RecursiveField extends FieldBase<undefined> {
79
164
  type: "recursive"
165
+ /** Type name for the recursive type */
166
+ typeName: string
80
167
  /** Lazily extract the inner field to prevent infinite loops */
81
- extract: () => ArgumentField
168
+ extract: () => Field
169
+ /**
170
+ * Get default value for the recursive type.
171
+ * Evaluates the inner type on demand.
172
+ */
173
+ getInnerDefault: () => unknown
82
174
  }
83
175
 
84
176
  // ════════════════════════════════════════════════════════════════════════════
85
177
  // Primitive Types
86
178
  // ════════════════════════════════════════════════════════════════════════════
87
179
 
88
- export interface PrincipalArgumentField extends ArgumentFieldBase {
180
+ export interface PrincipalField extends FieldBase<string> {
89
181
  type: "principal"
90
- /** Display format: string */
91
- defaultValue: string
92
182
  maxLength: number
93
183
  minLength: number
94
184
  }
95
185
 
96
- export interface NumberArgumentField extends ArgumentFieldBase {
186
+ export interface NumberField extends FieldBase<string> {
97
187
  type: "number"
98
- /** Display format: string (for bigint compatibility) */
99
- defaultValue: string
100
- /** Original Candid type: nat, int, nat64, etc. */
188
+ /**
189
+ * Original Candid type: nat, int, nat8, nat16, nat32, nat64, int8, int16, int32, int64, float32, float64
190
+ */
101
191
  candidType: string
192
+ /** Whether this is an unsigned type */
193
+ unsigned: boolean
194
+ /** Whether this is a floating point type */
195
+ isFloat: boolean
196
+ /** Bit width if applicable (8, 16, 32, 64, or undefined for unbounded) */
197
+ bits?: number
198
+ /** Minimum value constraint (for bounded types) */
199
+ min?: string
200
+ /** Maximum value constraint (for bounded types) */
201
+ max?: string
102
202
  }
103
203
 
104
- export interface TextArgumentField extends ArgumentFieldBase {
204
+ export interface TextField extends FieldBase<string> {
105
205
  type: "text"
106
- defaultValue: string
206
+ /** Minimum length constraint */
207
+ minLength?: number
208
+ /** Maximum length constraint */
209
+ maxLength?: number
210
+ /** Whether to render as multiline textarea */
211
+ multiline?: boolean
107
212
  }
108
213
 
109
- export interface BooleanArgumentField extends ArgumentFieldBase {
214
+ export interface BooleanField extends FieldBase<boolean> {
110
215
  type: "boolean"
111
- defaultValue: boolean
112
216
  }
113
217
 
114
- export interface NullArgumentField extends ArgumentFieldBase {
218
+ export interface NullField extends FieldBase<null> {
115
219
  type: "null"
116
- defaultValue: null
117
220
  }
118
221
 
119
- export interface UnknownArgumentField extends ArgumentFieldBase {
222
+ export interface UnknownField extends FieldBase<undefined> {
120
223
  type: "unknown"
121
- defaultValue: undefined
122
224
  }
123
225
 
124
226
  // ════════════════════════════════════════════════════════════════════════════
125
227
  // Union Type
126
228
  // ════════════════════════════════════════════════════════════════════════════
127
229
 
128
- export type ArgumentField =
129
- | RecordArgumentField
130
- | VariantArgumentField
131
- | TupleArgumentField
132
- | OptionalArgumentField
133
- | VectorArgumentField
134
- | BlobArgumentField
135
- | RecursiveArgumentField
136
- | PrincipalArgumentField
137
- | NumberArgumentField
138
- | TextArgumentField
139
- | BooleanArgumentField
140
- | NullArgumentField
141
- | UnknownArgumentField
230
+ export type Field =
231
+ | RecordField
232
+ | VariantField
233
+ | TupleField
234
+ | OptionalField
235
+ | VectorField
236
+ | BlobField
237
+ | RecursiveField
238
+ | PrincipalField
239
+ | NumberField
240
+ | TextField
241
+ | BooleanField
242
+ | NullField
243
+ | UnknownField
142
244
 
143
245
  // ════════════════════════════════════════════════════════════════════════════
144
- // Method & Service Level
246
+ // Form Metadata - TanStack Form Integration
145
247
  // ════════════════════════════════════════════════════════════════════════════
146
248
 
147
- export interface MethodArgumentsMeta<
249
+ /**
250
+ * Form metadata for a Candid method.
251
+ * Contains all information needed to create a TanStack Form instance.
252
+ *
253
+ * @example
254
+ * ```tsx
255
+ * import { useForm } from '@tanstack/react-form'
256
+ *
257
+ * function MethodForm({ meta }: { meta: FormMeta }) {
258
+ * const form = useForm({
259
+ * ...meta.formOptions,
260
+ * onSubmit: async ({ value }) => {
261
+ * await actor[meta.functionName](...value)
262
+ * }
263
+ * })
264
+ *
265
+ * return (
266
+ * <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
267
+ * {meta.fields.map(field => (
268
+ * <form.Field key={field.name} name={field.name}>
269
+ * {(fieldApi) => <DynamicInput field={field} fieldApi={fieldApi} />}
270
+ * </form.Field>
271
+ * ))}
272
+ * <button type="submit">Submit</button>
273
+ * </form>
274
+ * )
275
+ * }
276
+ * ```
277
+ */
278
+ export interface ArgumentsMeta<
148
279
  A = BaseActor,
149
280
  Name extends FunctionName<A> = FunctionName<A>,
150
281
  > {
282
+ /** Whether this is a "query" or "update" function */
151
283
  functionType: FunctionType
284
+ /** The function name */
152
285
  functionName: Name
153
- fields: ArgumentField[]
286
+ /** Argument field definitions for rendering */
287
+ fields: Field[]
288
+ /** Default values for all arguments (as a tuple) */
154
289
  defaultValues: unknown[]
290
+ /** Combined Zod schema for all arguments */
291
+ schema: z.ZodTuple<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
292
+ /** Number of arguments */
293
+ argCount: number
294
+ /** Whether the function takes no arguments */
295
+ isNoArgs: boolean
155
296
  }
156
297
 
157
- export type ServiceArgumentsMeta<A = BaseActor> = {
158
- [K in FunctionName<A>]: MethodArgumentsMeta<A, K>
298
+ /**
299
+ * Options that can be spread into useForm().
300
+ * Pre-configured with defaultValues and validators.
301
+ */
302
+ export interface FormOptions {
303
+ /** Initial form values */
304
+ defaultValues: unknown[]
305
+ /** Validators using the Zod schema */
306
+ validators: {
307
+ onChange: z.ZodTypeAny
308
+ onBlur: z.ZodTypeAny
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Service-level form metadata.
314
+ * Maps each method name to its FormMeta.
315
+ */
316
+ export type ArgumentsServiceMeta<A = BaseActor> = {
317
+ [K in FunctionName<A>]: ArgumentsMeta<A, K>
159
318
  }
160
319
 
161
320
  // ════════════════════════════════════════════════════════════════════════════
162
- // Type Utilities
321
+ // Type Utilities & Guards
163
322
  // ════════════════════════════════════════════════════════════════════════════
164
323
 
165
- export type ArgumentFieldByType<T extends ArgumentFieldType> = Extract<
166
- ArgumentField,
324
+ /** Extract a specific field type */
325
+ export type FieldByType<T extends ArgumentFieldType> = Extract<
326
+ Field,
167
327
  { type: T }
168
328
  >
329
+
330
+ /** Compound field types that contain other fields */
331
+ export type CompoundField =
332
+ | RecordField
333
+ | VariantField
334
+ | TupleField
335
+ | OptionalField
336
+ | VectorField
337
+ | RecursiveField
338
+
339
+ /** Primitive field types */
340
+ export type PrimitiveField =
341
+ | PrincipalField
342
+ | NumberField
343
+ | TextField
344
+ | BooleanField
345
+ | NullField
346
+
347
+ /**
348
+ * Type guard for checking specific field types.
349
+ *
350
+ * @example
351
+ * ```tsx
352
+ * function FieldInput({ field }: { field: Field }) {
353
+ * if (isFieldType(field, 'record')) {
354
+ * // field is now typed as RecordField
355
+ * return <RecordInput field={field} />
356
+ * }
357
+ * if (isFieldType(field, 'text')) {
358
+ * // field is now typed as TextField
359
+ * return <TextInput field={field} />
360
+ * }
361
+ * // ...
362
+ * }
363
+ * ```
364
+ */
365
+ export function isFieldType<T extends ArgumentFieldType>(
366
+ field: Field,
367
+ type: T
368
+ ): field is FieldByType<T> {
369
+ return field.type === type
370
+ }
371
+
372
+ /** Check if a field is a compound type (contains other fields) */
373
+ export function isCompoundField(field: Field): field is CompoundField {
374
+ return [
375
+ "record",
376
+ "variant",
377
+ "tuple",
378
+ "optional",
379
+ "vector",
380
+ "recursive",
381
+ ].includes(field.type)
382
+ }
383
+
384
+ /** Check if a field is a primitive type */
385
+ export function isPrimitiveField(field: Field): field is PrimitiveField {
386
+ return ["principal", "number", "text", "boolean", "null"].includes(field.type)
387
+ }
388
+
389
+ /** Check if a field has children (for iteration) */
390
+ export function hasChildFields(
391
+ field: Field
392
+ ): field is RecordField | VariantField | TupleField {
393
+ return "fields" in field && Array.isArray((field as RecordField).fields)
394
+ }
@@ -1,4 +1,4 @@
1
- import { IDL } from "./types"
1
+ import { IDL } from "@icp-sdk/core/candid"
2
2
 
3
3
  export const extractAndSortArgs = <T extends Record<string, unknown>>(
4
4
  argsObject: T