@ic-reactor/candid 3.0.2-beta.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/dist/adapter.js +2 -1
- package/dist/adapter.js.map +1 -1
- package/dist/display-reactor.d.ts +4 -13
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +22 -8
- package/dist/display-reactor.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +108 -0
- package/dist/metadata-display-reactor.d.ts.map +1 -0
- package/dist/metadata-display-reactor.js +141 -0
- package/dist/metadata-display-reactor.js.map +1 -0
- package/dist/reactor.d.ts +1 -1
- package/dist/reactor.d.ts.map +1 -1
- package/dist/reactor.js +10 -6
- package/dist/reactor.js.map +1 -1
- package/dist/types.d.ts +38 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +4 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +33 -10
- package/dist/utils.js.map +1 -1
- package/dist/visitor/arguments/helpers.d.ts +55 -0
- package/dist/visitor/arguments/helpers.d.ts.map +1 -0
- package/dist/visitor/arguments/helpers.js +123 -0
- package/dist/visitor/arguments/helpers.js.map +1 -0
- package/dist/visitor/arguments/index.d.ts +101 -0
- package/dist/visitor/arguments/index.d.ts.map +1 -0
- package/dist/visitor/arguments/index.js +780 -0
- package/dist/visitor/arguments/index.js.map +1 -0
- package/dist/visitor/arguments/types.d.ts +270 -0
- package/dist/visitor/arguments/types.d.ts.map +1 -0
- package/dist/visitor/arguments/types.js +26 -0
- package/dist/visitor/arguments/types.js.map +1 -0
- package/dist/visitor/constants.d.ts +4 -0
- package/dist/visitor/constants.d.ts.map +1 -0
- package/dist/visitor/constants.js +73 -0
- package/dist/visitor/constants.js.map +1 -0
- package/dist/visitor/helpers.d.ts +30 -0
- package/dist/visitor/helpers.d.ts.map +1 -0
- package/dist/visitor/helpers.js +204 -0
- package/dist/visitor/helpers.js.map +1 -0
- package/dist/visitor/index.d.ts +5 -0
- package/dist/visitor/index.d.ts.map +1 -0
- package/dist/visitor/index.js +5 -0
- package/dist/visitor/index.js.map +1 -0
- package/dist/visitor/returns/index.d.ts +38 -0
- package/dist/visitor/returns/index.d.ts.map +1 -0
- package/dist/visitor/returns/index.js +460 -0
- package/dist/visitor/returns/index.js.map +1 -0
- package/dist/visitor/returns/types.d.ts +202 -0
- package/dist/visitor/returns/types.d.ts.map +1 -0
- package/dist/visitor/returns/types.js +2 -0
- package/dist/visitor/returns/types.js.map +1 -0
- package/dist/visitor/types.d.ts +19 -0
- package/dist/visitor/types.d.ts.map +1 -0
- package/dist/visitor/types.js +2 -0
- package/dist/visitor/types.js.map +1 -0
- package/package.json +16 -7
- package/src/adapter.ts +446 -0
- package/src/constants.ts +11 -0
- package/src/display-reactor.ts +337 -0
- package/src/index.ts +8 -0
- package/src/metadata-display-reactor.ts +230 -0
- package/src/reactor.ts +199 -0
- package/src/types.ts +127 -0
- package/src/utils.ts +60 -0
- package/src/visitor/arguments/helpers.ts +153 -0
- package/src/visitor/arguments/index.test.ts +1439 -0
- package/src/visitor/arguments/index.ts +981 -0
- package/src/visitor/arguments/schema.test.ts +324 -0
- package/src/visitor/arguments/types.ts +387 -0
- package/src/visitor/constants.ts +76 -0
- package/src/visitor/helpers.test.ts +274 -0
- package/src/visitor/helpers.ts +223 -0
- package/src/visitor/index.ts +4 -0
- package/src/visitor/returns/index.test.ts +2377 -0
- package/src/visitor/returns/index.ts +658 -0
- package/src/visitor/returns/types.ts +302 -0
- package/src/visitor/types.ts +75 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { isQuery } from "../helpers"
|
|
2
|
+
import { checkTextFormat, checkNumberFormat } from "../constants"
|
|
3
|
+
import { formatLabel } from "../arguments/helpers"
|
|
4
|
+
import { MetadataError } from "../arguments/types"
|
|
5
|
+
import type {
|
|
6
|
+
ResultNode,
|
|
7
|
+
ResolvedNode,
|
|
8
|
+
VisitorDataType,
|
|
9
|
+
MethodMeta,
|
|
10
|
+
ServiceMeta,
|
|
11
|
+
MethodResult,
|
|
12
|
+
NumberFormat,
|
|
13
|
+
TextFormat,
|
|
14
|
+
} from "./types"
|
|
15
|
+
|
|
16
|
+
import { sha256 } from "@noble/hashes/sha2.js"
|
|
17
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
18
|
+
import { DisplayCodecVisitor, uint8ArrayToHex } from "@ic-reactor/core"
|
|
19
|
+
import type {
|
|
20
|
+
ActorMethodReturnType,
|
|
21
|
+
BaseActor,
|
|
22
|
+
FunctionName,
|
|
23
|
+
FunctionType,
|
|
24
|
+
} from "@ic-reactor/core"
|
|
25
|
+
|
|
26
|
+
export * from "./types"
|
|
27
|
+
|
|
28
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// Node Factory - Eliminates Boilerplate
|
|
30
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
type Codec = { decode: (v: unknown) => unknown }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a primitive node with automatic resolve implementation.
|
|
36
|
+
*/
|
|
37
|
+
function primitiveNode<T extends VisitorDataType>(
|
|
38
|
+
type: T,
|
|
39
|
+
label: string,
|
|
40
|
+
candidType: string,
|
|
41
|
+
displayType: ResultNode["displayType"],
|
|
42
|
+
codec: Codec,
|
|
43
|
+
extras: object = {}
|
|
44
|
+
): ResultNode<T> {
|
|
45
|
+
const node: ResultNode<T> = {
|
|
46
|
+
type,
|
|
47
|
+
label,
|
|
48
|
+
displayLabel: formatLabel(label),
|
|
49
|
+
candidType,
|
|
50
|
+
displayType,
|
|
51
|
+
...extras,
|
|
52
|
+
resolve(data: unknown): ResolvedNode<T> {
|
|
53
|
+
try {
|
|
54
|
+
return {
|
|
55
|
+
...node,
|
|
56
|
+
value: codec.decode(data),
|
|
57
|
+
raw: data,
|
|
58
|
+
} as unknown as ResolvedNode<T>
|
|
59
|
+
} catch (e) {
|
|
60
|
+
throw new MetadataError(
|
|
61
|
+
`Failed to decode: ${e instanceof Error ? e.message : String(e)}`,
|
|
62
|
+
label,
|
|
63
|
+
candidType
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
} as unknown as ResultNode<T>
|
|
68
|
+
return node
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// Simplified Result Field Visitor
|
|
73
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
76
|
+
string,
|
|
77
|
+
ResultNode | MethodMeta<A> | ServiceMeta<A>
|
|
78
|
+
> {
|
|
79
|
+
private codec = new DisplayCodecVisitor()
|
|
80
|
+
|
|
81
|
+
private recCache = new Map<IDL.RecClass<any>, ResultNode<"recursive">>()
|
|
82
|
+
|
|
83
|
+
private getCodec(t: IDL.Type): Codec {
|
|
84
|
+
const codec = t.accept(this.codec, null) as any
|
|
85
|
+
return {
|
|
86
|
+
decode: (v: unknown) => {
|
|
87
|
+
try {
|
|
88
|
+
return typeof codec?.decode === "function" ? codec.decode(v) : v
|
|
89
|
+
} catch {
|
|
90
|
+
return v
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
// Service & Function
|
|
98
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
|
|
100
|
+
public visitService(t: IDL.ServiceClass): ServiceMeta<A> {
|
|
101
|
+
const result = {} as ServiceMeta<A>
|
|
102
|
+
for (const [name, func] of t._fields) {
|
|
103
|
+
// Process each service method using dedicated method handler
|
|
104
|
+
result[name as FunctionName<A>] = this.visitFuncAsMethod(
|
|
105
|
+
func,
|
|
106
|
+
name as FunctionName<A>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handle func type when encountered as a service method definition.
|
|
114
|
+
* Returns MethodMeta with information about the method's inputs/outputs.
|
|
115
|
+
* This is public so callers can explicitly request method metadata.
|
|
116
|
+
*/
|
|
117
|
+
public visitFuncAsMethod(
|
|
118
|
+
t: IDL.FuncClass,
|
|
119
|
+
functionName: FunctionName<A>
|
|
120
|
+
): MethodMeta<A> {
|
|
121
|
+
const functionType: FunctionType = isQuery(t) ? "query" : "update"
|
|
122
|
+
const returns = t.retTypes.map((ret, i) =>
|
|
123
|
+
ret.accept(this, `__ret${i}`)
|
|
124
|
+
) as ResultNode[]
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
functionType,
|
|
128
|
+
functionName,
|
|
129
|
+
returns,
|
|
130
|
+
returnCount: t.retTypes.length,
|
|
131
|
+
resolve: (
|
|
132
|
+
data: ActorMethodReturnType<A[FunctionName<A>]>
|
|
133
|
+
): MethodResult<A> => {
|
|
134
|
+
const dataArray = returns.length <= 1 ? [data] : (data as unknown[])
|
|
135
|
+
return {
|
|
136
|
+
functionType,
|
|
137
|
+
functionName,
|
|
138
|
+
results: returns.map((node, i) => node.resolve(dataArray[i])),
|
|
139
|
+
raw: data,
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handle func type when encountered as a data field (e.g., callback in a record).
|
|
147
|
+
* Returns ResultNode that can resolve [Principal, string] data to a func reference.
|
|
148
|
+
*/
|
|
149
|
+
public visitFunc(_t: IDL.FuncClass, label: string): ResultNode<"func"> {
|
|
150
|
+
const node: ResultNode<"func"> = {
|
|
151
|
+
type: "func",
|
|
152
|
+
label,
|
|
153
|
+
displayLabel: formatLabel(label),
|
|
154
|
+
candidType: "func",
|
|
155
|
+
displayType: "func",
|
|
156
|
+
canisterId: "", // placeholder, populated on resolve
|
|
157
|
+
methodName: "", // placeholder, populated on resolve
|
|
158
|
+
resolve(data: unknown): ResolvedNode<"func"> {
|
|
159
|
+
// Func values are represented as [Principal, string] tuples
|
|
160
|
+
if (!Array.isArray(data) || data.length !== 2) {
|
|
161
|
+
throw new MetadataError(
|
|
162
|
+
`Expected func reference [Principal, string], but got ${typeof data}`,
|
|
163
|
+
label,
|
|
164
|
+
"func"
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
const [principal, methodName] = data
|
|
168
|
+
const canisterId =
|
|
169
|
+
typeof principal === "string"
|
|
170
|
+
? principal
|
|
171
|
+
: (principal?.toText?.() ?? String(principal))
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...node,
|
|
175
|
+
canisterId,
|
|
176
|
+
methodName: String(methodName),
|
|
177
|
+
raw: data,
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
return node
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
185
|
+
// Compound Types
|
|
186
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
187
|
+
|
|
188
|
+
public visitRecord(
|
|
189
|
+
_t: IDL.RecordClass,
|
|
190
|
+
fields_: Array<[string, IDL.Type]>,
|
|
191
|
+
label: string
|
|
192
|
+
): ResultNode<"record"> | ResultNode<"funcRecord"> {
|
|
193
|
+
const fields: Record<string, ResultNode> = {}
|
|
194
|
+
// Track func fields for funcRecord detection
|
|
195
|
+
const funcEntries: Array<{
|
|
196
|
+
key: string
|
|
197
|
+
funcType: IDL.FuncClass
|
|
198
|
+
node: ResultNode<"func">
|
|
199
|
+
}> = []
|
|
200
|
+
|
|
201
|
+
for (const [key, type] of fields_) {
|
|
202
|
+
const fieldNode = type.accept(this, key) as ResultNode
|
|
203
|
+
fields[key] = fieldNode
|
|
204
|
+
|
|
205
|
+
if (type instanceof IDL.FuncClass) {
|
|
206
|
+
funcEntries.push({
|
|
207
|
+
key,
|
|
208
|
+
funcType: type,
|
|
209
|
+
node: fieldNode as ResultNode<"func">,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── funcRecord: exactly one func field + other argument fields ──
|
|
215
|
+
if (funcEntries.length === 1) {
|
|
216
|
+
const {
|
|
217
|
+
key: funcFieldKey,
|
|
218
|
+
funcType,
|
|
219
|
+
node: funcFieldNode,
|
|
220
|
+
} = funcEntries[0]
|
|
221
|
+
const funcCallType: "query" | "update" = isQuery(funcType)
|
|
222
|
+
? "query"
|
|
223
|
+
: "update"
|
|
224
|
+
|
|
225
|
+
const argFields: Record<string, ResultNode> = {}
|
|
226
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
227
|
+
if (k !== funcFieldKey) argFields[k] = v
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const node: ResultNode<"funcRecord"> = {
|
|
231
|
+
type: "funcRecord",
|
|
232
|
+
label,
|
|
233
|
+
displayLabel: formatLabel(label),
|
|
234
|
+
candidType: "record",
|
|
235
|
+
displayType: "func-record",
|
|
236
|
+
canisterId: "",
|
|
237
|
+
methodName: "",
|
|
238
|
+
funcType: funcCallType,
|
|
239
|
+
funcClass: funcType,
|
|
240
|
+
funcFieldKey,
|
|
241
|
+
funcField: funcFieldNode,
|
|
242
|
+
argFields,
|
|
243
|
+
fields,
|
|
244
|
+
resolve(data: unknown): ResolvedNode<"funcRecord"> {
|
|
245
|
+
if (data === null || data === undefined) {
|
|
246
|
+
throw new MetadataError(
|
|
247
|
+
`Expected funcRecord, but got ${data === null ? "null" : "undefined"}`,
|
|
248
|
+
label,
|
|
249
|
+
"record"
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
const recordData = data as Record<string, unknown>
|
|
253
|
+
const resolvedFields: Record<string, ResolvedNode> = {}
|
|
254
|
+
let index = 0
|
|
255
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
256
|
+
const value =
|
|
257
|
+
recordData[key] !== undefined
|
|
258
|
+
? recordData[key]
|
|
259
|
+
: recordData[index]
|
|
260
|
+
resolvedFields[key] = field.resolve(value)
|
|
261
|
+
index++
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const resolvedFuncField = resolvedFields[
|
|
265
|
+
funcFieldKey
|
|
266
|
+
] as ResolvedNode<"func">
|
|
267
|
+
|
|
268
|
+
const resolvedArgFields: Record<string, ResolvedNode> = {}
|
|
269
|
+
for (const [k, v] of Object.entries(resolvedFields)) {
|
|
270
|
+
if (k !== funcFieldKey) resolvedArgFields[k] = v
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Build display-type default args ready for callMethod
|
|
274
|
+
const argRecord = Object.fromEntries(
|
|
275
|
+
Object.entries(resolvedArgFields).map(([k, v]) => [
|
|
276
|
+
k,
|
|
277
|
+
v.value ?? v.raw,
|
|
278
|
+
])
|
|
279
|
+
)
|
|
280
|
+
const defaultArgs = funcType.argTypes.length > 0 ? [argRecord] : []
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
...node,
|
|
284
|
+
canisterId: resolvedFuncField.canisterId,
|
|
285
|
+
methodName: resolvedFuncField.methodName,
|
|
286
|
+
funcField: resolvedFuncField,
|
|
287
|
+
argFields: resolvedArgFields,
|
|
288
|
+
fields: resolvedFields,
|
|
289
|
+
defaultArgs,
|
|
290
|
+
raw: data,
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
return node
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Regular record ──
|
|
298
|
+
const node: ResultNode<"record"> = {
|
|
299
|
+
type: "record",
|
|
300
|
+
label,
|
|
301
|
+
displayLabel: formatLabel(label),
|
|
302
|
+
candidType: "record",
|
|
303
|
+
displayType: "object",
|
|
304
|
+
fields,
|
|
305
|
+
resolve(data: unknown): ResolvedNode<"record"> {
|
|
306
|
+
if (data === null || data === undefined) {
|
|
307
|
+
throw new MetadataError(
|
|
308
|
+
`Expected record, but got ${data === null ? "null" : "undefined"}`,
|
|
309
|
+
label,
|
|
310
|
+
"record"
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
const recordData = data as Record<string, unknown>
|
|
314
|
+
const resolvedFields: Record<string, ResolvedNode> = {}
|
|
315
|
+
let index = 0
|
|
316
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
317
|
+
// Try named key first, then try numeric index (for tuples/indexed records)
|
|
318
|
+
const value =
|
|
319
|
+
recordData[key] !== undefined ? recordData[key] : recordData[index]
|
|
320
|
+
|
|
321
|
+
if (!field || typeof field.resolve !== "function") {
|
|
322
|
+
throw new MetadataError(
|
|
323
|
+
`Field "${key}" is not a valid ResultNode`,
|
|
324
|
+
`${label}.${key}`,
|
|
325
|
+
"record"
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
resolvedFields[key] = field.resolve(value)
|
|
330
|
+
index++
|
|
331
|
+
}
|
|
332
|
+
return { ...node, fields: resolvedFields, raw: data }
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
return node
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public visitVariant(
|
|
339
|
+
_t: IDL.VariantClass,
|
|
340
|
+
fields_: Array<[string, IDL.Type]>,
|
|
341
|
+
label: string
|
|
342
|
+
): ResultNode<"variant"> {
|
|
343
|
+
const options: Record<string, ResultNode> = {}
|
|
344
|
+
for (const [key, type] of fields_) {
|
|
345
|
+
options[key] = type.accept(this, key) as ResultNode
|
|
346
|
+
}
|
|
347
|
+
const isResult =
|
|
348
|
+
("Ok" in options && "Err" in options) ||
|
|
349
|
+
("ok" in options && "err" in options)
|
|
350
|
+
const isNullVariant =
|
|
351
|
+
!isResult &&
|
|
352
|
+
Object.values(options).every((option) => option.type === "null")
|
|
353
|
+
const node: ResultNode<"variant"> = {
|
|
354
|
+
type: "variant",
|
|
355
|
+
label,
|
|
356
|
+
displayLabel: formatLabel(label),
|
|
357
|
+
candidType: "variant",
|
|
358
|
+
displayType: isResult
|
|
359
|
+
? "result"
|
|
360
|
+
: isNullVariant
|
|
361
|
+
? "variant-null"
|
|
362
|
+
: "variant",
|
|
363
|
+
options,
|
|
364
|
+
selectedValue: {} as ResultNode, // placeholder, populated on resolve
|
|
365
|
+
resolve(data: unknown): ResolvedNode<"variant"> {
|
|
366
|
+
if (data === null || data === undefined) {
|
|
367
|
+
throw new MetadataError(
|
|
368
|
+
`Expected variant, but got ${data === null ? "null" : "undefined"}, raw: ${data}`,
|
|
369
|
+
label,
|
|
370
|
+
"variant"
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
const variantData = data as Record<string, unknown>
|
|
374
|
+
// Support both raw { Selected: value } and transformed { _type: 'Selected', Selected: value }
|
|
375
|
+
const selected =
|
|
376
|
+
(variantData._type as string) || Object.keys(variantData)[0]
|
|
377
|
+
const optionNode = options[selected]
|
|
378
|
+
|
|
379
|
+
if (!optionNode) {
|
|
380
|
+
throw new MetadataError(
|
|
381
|
+
`Option "${selected}" not found. Available: ${Object.keys(options).join(", ")}`,
|
|
382
|
+
label,
|
|
383
|
+
"variant"
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
...node,
|
|
388
|
+
selected,
|
|
389
|
+
selectedValue: optionNode.resolve(variantData[selected]),
|
|
390
|
+
raw: data,
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
return node
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
public visitTuple<T extends IDL.Type[]>(
|
|
398
|
+
_t: IDL.TupleClass<T>,
|
|
399
|
+
components: IDL.Type[],
|
|
400
|
+
label: string
|
|
401
|
+
): ResultNode<"tuple"> {
|
|
402
|
+
const items = components.map(
|
|
403
|
+
(t, i) => t.accept(this, `_${i}`) as ResultNode
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const node: ResultNode<"tuple"> = {
|
|
407
|
+
type: "tuple",
|
|
408
|
+
label,
|
|
409
|
+
displayLabel: formatLabel(label),
|
|
410
|
+
candidType: "tuple",
|
|
411
|
+
displayType: "array",
|
|
412
|
+
items,
|
|
413
|
+
resolve(data: unknown): ResolvedNode<"tuple"> {
|
|
414
|
+
if (data === null || data === undefined || !Array.isArray(data)) {
|
|
415
|
+
throw new MetadataError(
|
|
416
|
+
`Expected tuple, but got ${data === null ? "null" : typeof data}, raw: ${data}`,
|
|
417
|
+
label,
|
|
418
|
+
"tuple"
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
const tupleData = data as unknown[]
|
|
422
|
+
return {
|
|
423
|
+
...node,
|
|
424
|
+
items: items.map((item, i) => item.resolve(tupleData[i])),
|
|
425
|
+
raw: data,
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
return node
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public visitOpt<T>(
|
|
433
|
+
_t: IDL.OptClass<T>,
|
|
434
|
+
ty: IDL.Type<T>,
|
|
435
|
+
label: string
|
|
436
|
+
): ResultNode<"optional"> {
|
|
437
|
+
const inner = ty.accept(this, label) as ResultNode
|
|
438
|
+
|
|
439
|
+
const node: ResultNode<"optional"> = {
|
|
440
|
+
type: "optional",
|
|
441
|
+
label,
|
|
442
|
+
displayLabel: formatLabel(label),
|
|
443
|
+
candidType: "opt",
|
|
444
|
+
displayType: "nullable",
|
|
445
|
+
value: null, // null until resolved
|
|
446
|
+
resolve(data: unknown): ResolvedNode<"optional"> {
|
|
447
|
+
// If data is an array (raw format [T] or []), unwrap it.
|
|
448
|
+
// Otherwise, use data directly (already transformed or null/undefined).
|
|
449
|
+
const resolved = Array.isArray(data)
|
|
450
|
+
? data.length > 0
|
|
451
|
+
? inner.resolve(data[0])
|
|
452
|
+
: null
|
|
453
|
+
: data !== null && data !== undefined
|
|
454
|
+
? inner.resolve(data)
|
|
455
|
+
: null
|
|
456
|
+
|
|
457
|
+
return { ...node, value: resolved, raw: data }
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
return node
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
public visitVec<T>(
|
|
464
|
+
_t: IDL.VecClass<T>,
|
|
465
|
+
ty: IDL.Type<T>,
|
|
466
|
+
label: string
|
|
467
|
+
): ResultNode<"vector"> | ResultNode<"blob"> {
|
|
468
|
+
// Blob detection (vec nat8)
|
|
469
|
+
if (ty instanceof IDL.FixedNatClass && ty._bits === 8) {
|
|
470
|
+
const codec = this.getCodec(_t)
|
|
471
|
+
const node: ResultNode<"blob"> = {
|
|
472
|
+
type: "blob",
|
|
473
|
+
label,
|
|
474
|
+
displayLabel: formatLabel(label),
|
|
475
|
+
candidType: "blob",
|
|
476
|
+
displayType: "string",
|
|
477
|
+
length: 0,
|
|
478
|
+
hash: "",
|
|
479
|
+
value: "", // empty schema placeholder, populated on resolve
|
|
480
|
+
resolve(data: unknown): ResolvedNode<"blob"> {
|
|
481
|
+
const value = codec.decode(data) as string | Uint8Array
|
|
482
|
+
return {
|
|
483
|
+
...node,
|
|
484
|
+
value,
|
|
485
|
+
displayType: typeof value === "string" ? "string" : "blob",
|
|
486
|
+
hash: uint8ArrayToHex(
|
|
487
|
+
sha256(
|
|
488
|
+
data instanceof Uint8Array
|
|
489
|
+
? data
|
|
490
|
+
: new Uint8Array(data as number[])
|
|
491
|
+
)
|
|
492
|
+
),
|
|
493
|
+
length: value.length,
|
|
494
|
+
raw: data,
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
return node
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const itemSchema = ty.accept(this, "item") as ResultNode
|
|
502
|
+
|
|
503
|
+
const node: ResultNode<"vector"> = {
|
|
504
|
+
type: "vector",
|
|
505
|
+
label,
|
|
506
|
+
displayLabel: formatLabel(label),
|
|
507
|
+
candidType: "vec",
|
|
508
|
+
displayType: "array",
|
|
509
|
+
items: [], // empty schema placeholder, populated on resolve
|
|
510
|
+
resolve(data: unknown): ResolvedNode<"vector"> {
|
|
511
|
+
if (data === null || data === undefined || !Array.isArray(data)) {
|
|
512
|
+
throw new MetadataError(
|
|
513
|
+
`Expected vector, but got ${data === null ? "null" : typeof data}, raw: ${data}`,
|
|
514
|
+
label,
|
|
515
|
+
"vec"
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
const vectorData = data as unknown[]
|
|
519
|
+
return {
|
|
520
|
+
...node,
|
|
521
|
+
items: vectorData.map((v) => itemSchema.resolve(v)),
|
|
522
|
+
raw: data,
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
return node
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
public visitRec<T>(
|
|
530
|
+
t: IDL.RecClass<T>,
|
|
531
|
+
ty: IDL.ConstructType<T>,
|
|
532
|
+
label: string
|
|
533
|
+
): ResultNode<"recursive"> {
|
|
534
|
+
if (this.recCache.has(t)) {
|
|
535
|
+
return this.recCache.get(t)! as ResultNode<"recursive">
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const self = this
|
|
539
|
+
// Lazy extraction to prevent infinite loops
|
|
540
|
+
let innerSchema: ResultNode | null = null
|
|
541
|
+
const getInner = () =>
|
|
542
|
+
(innerSchema ??= ty.accept(self, label) as ResultNode)
|
|
543
|
+
|
|
544
|
+
const node: ResultNode<"recursive"> = {
|
|
545
|
+
type: "recursive",
|
|
546
|
+
label,
|
|
547
|
+
displayLabel: formatLabel(label),
|
|
548
|
+
candidType: "rec",
|
|
549
|
+
displayType: "recursive",
|
|
550
|
+
inner: {} as ResultNode, // placeholder, populated on resolve
|
|
551
|
+
resolve(data: unknown): ResolvedNode<"recursive"> {
|
|
552
|
+
return { ...node, inner: getInner().resolve(data), raw: data }
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.recCache.set(t, node)
|
|
557
|
+
return node
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
561
|
+
// Primitives - Using Factory
|
|
562
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
563
|
+
|
|
564
|
+
public visitPrincipal(
|
|
565
|
+
t: IDL.PrincipalClass,
|
|
566
|
+
label: string
|
|
567
|
+
): ResultNode<"principal"> {
|
|
568
|
+
return primitiveNode(
|
|
569
|
+
"principal",
|
|
570
|
+
label,
|
|
571
|
+
"principal",
|
|
572
|
+
"string",
|
|
573
|
+
this.getCodec(t),
|
|
574
|
+
{
|
|
575
|
+
format: checkTextFormat(label) as TextFormat,
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
public visitText(t: IDL.TextClass, label: string): ResultNode<"text"> {
|
|
581
|
+
return primitiveNode("text", label, "text", "string", this.getCodec(t), {
|
|
582
|
+
format: checkTextFormat(label) as TextFormat,
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
public visitBool(t: IDL.BoolClass, label: string): ResultNode<"boolean"> {
|
|
587
|
+
return primitiveNode("boolean", label, "bool", "boolean", this.getCodec(t))
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
public visitNull(t: IDL.NullClass, label: string): ResultNode<"null"> {
|
|
591
|
+
return primitiveNode("null", label, "null", "null", this.getCodec(t))
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
public visitInt(t: IDL.IntClass, label: string): ResultNode<"number"> {
|
|
595
|
+
return primitiveNode("number", label, "int", "string", this.getCodec(t), {
|
|
596
|
+
format: checkNumberFormat(label) as NumberFormat,
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
public visitNat(t: IDL.NatClass, label: string): ResultNode<"number"> {
|
|
601
|
+
return primitiveNode("number", label, "nat", "string", this.getCodec(t), {
|
|
602
|
+
format: checkNumberFormat(label) as NumberFormat,
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
public visitFloat(t: IDL.FloatClass, label: string): ResultNode<"number"> {
|
|
607
|
+
return primitiveNode(
|
|
608
|
+
"number",
|
|
609
|
+
label,
|
|
610
|
+
`float${t._bits}`,
|
|
611
|
+
"number",
|
|
612
|
+
this.getCodec(t),
|
|
613
|
+
{
|
|
614
|
+
format: checkNumberFormat(label) as NumberFormat,
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
public visitFixedInt(
|
|
620
|
+
t: IDL.FixedIntClass,
|
|
621
|
+
label: string
|
|
622
|
+
): ResultNode<"number"> {
|
|
623
|
+
const bits = t._bits
|
|
624
|
+
return primitiveNode(
|
|
625
|
+
"number",
|
|
626
|
+
label,
|
|
627
|
+
`int${bits}`,
|
|
628
|
+
bits <= 32 ? "number" : "string",
|
|
629
|
+
this.getCodec(t),
|
|
630
|
+
{
|
|
631
|
+
format: checkNumberFormat(label) as NumberFormat,
|
|
632
|
+
}
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
public visitFixedNat(
|
|
637
|
+
t: IDL.FixedNatClass,
|
|
638
|
+
label: string
|
|
639
|
+
): ResultNode<"number"> {
|
|
640
|
+
const bits = t._bits
|
|
641
|
+
return primitiveNode(
|
|
642
|
+
"number",
|
|
643
|
+
label,
|
|
644
|
+
`nat${bits}`,
|
|
645
|
+
bits <= 32 ? "number" : "string",
|
|
646
|
+
this.getCodec(t),
|
|
647
|
+
{
|
|
648
|
+
format: checkNumberFormat(label) as NumberFormat,
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
public visitType<T>(_t: IDL.Type<T>, label: string): ResultNode<"unknown"> {
|
|
654
|
+
return primitiveNode("unknown", label, "unknown", "unknown", {
|
|
655
|
+
decode: (v) => v,
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
}
|