@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,230 @@
|
|
|
1
|
+
# Argument Field Visitor
|
|
2
|
+
|
|
3
|
+
The `ArgumentFieldVisitor` traverses Candid IDL types to generate two things:
|
|
4
|
+
|
|
5
|
+
1. **Field Metadata**: Structure, labels, names (for form binding), and default values for rendering form fields.
|
|
6
|
+
2. **Validation Schema**: A Zod schema for validating form inputs.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### 1. Initialize the Visitor
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { ArgumentFieldVisitor } from "@ic-reactor/candid"
|
|
14
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
15
|
+
|
|
16
|
+
const visitor = new ArgumentFieldVisitor()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Generate Metadata & Schema
|
|
20
|
+
|
|
21
|
+
You can visit a single function or an entire service.
|
|
22
|
+
|
|
23
|
+
#### For a Service
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const serviceMeta = visitor.visitService(idlFactory({ IDL }))
|
|
27
|
+
const transferMeta = serviceMeta["icrc1_transfer"]
|
|
28
|
+
|
|
29
|
+
console.log(transferMeta)
|
|
30
|
+
// Output:
|
|
31
|
+
// {
|
|
32
|
+
// functionName: "icrc1_transfer",
|
|
33
|
+
// functionType: "update",
|
|
34
|
+
// fields: [...], // Field definitions for rendering
|
|
35
|
+
// defaultValues: [...], // Default values for the form (array of argument defaults)
|
|
36
|
+
// schema: ZodSchema, // Zod schema for validation
|
|
37
|
+
// argCount: 1, // Number of arguments
|
|
38
|
+
// isNoArgs: false // Whether the function takes no arguments
|
|
39
|
+
// }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### For a Single Function
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
|
|
46
|
+
const meta = visitor.visitFunc(funcType, "myMethod")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Field Properties
|
|
50
|
+
|
|
51
|
+
Each field in `meta.fields` has the following properties:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
{
|
|
55
|
+
type: "text" | "number" | "boolean" | "principal" | "record" | "variant" | ...,
|
|
56
|
+
label: "fieldName", // Human-readable label
|
|
57
|
+
name: "[0].field.nested", // TanStack Form compatible path
|
|
58
|
+
defaultValue: ..., // Default value for this field
|
|
59
|
+
schema: ZodSchema, // Zod schema for this field
|
|
60
|
+
candidType: "text", // Original Candid type
|
|
61
|
+
ui: { // Optional UI hints
|
|
62
|
+
placeholder: "e.g. 100",
|
|
63
|
+
},
|
|
64
|
+
// Type-specific properties:
|
|
65
|
+
// - For "number" fields (Nat8, Int32, Float): min, max, unsigned, isFloat, bits
|
|
66
|
+
// - For "text" fields (Nat, Int, Nat64): (handled as text for BigInt support)
|
|
67
|
+
// - For variants: options, optionMap, getOptionDefault()
|
|
68
|
+
// - For vectors: itemField, getItemDefault()
|
|
69
|
+
// - For optionals: innerField, getInnerDefault()
|
|
70
|
+
// - For records: fields, fieldMap
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Special Handling & Validation
|
|
75
|
+
|
|
76
|
+
#### BigInts as Text
|
|
77
|
+
|
|
78
|
+
Large integer types (`Nat`, `Int`, `Nat64`, `Int64`, `Nat32` > 32-bit representations) are generated with `type: "text"`.
|
|
79
|
+
|
|
80
|
+
- **Reason**: Standard JavaScript numbers lose precision for values > `2^53 - 1`. HTML number inputs can be unreliable for large integers.
|
|
81
|
+
- **Validation**: The Zod schema strictly validates these as **strings containing only digits** (or sign for signed types).
|
|
82
|
+
- **Label**: They retain their `candidType` (e.g. `nat`) for reference.
|
|
83
|
+
|
|
84
|
+
#### Strict Validation
|
|
85
|
+
|
|
86
|
+
- **Required Fields**: Text and Number fields include `.min(1, "Required")`. Empty strings are rejected.
|
|
87
|
+
- **Integers**: Regex validation ensures only digits (no decimals).
|
|
88
|
+
- **Floats**: Float32/Float64 allow decimal points (e.g., `123.1`) and are validated using standard `!isNaN(Number(val))`.
|
|
89
|
+
- **Principals**: Validated using `Principal.fromText()`. Empty strings are rejected.
|
|
90
|
+
|
|
91
|
+
#### Optional Fields
|
|
92
|
+
|
|
93
|
+
- **Behavior**: Optional fields (`Opt`) wrap the inner schema.
|
|
94
|
+
- **Empty Handling**: An empty string input (`""`) is automatically transformed to `null` (Candid `null` / `None`), ensuring optional fields can be cleared.
|
|
95
|
+
|
|
96
|
+
### 5. Integration with TanStack Form
|
|
97
|
+
|
|
98
|
+
The visitor is optimized for standard form libraries like TanStack Form.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { useForm } from "@tanstack/react-form"
|
|
102
|
+
|
|
103
|
+
function MethodForm({ meta }) {
|
|
104
|
+
const form = useForm({
|
|
105
|
+
defaultValues: meta.defaultValues,
|
|
106
|
+
validators: {
|
|
107
|
+
onChange: meta.schema, // Use generated Zod schema for validation
|
|
108
|
+
},
|
|
109
|
+
onSubmit: async ({ value }) => {
|
|
110
|
+
console.log("Structured Data:", value)
|
|
111
|
+
// value is ready to be passed to strict Candid adapters
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<form
|
|
117
|
+
onSubmit={(e) => {
|
|
118
|
+
e.preventDefault()
|
|
119
|
+
e.stopPropagation()
|
|
120
|
+
form.handleSubmit()
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{meta.fields.map((field) => (
|
|
124
|
+
<form.Field key={field.name} name={field.name}>
|
|
125
|
+
{(fieldApi) => (
|
|
126
|
+
<div>
|
|
127
|
+
<label>{field.label}</label>
|
|
128
|
+
<input
|
|
129
|
+
type={
|
|
130
|
+
field.type === "text" || field.type === "principal"
|
|
131
|
+
? "text"
|
|
132
|
+
: field.type
|
|
133
|
+
}
|
|
134
|
+
value={fieldApi.state.value}
|
|
135
|
+
onChange={(e) => fieldApi.handleChange(e.target.value)}
|
|
136
|
+
placeholder={field.ui?.placeholder}
|
|
137
|
+
/>
|
|
138
|
+
{fieldApi.state.meta.errors.map((err) => (
|
|
139
|
+
<span key={err} className="error">
|
|
140
|
+
{err}
|
|
141
|
+
</span>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</form.Field>
|
|
146
|
+
))}
|
|
147
|
+
<button type="submit">Submit</button>
|
|
148
|
+
</form>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 6. Dynamic Fields
|
|
154
|
+
|
|
155
|
+
For **Vectors** and **Variants**, you can access helper paths dynamically:
|
|
156
|
+
|
|
157
|
+
- **Vector**: Field name `items` -> Item name `items[0]`, `items[1]`.
|
|
158
|
+
- **Record**: Field name `user` -> Nested `user.name`.
|
|
159
|
+
|
|
160
|
+
The `name` property in the metadata is pre-calculated to match this structure (e.g., `[0].args.user.name` if it's the first argument).
|
|
161
|
+
|
|
162
|
+
### 7. Working with Vectors (Arrays)
|
|
163
|
+
|
|
164
|
+
Use helper methods like `getItemDefault()` to manage array items.
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
function VectorField({ field, form }) {
|
|
168
|
+
return (
|
|
169
|
+
<form.Field name={field.name} mode="array">
|
|
170
|
+
{(arrayFieldApi) => (
|
|
171
|
+
<div>
|
|
172
|
+
{arrayFieldApi.state.value.map((_, index) => (
|
|
173
|
+
/* Render items using field.name + [index] */
|
|
174
|
+
))}
|
|
175
|
+
<button
|
|
176
|
+
onClick={() => arrayFieldApi.pushValue(field.getItemDefault())}
|
|
177
|
+
>
|
|
178
|
+
Add Item
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</form.Field>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 8. Working with Variants
|
|
188
|
+
|
|
189
|
+
Use `optionMap` for lookup and `getOptionDefault()` for switching types.
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<select
|
|
193
|
+
onChange={(e) => {
|
|
194
|
+
// Switch variant type and default value
|
|
195
|
+
const newValue = field.getOptionDefault(e.target.value)
|
|
196
|
+
form.setFieldValue(field.name, newValue)
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{field.options.map((opt) => (
|
|
200
|
+
<option key={opt}>{opt}</option>
|
|
201
|
+
))}
|
|
202
|
+
</select>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 9. Type Guards
|
|
206
|
+
|
|
207
|
+
The library exports type guard utilities for safer type narrowing:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import {
|
|
211
|
+
isFieldType,
|
|
212
|
+
isCompoundField,
|
|
213
|
+
isPrimitiveField,
|
|
214
|
+
} from "@ic-reactor/candid"
|
|
215
|
+
|
|
216
|
+
if (isFieldType(field, "record")) {
|
|
217
|
+
// field is RecordArgumentField
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 10. Recursive Types
|
|
222
|
+
|
|
223
|
+
Recursive types (like linked lists) use `z.lazy()` schemas. Use `field.extract()` to get the inner definition when rendering.
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
function RecursiveField({ field }) {
|
|
227
|
+
const innerField = useMemo(() => field.extract(), [field])
|
|
228
|
+
return <DynamicField field={innerField} ... />
|
|
229
|
+
}
|
|
230
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import { IDL } from "@icp-sdk/core/candid"
|
|
3
|
-
import { ArgumentFieldVisitor } from "./index"
|
|
3
|
+
import { ArgumentFieldVisitor, VectorField } from "./index"
|
|
4
4
|
|
|
5
5
|
describe("ArgumentFieldVisitor", () => {
|
|
6
6
|
const visitor = new ArgumentFieldVisitor()
|
|
@@ -53,51 +53,70 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
53
53
|
it("should handle nat type", () => {
|
|
54
54
|
const field = visitor.visitNat(IDL.Nat, "amount")
|
|
55
55
|
|
|
56
|
-
expect(field.type).toBe("
|
|
56
|
+
expect(field.type).toBe("text")
|
|
57
57
|
expect(field.label).toBe("amount")
|
|
58
58
|
expect(field.candidType).toBe("nat")
|
|
59
59
|
expect(field.defaultValue).toBe("")
|
|
60
|
+
// TextField doesn't have isFloat or unsigned properties in the types
|
|
60
61
|
})
|
|
61
62
|
|
|
62
63
|
it("should handle int type", () => {
|
|
63
64
|
const field = visitor.visitInt(IDL.Int, "balance")
|
|
64
65
|
|
|
65
|
-
expect(field.type).toBe("
|
|
66
|
+
expect(field.type).toBe("text")
|
|
66
67
|
expect(field.candidType).toBe("int")
|
|
67
68
|
})
|
|
68
69
|
|
|
69
|
-
it("should handle nat8 type", () => {
|
|
70
|
+
it("should handle nat8 type with min/max", () => {
|
|
70
71
|
const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
if (field.type === "number") {
|
|
74
|
+
expect(field.type).toBe("number")
|
|
75
|
+
expect(field.candidType).toBe("nat8")
|
|
76
|
+
expect(field.bits).toBe(8)
|
|
77
|
+
expect(field.min).toBe("0")
|
|
78
|
+
expect(field.max).toBe("255")
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error("Expected number field for nat8")
|
|
81
|
+
}
|
|
74
82
|
})
|
|
75
83
|
|
|
76
|
-
it("should handle nat64 type", () => {
|
|
84
|
+
it("should handle nat64 type with min/max", () => {
|
|
77
85
|
const field = visitor.visitFixedNat(
|
|
78
86
|
IDL.Nat64 as IDL.FixedNatClass,
|
|
79
87
|
"timestamp"
|
|
80
88
|
)
|
|
81
89
|
|
|
82
|
-
expect(field.type).toBe("
|
|
90
|
+
expect(field.type).toBe("text")
|
|
83
91
|
expect(field.candidType).toBe("nat64")
|
|
92
|
+
// Large numbers are now text fields and don't carry bit/min/max metadata in the same way
|
|
84
93
|
})
|
|
85
94
|
|
|
86
|
-
it("should handle int32 type", () => {
|
|
95
|
+
it("should handle int32 type with min/max", () => {
|
|
87
96
|
const field = visitor.visitFixedInt(
|
|
88
97
|
IDL.Int32 as IDL.FixedIntClass,
|
|
89
98
|
"count"
|
|
90
99
|
)
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
if (field.type === "number") {
|
|
102
|
+
expect(field.type).toBe("number")
|
|
103
|
+
expect(field.candidType).toBe("int32")
|
|
104
|
+
expect(field.bits).toBe(32)
|
|
105
|
+
expect(field.min).toBe("-2147483648")
|
|
106
|
+
expect(field.max).toBe("2147483647")
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error("Expected number field for int32")
|
|
109
|
+
}
|
|
94
110
|
})
|
|
95
111
|
|
|
96
112
|
it("should handle float64 type", () => {
|
|
97
113
|
const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "price")
|
|
98
114
|
|
|
99
115
|
expect(field.type).toBe("number")
|
|
100
|
-
expect(field.candidType).toBe("
|
|
116
|
+
expect(field.candidType).toBe("float64")
|
|
117
|
+
if (field.type === "number") {
|
|
118
|
+
expect(field.isFloat).toBe(true)
|
|
119
|
+
}
|
|
101
120
|
})
|
|
102
121
|
})
|
|
103
122
|
|
|
@@ -123,6 +142,8 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
123
142
|
expect(field.type).toBe("record")
|
|
124
143
|
expect(field.label).toBe("person")
|
|
125
144
|
expect(field.fields).toHaveLength(2)
|
|
145
|
+
expect(field.fieldMap.has("name")).toBe(true)
|
|
146
|
+
expect(field.fieldMap.has("age")).toBe(true)
|
|
126
147
|
|
|
127
148
|
const nameField = field.fields.find((f) => f.label === "name")
|
|
128
149
|
if (!nameField || nameField.type !== "text") {
|
|
@@ -132,13 +153,13 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
132
153
|
expect(nameField.defaultValue).toBe("")
|
|
133
154
|
|
|
134
155
|
const ageField = field.fields.find((f) => f.label === "age")
|
|
135
|
-
if (!ageField || ageField.type !== "
|
|
136
|
-
throw new Error("Age field not found or not
|
|
156
|
+
if (!ageField || ageField.type !== "text") {
|
|
157
|
+
throw new Error("Age field not found or not text")
|
|
137
158
|
}
|
|
138
|
-
expect(ageField.type).toBe("
|
|
159
|
+
expect(ageField.type).toBe("text")
|
|
139
160
|
expect(ageField.candidType).toBe("nat")
|
|
140
161
|
|
|
141
|
-
expect(field.
|
|
162
|
+
expect(field.defaultValue).toEqual({
|
|
142
163
|
name: "",
|
|
143
164
|
age: "",
|
|
144
165
|
})
|
|
@@ -172,7 +193,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
172
193
|
expect(addressField.type).toBe("record")
|
|
173
194
|
expect(addressField.fields).toHaveLength(2)
|
|
174
195
|
|
|
175
|
-
expect(field.
|
|
196
|
+
expect(field.defaultValue).toEqual({
|
|
176
197
|
name: "",
|
|
177
198
|
address: {
|
|
178
199
|
street: "",
|
|
@@ -223,10 +244,10 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
223
244
|
|
|
224
245
|
// Check 'amount' field
|
|
225
246
|
const amountField = field.fields.find((f) => f.label === "amount")
|
|
226
|
-
if (!amountField || amountField.type !== "
|
|
227
|
-
throw new Error("Amount field not found or not
|
|
247
|
+
if (!amountField || amountField.type !== "text") {
|
|
248
|
+
throw new Error("Amount field not found or not text")
|
|
228
249
|
}
|
|
229
|
-
expect(amountField.type).toBe("
|
|
250
|
+
expect(amountField.type).toBe("text")
|
|
230
251
|
expect(amountField.candidType).toBe("nat")
|
|
231
252
|
|
|
232
253
|
// Check optional 'fee' field
|
|
@@ -235,7 +256,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
235
256
|
throw new Error("Fee field not found or not optional")
|
|
236
257
|
}
|
|
237
258
|
expect(feeField.type).toBe("optional")
|
|
238
|
-
expect(feeField.innerField.type).toBe("
|
|
259
|
+
expect(feeField.innerField.type).toBe("text")
|
|
239
260
|
})
|
|
240
261
|
})
|
|
241
262
|
|
|
@@ -261,10 +282,15 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
261
282
|
expect(field.options).toEqual(["Inactive", "Active", "Pending"])
|
|
262
283
|
expect(field.defaultOption).toBe("Inactive")
|
|
263
284
|
expect(field.fields).toHaveLength(3)
|
|
285
|
+
expect(field.optionMap.has("Active")).toBe(true)
|
|
264
286
|
|
|
265
287
|
field.fields.forEach((f) => {
|
|
266
288
|
expect(f.type).toBe("null")
|
|
267
289
|
})
|
|
290
|
+
|
|
291
|
+
// Test getOptionDefault helper
|
|
292
|
+
expect(field.getOptionDefault("Active")).toEqual({ Active: null })
|
|
293
|
+
expect(field.getOptionDefault("Pending")).toEqual({ Pending: null })
|
|
268
294
|
})
|
|
269
295
|
|
|
270
296
|
it("should handle variant with payloads", () => {
|
|
@@ -312,10 +338,10 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
312
338
|
expect(transferField.fields).toHaveLength(2)
|
|
313
339
|
|
|
314
340
|
const burnField = field.fields.find((f) => f.label === "Burn")
|
|
315
|
-
if (!burnField || burnField.type !== "
|
|
316
|
-
throw new Error("Burn field not found or not
|
|
341
|
+
if (!burnField || burnField.type !== "text") {
|
|
342
|
+
throw new Error("Burn field not found or not text")
|
|
317
343
|
}
|
|
318
|
-
expect(burnField.type).toBe("
|
|
344
|
+
expect(burnField.type).toBe("text")
|
|
319
345
|
})
|
|
320
346
|
|
|
321
347
|
it("should handle Result variant (Ok/Err)", () => {
|
|
@@ -337,10 +363,10 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
337
363
|
expect(field.options).toContain("Err")
|
|
338
364
|
|
|
339
365
|
const okField = field.fields.find((f) => f.label === "Ok")
|
|
340
|
-
if (!okField || okField.type !== "
|
|
341
|
-
throw new Error("Ok field not found or not
|
|
366
|
+
if (!okField || okField.type !== "text") {
|
|
367
|
+
throw new Error("Ok field not found or not text")
|
|
342
368
|
}
|
|
343
|
-
expect(okField.type).toBe("
|
|
369
|
+
expect(okField.type).toBe("text")
|
|
344
370
|
|
|
345
371
|
const errField = field.fields.find((f) => f.label === "Err")
|
|
346
372
|
if (!errField || errField.type !== "text") {
|
|
@@ -359,8 +385,8 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
359
385
|
expect(field.label).toBe("pair")
|
|
360
386
|
expect(field.fields).toHaveLength(2)
|
|
361
387
|
expect(field.fields[0].type).toBe("text")
|
|
362
|
-
expect(field.fields[1].type).toBe("
|
|
363
|
-
expect(field.
|
|
388
|
+
expect(field.fields[1].type).toBe("text")
|
|
389
|
+
expect(field.defaultValue).toEqual(["", ""])
|
|
364
390
|
})
|
|
365
391
|
|
|
366
392
|
it("triple tuple", () => {
|
|
@@ -374,9 +400,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
374
400
|
expect(field.type).toBe("tuple")
|
|
375
401
|
expect(field.fields).toHaveLength(3)
|
|
376
402
|
expect(field.fields[0].type).toBe("principal")
|
|
377
|
-
expect(field.fields[1].type).toBe("
|
|
403
|
+
expect(field.fields[1].type).toBe("text")
|
|
378
404
|
expect(field.fields[2].type).toBe("boolean")
|
|
379
|
-
expect(field.
|
|
405
|
+
expect(field.defaultValue).toEqual(["", "", false])
|
|
380
406
|
})
|
|
381
407
|
})
|
|
382
408
|
|
|
@@ -389,6 +415,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
389
415
|
expect(field.label).toBe("nickname")
|
|
390
416
|
expect(field.defaultValue).toBe(null)
|
|
391
417
|
expect(field.innerField.type).toBe("text")
|
|
418
|
+
expect(field.getInnerDefault()).toBe("")
|
|
392
419
|
})
|
|
393
420
|
|
|
394
421
|
it("should handle optional record", () => {
|
|
@@ -408,6 +435,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
408
435
|
} else {
|
|
409
436
|
throw new Error("Inner field is not record")
|
|
410
437
|
}
|
|
438
|
+
|
|
439
|
+
// Test getInnerDefault helper
|
|
440
|
+
expect(field.getInnerDefault()).toEqual({ name: "", value: "" })
|
|
411
441
|
})
|
|
412
442
|
|
|
413
443
|
it("should handle nested optional", () => {
|
|
@@ -420,7 +450,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
420
450
|
expect(field.innerField.type).toBe("optional")
|
|
421
451
|
const inner = field.innerField
|
|
422
452
|
if (inner.type === "optional") {
|
|
423
|
-
expect(inner.innerField.type).toBe("
|
|
453
|
+
expect(inner.innerField.type).toBe("text")
|
|
424
454
|
} else {
|
|
425
455
|
throw new Error("Inner field is not optional")
|
|
426
456
|
}
|
|
@@ -430,12 +460,13 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
430
460
|
describe("Vector Types", () => {
|
|
431
461
|
it("should handle vector of primitives", () => {
|
|
432
462
|
const vecType = IDL.Vec(IDL.Text)
|
|
433
|
-
const field = visitor.visitVec(vecType, IDL.Text, "tags")
|
|
463
|
+
const field = visitor.visitVec(vecType, IDL.Text, "tags") as VectorField
|
|
434
464
|
|
|
435
465
|
expect(field.type).toBe("vector")
|
|
436
466
|
expect(field.label).toBe("tags")
|
|
437
467
|
expect(field.defaultValue).toEqual([])
|
|
438
468
|
expect(field.itemField.type).toBe("text")
|
|
469
|
+
expect(field.getItemDefault()).toBe("")
|
|
439
470
|
})
|
|
440
471
|
|
|
441
472
|
it("should handle vector of records", () => {
|
|
@@ -444,7 +475,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
444
475
|
name: IDL.Text,
|
|
445
476
|
})
|
|
446
477
|
const vecType = IDL.Vec(recType)
|
|
447
|
-
const field = visitor.visitVec(vecType, recType, "items")
|
|
478
|
+
const field = visitor.visitVec(vecType, recType, "items") as VectorField
|
|
448
479
|
|
|
449
480
|
expect(field.type).toBe("vector")
|
|
450
481
|
expect(field.itemField.type).toBe("record")
|
|
@@ -454,6 +485,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
454
485
|
} else {
|
|
455
486
|
throw new Error("Item field is not record")
|
|
456
487
|
}
|
|
488
|
+
|
|
489
|
+
// Test getItemDefault helper
|
|
490
|
+
expect(field.getItemDefault()).toEqual({ id: "", name: "" })
|
|
457
491
|
})
|
|
458
492
|
|
|
459
493
|
it("blob (vec nat8)", () => {
|
|
@@ -463,6 +497,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
463
497
|
expect(field.type).toBe("blob")
|
|
464
498
|
expect(field.label).toBe("data")
|
|
465
499
|
expect(field.defaultValue).toBe("")
|
|
500
|
+
if (field.type === "blob") {
|
|
501
|
+
expect(field.acceptedFormats).toEqual(["hex", "base64", "file"])
|
|
502
|
+
}
|
|
466
503
|
})
|
|
467
504
|
|
|
468
505
|
it("should handle nested vectors", () => {
|
|
@@ -474,7 +511,7 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
474
511
|
expect(field.itemField.type).toBe("vector")
|
|
475
512
|
const item = field.itemField
|
|
476
513
|
if (item.type === "vector") {
|
|
477
|
-
expect(item.itemField.type).toBe("
|
|
514
|
+
expect(item.itemField.type).toBe("text")
|
|
478
515
|
} else {
|
|
479
516
|
throw new Error("Item field is not vector")
|
|
480
517
|
}
|
|
@@ -508,7 +545,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
508
545
|
|
|
509
546
|
expect(field.type).toBe("recursive")
|
|
510
547
|
expect(field.label).toBe("tree")
|
|
548
|
+
expect(field.typeName).toBeDefined()
|
|
511
549
|
expect(typeof field.extract).toBe("function")
|
|
550
|
+
expect(typeof field.getInnerDefault).toBe("function")
|
|
512
551
|
|
|
513
552
|
// Extract should return a variant
|
|
514
553
|
const extracted = field.extract()
|
|
@@ -576,6 +615,8 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
576
615
|
expect(meta.fields).toHaveLength(1)
|
|
577
616
|
expect(meta.fields[0].type).toBe("text")
|
|
578
617
|
expect(meta.defaultValues).toEqual([""])
|
|
618
|
+
expect(meta.argCount).toBe(1)
|
|
619
|
+
expect(meta.isNoArgs).toBe(false)
|
|
579
620
|
})
|
|
580
621
|
|
|
581
622
|
it("should handle update function", () => {
|
|
@@ -618,8 +659,9 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
618
659
|
|
|
619
660
|
expect(meta.fields).toHaveLength(3)
|
|
620
661
|
expect(meta.fields[0].type).toBe("principal")
|
|
621
|
-
expect(meta.fields[1].type).toBe("
|
|
662
|
+
expect(meta.fields[1].type).toBe("text")
|
|
622
663
|
expect(meta.fields[2].type).toBe("optional")
|
|
664
|
+
expect(meta.argCount).toBe(3)
|
|
623
665
|
})
|
|
624
666
|
|
|
625
667
|
it("should handle function with no arguments", () => {
|
|
@@ -629,6 +671,8 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
629
671
|
expect(meta.functionType).toBe("query")
|
|
630
672
|
expect(meta.fields).toHaveLength(0)
|
|
631
673
|
expect(meta.defaultValues).toEqual([])
|
|
674
|
+
expect(meta.argCount).toBe(0)
|
|
675
|
+
expect(meta.isNoArgs).toBe(true)
|
|
632
676
|
})
|
|
633
677
|
})
|
|
634
678
|
|
|
@@ -685,15 +729,16 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
685
729
|
const getMetadataMeta = serviceMeta["get_metadata"]
|
|
686
730
|
expect(getMetadataMeta.functionType).toBe("query")
|
|
687
731
|
expect(getMetadataMeta.fields).toHaveLength(0)
|
|
732
|
+
expect(getMetadataMeta.isNoArgs).toBe(true)
|
|
688
733
|
})
|
|
689
734
|
})
|
|
690
735
|
|
|
691
736
|
// ════════════════════════════════════════════════════════════════════════
|
|
692
|
-
// Path Generation
|
|
737
|
+
// Name (Path) Generation
|
|
693
738
|
// ════════════════════════════════════════════════════════════════════════
|
|
694
739
|
|
|
695
|
-
describe("
|
|
696
|
-
it("should generate correct
|
|
740
|
+
describe("Name Generation", () => {
|
|
741
|
+
it("should generate correct names for nested records", () => {
|
|
697
742
|
const funcType = IDL.Func(
|
|
698
743
|
[
|
|
699
744
|
IDL.Record({
|
|
@@ -713,28 +758,28 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
713
758
|
if (argRecord.type !== "record") {
|
|
714
759
|
throw new Error("Expected record field")
|
|
715
760
|
}
|
|
716
|
-
expect(argRecord.
|
|
761
|
+
expect(argRecord.name).toBe("[0]")
|
|
717
762
|
|
|
718
763
|
const userRecord = argRecord.fields.find((f) => f.label === "user")
|
|
719
764
|
if (!userRecord || userRecord.type !== "record") {
|
|
720
765
|
throw new Error("User record not found or not record")
|
|
721
766
|
}
|
|
722
|
-
expect(userRecord.
|
|
767
|
+
expect(userRecord.name).toBe("[0].user")
|
|
723
768
|
|
|
724
769
|
const nameField = userRecord.fields.find((f) => f.label === "name")
|
|
725
770
|
if (!nameField || nameField.type !== "text") {
|
|
726
771
|
throw new Error("Name field not found or not text")
|
|
727
772
|
}
|
|
728
|
-
expect(nameField.
|
|
773
|
+
expect(nameField.name).toBe("[0].user.name")
|
|
729
774
|
|
|
730
775
|
const ageField = userRecord.fields.find((f) => f.label === "age")
|
|
731
|
-
if (!ageField || ageField.type !== "
|
|
732
|
-
throw new Error("Age field not found or not
|
|
776
|
+
if (!ageField || ageField.type !== "text") {
|
|
777
|
+
throw new Error("Age field not found or not text")
|
|
733
778
|
}
|
|
734
|
-
expect(ageField.
|
|
779
|
+
expect(ageField.name).toBe("[0].user.age")
|
|
735
780
|
})
|
|
736
781
|
|
|
737
|
-
it("should generate correct
|
|
782
|
+
it("should generate correct names for vectors", () => {
|
|
738
783
|
const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
|
|
739
784
|
const meta = visitor.visitFunc(funcType, "addTags")
|
|
740
785
|
|
|
@@ -742,8 +787,8 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
742
787
|
if (vecField.type !== "vector") {
|
|
743
788
|
throw new Error("Expected vector field")
|
|
744
789
|
}
|
|
745
|
-
expect(vecField.
|
|
746
|
-
expect(vecField.itemField.
|
|
790
|
+
expect(vecField.name).toBe("[0]")
|
|
791
|
+
expect(vecField.itemField.name).toBe("[0][0]")
|
|
747
792
|
})
|
|
748
793
|
})
|
|
749
794
|
|
|
@@ -801,10 +846,10 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
801
846
|
|
|
802
847
|
// Check amount field
|
|
803
848
|
const amountField = field.fields.find((f) => f.label === "amount")
|
|
804
|
-
if (!amountField || amountField.type !== "
|
|
805
|
-
throw new Error("Amount field not found or not
|
|
849
|
+
if (!amountField || amountField.type !== "text") {
|
|
850
|
+
throw new Error("Amount field not found or not text")
|
|
806
851
|
}
|
|
807
|
-
expect(amountField.type).toBe("
|
|
852
|
+
expect(amountField.type).toBe("text")
|
|
808
853
|
expect(amountField.candidType).toBe("nat")
|
|
809
854
|
})
|
|
810
855
|
|
|
@@ -879,4 +924,52 @@ describe("ArgumentFieldVisitor", () => {
|
|
|
879
924
|
expect(transferField.fields.length).toBeGreaterThan(3)
|
|
880
925
|
})
|
|
881
926
|
})
|
|
927
|
+
|
|
928
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
929
|
+
// Helper Methods
|
|
930
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
931
|
+
|
|
932
|
+
describe("Helper Methods", () => {
|
|
933
|
+
it("variant getOptionDefault should return correct defaults", () => {
|
|
934
|
+
const statusType = IDL.Variant({
|
|
935
|
+
Active: IDL.Null,
|
|
936
|
+
Pending: IDL.Record({ reason: IDL.Text }),
|
|
937
|
+
})
|
|
938
|
+
const field = visitor.visitVariant(
|
|
939
|
+
statusType,
|
|
940
|
+
[
|
|
941
|
+
["Active", IDL.Null],
|
|
942
|
+
["Pending", IDL.Record({ reason: IDL.Text })],
|
|
943
|
+
],
|
|
944
|
+
"status"
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
expect(field.getOptionDefault("Active")).toEqual({ Active: null })
|
|
948
|
+
expect(field.getOptionDefault("Pending")).toEqual({
|
|
949
|
+
Pending: { reason: "" },
|
|
950
|
+
})
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
it("vector getItemDefault should return item default", () => {
|
|
954
|
+
const vecType = IDL.Vec(IDL.Record({ name: IDL.Text }))
|
|
955
|
+
const field = visitor.visitVec(
|
|
956
|
+
vecType,
|
|
957
|
+
IDL.Record({ name: IDL.Text }),
|
|
958
|
+
"items"
|
|
959
|
+
) as VectorField
|
|
960
|
+
|
|
961
|
+
expect(field.getItemDefault()).toEqual({ name: "" })
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
it("optional getInnerDefault should return inner default", () => {
|
|
965
|
+
const optType = IDL.Opt(IDL.Record({ value: IDL.Nat }))
|
|
966
|
+
const field = visitor.visitOpt(
|
|
967
|
+
optType,
|
|
968
|
+
IDL.Record({ value: IDL.Nat }),
|
|
969
|
+
"config"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
expect(field.getInnerDefault()).toEqual({ value: "" })
|
|
973
|
+
})
|
|
974
|
+
})
|
|
882
975
|
})
|