@ic-reactor/candid 3.0.11-beta.2 → 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.
- package/dist/display-reactor.d.ts +1 -2
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +1 -1
- package/dist/display-reactor.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +3 -3
- package/dist/metadata-display-reactor.d.ts.map +1 -1
- package/dist/visitor/arguments/index.d.ts +58 -39
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +273 -81
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +228 -45
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/arguments/types.js +40 -1
- package/dist/visitor/arguments/types.js.map +1 -1
- package/dist/visitor/helpers.d.ts +1 -1
- package/dist/visitor/helpers.d.ts.map +1 -1
- package/dist/visitor/returns/index.d.ts +1 -2
- package/dist/visitor/returns/index.d.ts.map +1 -1
- package/dist/visitor/returns/index.js +2 -3
- package/dist/visitor/returns/index.js.map +1 -1
- package/dist/visitor/types.d.ts +2 -3
- package/dist/visitor/types.d.ts.map +1 -1
- package/dist/visitor/types.js +1 -2
- package/dist/visitor/types.js.map +1 -1
- package/package.json +6 -3
- package/src/display-reactor.ts +4 -6
- package/src/index.ts +0 -1
- package/src/metadata-display-reactor.ts +6 -6
- package/src/visitor/arguments/README.md +230 -0
- package/src/visitor/arguments/index.test.ts +144 -51
- package/src/visitor/arguments/index.ts +351 -146
- package/src/visitor/arguments/schema.test.ts +215 -0
- package/src/visitor/arguments/types.ts +287 -61
- package/src/visitor/helpers.ts +1 -1
- package/src/visitor/returns/index.test.ts +1 -1
- package/src/visitor/returns/index.ts +2 -3
- 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
|
|
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
|
-
/**
|
|
32
|
-
|
|
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
|
|
76
|
+
export interface RecordField extends FieldBase<Record<string, unknown>> {
|
|
40
77
|
type: "record"
|
|
41
|
-
fields
|
|
42
|
-
|
|
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
|
|
84
|
+
export interface VariantField extends FieldBase<Record<string, unknown>> {
|
|
46
85
|
type: "variant"
|
|
47
|
-
fields
|
|
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
|
-
|
|
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
|
|
109
|
+
export interface TupleField extends FieldBase<unknown[]> {
|
|
54
110
|
type: "tuple"
|
|
55
|
-
fields
|
|
56
|
-
|
|
111
|
+
/** Tuple element fields in order */
|
|
112
|
+
fields: Field[]
|
|
57
113
|
}
|
|
58
114
|
|
|
59
|
-
export interface
|
|
115
|
+
export interface OptionalField extends FieldBase<null> {
|
|
60
116
|
type: "optional"
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
137
|
+
export interface VectorField extends FieldBase<unknown[]> {
|
|
66
138
|
type: "vector"
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
155
|
+
export interface BlobField extends FieldBase<string> {
|
|
72
156
|
type: "blob"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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: () =>
|
|
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
|
|
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
|
|
186
|
+
export interface NumberField extends FieldBase<string> {
|
|
97
187
|
type: "number"
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
204
|
+
export interface TextField extends FieldBase<string> {
|
|
105
205
|
type: "text"
|
|
106
|
-
|
|
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
|
|
214
|
+
export interface BooleanField extends FieldBase<boolean> {
|
|
110
215
|
type: "boolean"
|
|
111
|
-
defaultValue: boolean
|
|
112
216
|
}
|
|
113
217
|
|
|
114
|
-
export interface
|
|
218
|
+
export interface NullField extends FieldBase<null> {
|
|
115
219
|
type: "null"
|
|
116
|
-
defaultValue: null
|
|
117
220
|
}
|
|
118
221
|
|
|
119
|
-
export interface
|
|
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
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
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
|
-
//
|
|
246
|
+
// Form Metadata - TanStack Form Integration
|
|
145
247
|
// ════════════════════════════════════════════════════════════════════════════
|
|
146
248
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
+
}
|
package/src/visitor/helpers.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
|
-
import { IDL } from "../types"
|
|
3
2
|
import { ResultFieldVisitor } from "./index"
|
|
4
3
|
import type {
|
|
5
4
|
ResultNode,
|
|
@@ -19,6 +18,7 @@ import type {
|
|
|
19
18
|
MethodMeta,
|
|
20
19
|
ServiceMeta,
|
|
21
20
|
} from "./types"
|
|
21
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
22
22
|
|
|
23
23
|
describe("ResultFieldVisitor", () => {
|
|
24
24
|
const visitor = new ResultFieldVisitor()
|