codama-renderers-dart 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.browser.cjs +456 -127
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +456 -128
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +456 -127
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +456 -128
- package/dist/index.node.js.map +1 -1
- package/dist/index.react-native.js +456 -128
- package/dist/index.react-native.js.map +1 -1
- package/dist/types/fragments/accountPage.d.ts.map +1 -1
- package/dist/types/fragments/instructionPage.d.ts.map +1 -1
- package/dist/types/fragments/typePage.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils/discriminators.d.ts +10 -0
- package/dist/types/utils/discriminators.d.ts.map +1 -0
- package/dist/types/utils/exactDecoder.d.ts +17 -0
- package/dist/types/utils/exactDecoder.d.ts.map +1 -0
- package/dist/types/utils/formatCode.d.ts +1 -1
- package/dist/types/utils/formatCode.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +4 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/normalizeRootNode.d.ts +11 -0
- package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
- package/dist/types/utils/valueNodes.d.ts +16 -0
- package/dist/types/utils/valueNodes.d.ts.map +1 -0
- package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
- package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
- package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
- package/package.json +21 -18
- package/readme.md +29 -0
- package/src/fragments/accountPage.ts +90 -17
- package/src/fragments/instructionPage.ts +103 -77
- package/src/fragments/typePage.ts +32 -12
- package/src/index.ts +1 -0
- package/src/utils/discriminators.ts +121 -0
- package/src/utils/exactDecoder.ts +82 -0
- package/src/utils/formatCode.ts +7 -11
- package/src/utils/index.ts +4 -0
- package/src/utils/normalizeRootNode.ts +76 -0
- package/src/utils/valueNodes.ts +87 -0
- package/src/visitors/getRenderMapVisitor.ts +1 -1
- package/src/visitors/getTypeManifestVisitor.ts +14 -8
- package/src/visitors/renderVisitor.ts +4 -1
|
@@ -7,16 +7,20 @@ import { visit } from "@codama/visitors-core";
|
|
|
7
7
|
|
|
8
8
|
import type { Fragment } from "../utils/fragment.js";
|
|
9
9
|
import {
|
|
10
|
-
emptyFragment,
|
|
11
10
|
fragment,
|
|
12
11
|
fragmentFromString,
|
|
13
12
|
mergeFragments,
|
|
14
13
|
use,
|
|
15
14
|
} from "../utils/fragment.js";
|
|
16
15
|
import type { RenderScope } from "../utils/options.js";
|
|
17
|
-
import { camelCase
|
|
16
|
+
import { camelCase } from "../utils/nameTransformers.js";
|
|
17
|
+
import { getDiscriminatorValidationFragment } from "../utils/discriminators.js";
|
|
18
|
+
import { getTopLevelDecoderFragment } from "../utils/exactDecoder.js";
|
|
19
|
+
import {
|
|
20
|
+
getDartValueFragment,
|
|
21
|
+
isConstDartValueNode,
|
|
22
|
+
} from "../utils/valueNodes.js";
|
|
18
23
|
import { getDiscriminatorConstantsFragment } from "./discriminatorConstants.js";
|
|
19
|
-
import { WELL_KNOWN_ADDRESSES } from "../utils/wellKnownAddresses.js";
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Generate a full Dart file for an instruction.
|
|
@@ -31,19 +35,55 @@ export function getInstructionPageFragment(
|
|
|
31
35
|
const parseFnName = scope.nameApi.instructionParseFunction(name);
|
|
32
36
|
|
|
33
37
|
const accounts = node.accounts ?? [];
|
|
34
|
-
const args = (node.arguments ?? []).filter(
|
|
35
|
-
(arg) => !isDiscriminatorArg(arg, node),
|
|
36
|
-
);
|
|
37
38
|
const allArgs = node.arguments ?? [];
|
|
39
|
+
const optionalAccountStrategy: string =
|
|
40
|
+
node.optionalAccountStrategy ?? "programId";
|
|
41
|
+
if (
|
|
42
|
+
optionalAccountStrategy !== "programId" &&
|
|
43
|
+
optionalAccountStrategy !== "omitted"
|
|
44
|
+
) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Unsupported optional account strategy on instruction "${node.name}": ${optionalAccountStrategy}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const args = allArgs.filter(
|
|
50
|
+
(arg) =>
|
|
51
|
+
!isDiscriminatorArg(arg, node) && !isOmittedDefaultArgument(arg),
|
|
52
|
+
);
|
|
38
53
|
|
|
39
54
|
// Build the instruction data class
|
|
40
55
|
const dataClassName = `${typeName}InstructionData`;
|
|
41
56
|
|
|
42
57
|
// Visit each arg type once and collect manifests for reuse
|
|
43
|
-
const allArgManifests =
|
|
58
|
+
const allArgManifests = (node.arguments ?? []).map((arg) => ({
|
|
44
59
|
arg,
|
|
45
60
|
manifest: visit(arg.type, scope.typeManifestVisitor),
|
|
46
61
|
}));
|
|
62
|
+
const argDefaultValues = new Map(
|
|
63
|
+
allArgManifests.flatMap(({ arg, manifest }) => {
|
|
64
|
+
if (arg.defaultValue == null) return [];
|
|
65
|
+
const needsRenderedDefault =
|
|
66
|
+
isOmittedDefaultArgument(arg) ||
|
|
67
|
+
isDiscriminatorArg(arg, node) ||
|
|
68
|
+
arg.defaultValue.kind !== "accountBumpValueNode";
|
|
69
|
+
if (!needsRenderedDefault) return [];
|
|
70
|
+
return [
|
|
71
|
+
[
|
|
72
|
+
arg,
|
|
73
|
+
getDartValueFragment(arg.defaultValue, manifest.type.content),
|
|
74
|
+
] as const,
|
|
75
|
+
];
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
for (const arg of allArgs.filter((candidate) =>
|
|
79
|
+
isDiscriminatorArg(candidate, node),
|
|
80
|
+
)) {
|
|
81
|
+
if (!argDefaultValues.has(arg)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Field discriminator "${arg.name}" on instruction "${node.name}" must reference a field with a default value.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
47
87
|
|
|
48
88
|
// Data fields (non-discriminator arguments)
|
|
49
89
|
const dataFieldDecls = allArgManifests
|
|
@@ -56,20 +96,25 @@ export function getInstructionPageFragment(
|
|
|
56
96
|
const dataCtorParams = allArgManifests
|
|
57
97
|
.map(({ arg, manifest }) => {
|
|
58
98
|
const fieldName = camelCase(arg.name as string);
|
|
99
|
+
if (isOmittedDefaultArgument(arg)) return "";
|
|
59
100
|
if (isDiscriminatorArg(arg, node)) {
|
|
60
101
|
return isConstDefaultValue(arg.defaultValue)
|
|
61
|
-
? ` this.${fieldName} = ${
|
|
102
|
+
? ` this.${fieldName} = ${argDefaultValues.get(arg)!.content},`
|
|
62
103
|
: ` ${manifest.type.content}? ${fieldName},`;
|
|
63
104
|
}
|
|
64
105
|
return ` required this.${fieldName},`;
|
|
65
106
|
})
|
|
107
|
+
.filter(Boolean)
|
|
66
108
|
.join("\n");
|
|
67
109
|
|
|
68
110
|
const dataCtorInitializers = allArgManifests
|
|
69
111
|
.map(({ arg }) => {
|
|
70
112
|
const fieldName = camelCase(arg.name as string);
|
|
113
|
+
if (isOmittedDefaultArgument(arg)) {
|
|
114
|
+
return ` ${fieldName} = ${argDefaultValues.get(arg)!.content}`;
|
|
115
|
+
}
|
|
71
116
|
if (isDiscriminatorArg(arg, node) && !isConstDefaultValue(arg.defaultValue)) {
|
|
72
|
-
return ` ${fieldName} = ${fieldName} ?? ${
|
|
117
|
+
return ` ${fieldName} = ${fieldName} ?? ${argDefaultValues.get(arg)!.content}`;
|
|
73
118
|
}
|
|
74
119
|
return null;
|
|
75
120
|
})
|
|
@@ -90,13 +135,17 @@ export function getInstructionPageFragment(
|
|
|
90
135
|
.join("\n");
|
|
91
136
|
|
|
92
137
|
const toMapFields = allArgs
|
|
93
|
-
.map(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
138
|
+
.map((arg) => {
|
|
139
|
+
const value =
|
|
140
|
+
isOmittedDefaultArgument(arg) || isDiscriminatorArg(arg, node)
|
|
141
|
+
? argDefaultValues.get(arg)!.content
|
|
142
|
+
: `value.${camelCase(arg.name as string)}`;
|
|
143
|
+
return ` '${arg.name as string}': ${value},`;
|
|
144
|
+
})
|
|
97
145
|
.join("\n");
|
|
98
146
|
|
|
99
147
|
const fromMapFields = allArgManifests
|
|
148
|
+
.filter(({ arg }) => !isOmittedDefaultArgument(arg))
|
|
100
149
|
.map(({ arg, manifest }) => {
|
|
101
150
|
const typeStr = manifest.type.content;
|
|
102
151
|
const isNullable = typeStr.endsWith("?");
|
|
@@ -105,7 +154,20 @@ export function getInstructionPageFragment(
|
|
|
105
154
|
})
|
|
106
155
|
.join("\n");
|
|
107
156
|
|
|
108
|
-
const dataCtorKeyword =
|
|
157
|
+
const dataCtorKeyword = allArgs.every(
|
|
158
|
+
(arg) =>
|
|
159
|
+
!isOmittedDefaultArgument(arg) ||
|
|
160
|
+
isConstDartValueNode(arg.defaultValue!),
|
|
161
|
+
) && allArgs.every(
|
|
162
|
+
(arg) =>
|
|
163
|
+
!isDiscriminatorArg(arg, node) ||
|
|
164
|
+
isConstDefaultValue(arg.defaultValue),
|
|
165
|
+
)
|
|
166
|
+
? "const "
|
|
167
|
+
: "";
|
|
168
|
+
const dataCtorSignature = dataCtorParams
|
|
169
|
+
? `${dataCtorKeyword}${dataClassName}({\n${dataCtorParams}\n })${dataCtorInitializers ? ` :\n${dataCtorInitializers}` : ""};`
|
|
170
|
+
: `${dataCtorKeyword}${dataClassName}()${dataCtorInitializers ? ` :\n${dataCtorInitializers}` : ""};`;
|
|
109
171
|
const dataEncoderName = `get${typeName}InstructionDataEncoder`;
|
|
110
172
|
const dataDecoderName = `get${typeName}InstructionDataDecoder`;
|
|
111
173
|
const dataCodecName = `get${typeName}InstructionDataCodec`;
|
|
@@ -155,7 +217,10 @@ export function getInstructionPageFragment(
|
|
|
155
217
|
const role = getAccountRole(acc);
|
|
156
218
|
const isOptional = acc.isOptional ?? false;
|
|
157
219
|
if (isOptional) {
|
|
158
|
-
|
|
220
|
+
if (optionalAccountStrategy === "omitted") {
|
|
221
|
+
return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}),`;
|
|
222
|
+
}
|
|
223
|
+
return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}) else AccountMeta(address: ${instrProgramParam}, role: AccountRole.readonly),`;
|
|
159
224
|
}
|
|
160
225
|
return ` AccountMeta(address: ${fieldName}, role: ${role}),`;
|
|
161
226
|
})
|
|
@@ -165,19 +230,31 @@ export function getInstructionPageFragment(
|
|
|
165
230
|
const dataConstruction = allArgs
|
|
166
231
|
.map((arg) => {
|
|
167
232
|
const fieldName = camelCase(arg.name as string);
|
|
168
|
-
if (
|
|
233
|
+
if (
|
|
234
|
+
isOmittedDefaultArgument(arg) ||
|
|
235
|
+
isDiscriminatorArg(arg, node)
|
|
236
|
+
) {
|
|
169
237
|
return ""; // Use default
|
|
170
238
|
}
|
|
171
|
-
// Use 'arg_<name>' prefix to reference builder params without shadowing.
|
|
172
|
-
const typeStr2 = argManifestMap.get(arg)?.type.content;
|
|
173
239
|
const skipDefault = arg.defaultValue?.kind === "accountBumpValueNode";
|
|
174
|
-
return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${
|
|
240
|
+
return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${argDefaultValues.get(arg)!.content}` : ""},`;
|
|
175
241
|
})
|
|
176
242
|
.filter(Boolean)
|
|
177
243
|
.join("\n");
|
|
178
244
|
|
|
179
245
|
// Discriminator
|
|
180
246
|
const discFragment = getDiscriminatorConstantsFragment(node, scope);
|
|
247
|
+
const discriminatorValidation = getDiscriminatorValidationFragment(
|
|
248
|
+
node,
|
|
249
|
+
scope,
|
|
250
|
+
);
|
|
251
|
+
const topLevelDecoder = getTopLevelDecoderFragment({
|
|
252
|
+
typeName: dataClassName,
|
|
253
|
+
description: `${name} instruction decoder`,
|
|
254
|
+
discriminatorValidation,
|
|
255
|
+
fromMapFields,
|
|
256
|
+
requireExactConsumption: true,
|
|
257
|
+
});
|
|
181
258
|
|
|
182
259
|
const parts: Fragment[] = [
|
|
183
260
|
fragment`// Auto-generated. Do not edit.
|
|
@@ -205,9 +282,7 @@ ${use("AccountRole", "solanaInstructions")}`,
|
|
|
205
282
|
parts.push(fragment`
|
|
206
283
|
@immutable
|
|
207
284
|
class ${fragmentFromString(dataClassName)} {
|
|
208
|
-
${fragmentFromString(
|
|
209
|
-
${fragmentFromString(dataCtorParams)}
|
|
210
|
-
})${fragmentFromString(dataCtorInitializers ? ` :\n${dataCtorInitializers}` : "")};
|
|
285
|
+
${fragmentFromString(dataCtorSignature)}
|
|
211
286
|
|
|
212
287
|
${fragmentFromString(dataFieldDecls)}
|
|
213
288
|
}`);
|
|
@@ -232,12 +307,7 @@ Decoder<${fragmentFromString(dataClassName)}> ${fragmentFromString(dataDecoderNa
|
|
|
232
307
|
${fragmentFromString(decFields)}
|
|
233
308
|
]);
|
|
234
309
|
|
|
235
|
-
|
|
236
|
-
structDecoder,
|
|
237
|
-
(Map<String, Object?> map, Uint8List bytes, int offset) => ${fragmentFromString(dataClassName)}(
|
|
238
|
-
${fragmentFromString(fromMapFields)}
|
|
239
|
-
),
|
|
240
|
-
);
|
|
310
|
+
${topLevelDecoder}
|
|
241
311
|
}
|
|
242
312
|
|
|
243
313
|
Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}> ${fragmentFromString(dataCodecName)}() {
|
|
@@ -280,6 +350,9 @@ ${fragmentFromString(dataClassName)} ${fragmentFromString(parseFnName)}(Instruct
|
|
|
280
350
|
result.imports.mergeWith(manifest.decoder.imports);
|
|
281
351
|
result.imports.mergeWith(manifest.type.imports);
|
|
282
352
|
}
|
|
353
|
+
for (const defaultValue of argDefaultValues.values()) {
|
|
354
|
+
result.imports.mergeWith(defaultValue.imports);
|
|
355
|
+
}
|
|
283
356
|
|
|
284
357
|
return result;
|
|
285
358
|
}
|
|
@@ -305,23 +378,6 @@ function isDiscriminatorArg(
|
|
|
305
378
|
);
|
|
306
379
|
}
|
|
307
380
|
|
|
308
|
-
function getDiscriminatorDefault(
|
|
309
|
-
arg: InstructionArgumentNode,
|
|
310
|
-
_node: InstructionNode,
|
|
311
|
-
): string {
|
|
312
|
-
if (arg.defaultValue) {
|
|
313
|
-
if (arg.defaultValue.kind === "numberValueNode") {
|
|
314
|
-
return String(arg.defaultValue.number);
|
|
315
|
-
}
|
|
316
|
-
if (arg.defaultValue.kind === "bytesValueNode") {
|
|
317
|
-
const data = arg.defaultValue.data as string;
|
|
318
|
-
const bytes = data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
|
|
319
|
-
return `Uint8List.fromList([${bytes.join(", ")}])`;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return "0";
|
|
323
|
-
}
|
|
324
|
-
|
|
325
381
|
function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue"]): boolean {
|
|
326
382
|
if (!defaultValue) return false;
|
|
327
383
|
switch (defaultValue.kind) {
|
|
@@ -335,36 +391,6 @@ function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue
|
|
|
335
391
|
}
|
|
336
392
|
}
|
|
337
393
|
|
|
338
|
-
function
|
|
339
|
-
arg
|
|
340
|
-
typeStr?: string,
|
|
341
|
-
): string {
|
|
342
|
-
if (!arg.defaultValue) return "null";
|
|
343
|
-
const isBigInt = typeStr === "BigInt";
|
|
344
|
-
const dv = arg.defaultValue;
|
|
345
|
-
switch (dv.kind) {
|
|
346
|
-
case "numberValueNode":
|
|
347
|
-
// Dart's `BigInt` requires a constructor; `0` is not assignable to `BigInt`.
|
|
348
|
-
return isBigInt ? `BigInt.from(${dv.number})` : String(dv.number);
|
|
349
|
-
case "booleanValueNode":
|
|
350
|
-
return String(dv.boolean);
|
|
351
|
-
case "stringValueNode":
|
|
352
|
-
return `'${dv.string}'`;
|
|
353
|
-
case "publicKeyValueNode": {
|
|
354
|
-
const wellKnownName = WELL_KNOWN_ADDRESSES.get(dv.publicKey);
|
|
355
|
-
if (wellKnownName) {
|
|
356
|
-
use(wellKnownName, "solanaAddresses");
|
|
357
|
-
return wellKnownName;
|
|
358
|
-
}
|
|
359
|
-
return `Address('${dv.publicKey}')`;
|
|
360
|
-
}
|
|
361
|
-
case "noneValueNode":
|
|
362
|
-
return "null";
|
|
363
|
-
case "bytesValueNode": {
|
|
364
|
-
const bytes = dv.data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
|
|
365
|
-
return `Uint8List.fromList([${bytes.join(", ")}])`;
|
|
366
|
-
}
|
|
367
|
-
default:
|
|
368
|
-
return "null";
|
|
369
|
-
}
|
|
394
|
+
function isOmittedDefaultArgument(arg: InstructionArgumentNode): boolean {
|
|
395
|
+
return arg.defaultValue != null && arg.defaultValueStrategy === "omitted";
|
|
370
396
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DefinedTypeNode,
|
|
3
|
+
EnumVariantTypeNode,
|
|
4
|
+
StructFieldTypeNode,
|
|
5
|
+
TypeNode,
|
|
6
|
+
} from "@codama/nodes";
|
|
2
7
|
import { resolveNestedTypeNode } from "@codama/nodes";
|
|
3
8
|
import { visit } from "@codama/visitors-core";
|
|
4
9
|
|
|
@@ -30,13 +35,14 @@ export function getTypePageFragment(
|
|
|
30
35
|
|
|
31
36
|
// Check what kind of type this is
|
|
32
37
|
if (typeNode.kind === "enumTypeNode") {
|
|
33
|
-
const
|
|
38
|
+
const variants = typeNode.variants ?? [];
|
|
39
|
+
const isScalar = variants.every(
|
|
34
40
|
(v) => v.kind === "enumEmptyVariantTypeNode",
|
|
35
41
|
);
|
|
36
42
|
if (isScalar) {
|
|
37
|
-
return getScalarEnumPageFragment(node, scope);
|
|
43
|
+
return getScalarEnumPageFragment(node, variants, scope);
|
|
38
44
|
}
|
|
39
|
-
return getDataEnumPageFragment(node, scope);
|
|
45
|
+
return getDataEnumPageFragment(node, variants, scope);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
if (typeNode.kind === "structTypeNode") {
|
|
@@ -52,6 +58,7 @@ export function getTypePageFragment(
|
|
|
52
58
|
*/
|
|
53
59
|
function getScalarEnumPageFragment(
|
|
54
60
|
node: DefinedTypeNode,
|
|
61
|
+
enumVariants: EnumVariantTypeNode[],
|
|
55
62
|
scope: RenderScope,
|
|
56
63
|
): Fragment {
|
|
57
64
|
const name = node.name as string;
|
|
@@ -59,7 +66,7 @@ function getScalarEnumPageFragment(
|
|
|
59
66
|
const enumNode = node.type;
|
|
60
67
|
if (enumNode.kind !== "enumTypeNode") return emptyFragment();
|
|
61
68
|
|
|
62
|
-
const variants =
|
|
69
|
+
const variants = enumVariants.map((v) => {
|
|
63
70
|
const variantName = scope.nameApi.enumVariant(v.name as string);
|
|
64
71
|
return variantName;
|
|
65
72
|
});
|
|
@@ -73,6 +80,18 @@ function getScalarEnumPageFragment(
|
|
|
73
80
|
const resolvedSize = resolveNestedTypeNode(enumNode.size);
|
|
74
81
|
const sizeType = resolvedSize.format ?? "u8";
|
|
75
82
|
const sizeCodecName = getNumberCodecName(sizeType);
|
|
83
|
+
const isU64 = sizeType === "u64";
|
|
84
|
+
const encodedVariantIndex = isU64
|
|
85
|
+
? "BigInt.from(value.index)"
|
|
86
|
+
: "value.index";
|
|
87
|
+
const decodedVariant = isU64
|
|
88
|
+
? `(BigInt value, Uint8List bytes, int offset) {
|
|
89
|
+
if (value.isNegative || value >= BigInt.from(${typeName}.values.length)) {
|
|
90
|
+
throw RangeError('Invalid ${typeName} discriminator: ' + value.toString());
|
|
91
|
+
}
|
|
92
|
+
return ${typeName}.values[value.toInt()];
|
|
93
|
+
}`
|
|
94
|
+
: `(int value, Uint8List bytes, int offset) => ${typeName}.values[value]`;
|
|
76
95
|
|
|
77
96
|
return fragment`// Auto-generated. Do not edit.
|
|
78
97
|
// ignore_for_file: type=lint
|
|
@@ -94,14 +113,14 @@ ${fragmentFromString(variantLines)}
|
|
|
94
113
|
Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
|
|
95
114
|
return transformEncoder(
|
|
96
115
|
get${fragmentFromString(sizeCodecName)}Encoder(),
|
|
97
|
-
(${fragmentFromString(typeName)} value) =>
|
|
116
|
+
(${fragmentFromString(typeName)} value) => ${fragmentFromString(encodedVariantIndex)},
|
|
98
117
|
);
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
Decoder<${fragmentFromString(typeName)}> ${fragmentFromString(decoderName)}() {
|
|
102
121
|
return transformDecoder(
|
|
103
122
|
get${fragmentFromString(sizeCodecName)}Decoder(),
|
|
104
|
-
|
|
123
|
+
${fragmentFromString(decodedVariant)},
|
|
105
124
|
);
|
|
106
125
|
}
|
|
107
126
|
|
|
@@ -115,6 +134,7 @@ Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragme
|
|
|
115
134
|
*/
|
|
116
135
|
function getDataEnumPageFragment(
|
|
117
136
|
node: DefinedTypeNode,
|
|
137
|
+
variants: EnumVariantTypeNode[],
|
|
118
138
|
scope: RenderScope,
|
|
119
139
|
): Fragment {
|
|
120
140
|
const name = node.name as string;
|
|
@@ -130,8 +150,8 @@ function getDataEnumPageFragment(
|
|
|
130
150
|
// Collect all field type manifests from variants so we can merge their imports
|
|
131
151
|
const allVariantManifests: { encoder: Fragment; decoder: Fragment; type: Fragment }[] = [];
|
|
132
152
|
|
|
133
|
-
for (let i = 0; i <
|
|
134
|
-
const variant =
|
|
153
|
+
for (let i = 0; i < variants.length; i++) {
|
|
154
|
+
const variant = variants[i];
|
|
135
155
|
const variantName = pascalCase(variant.name as string);
|
|
136
156
|
const variantClassName = scope.nameApi.sealedClassVariant(
|
|
137
157
|
name,
|
|
@@ -163,7 +183,7 @@ function getDataEnumPageFragment(
|
|
|
163
183
|
decodeCases.push(`case ${i}: return const ${variantClassName}();`);
|
|
164
184
|
} else if (variant.kind === "enumStructVariantTypeNode") {
|
|
165
185
|
const resolvedStruct = resolveNestedTypeNode(variant.struct);
|
|
166
|
-
const fields = resolvedStruct.fields;
|
|
186
|
+
const fields = resolvedStruct.fields ?? [];
|
|
167
187
|
|
|
168
188
|
// Visit each field type once and collect manifests
|
|
169
189
|
const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
|
|
@@ -271,7 +291,7 @@ ${fieldDecls}
|
|
|
271
291
|
decodeCases.push(`case ${i}: return ${variantClassName}(${fromMapFields.replace(/\n/g, ' ').replace(/,$/, '')});`);
|
|
272
292
|
} else if (variant.kind === "enumTupleVariantTypeNode") {
|
|
273
293
|
const resolvedTuple = resolveNestedTypeNode(variant.tuple);
|
|
274
|
-
const items = resolvedTuple.items;
|
|
294
|
+
const items = resolvedTuple.items ?? [];
|
|
275
295
|
if (items.length === 1) {
|
|
276
296
|
const manifest = visit(items[0], scope.typeManifestVisitor);
|
|
277
297
|
allVariantManifests.push(manifest);
|
|
@@ -384,7 +404,7 @@ function getStructPageFragment(
|
|
|
384
404
|
const structNode = node.type;
|
|
385
405
|
if (structNode.kind !== "structTypeNode") return emptyFragment();
|
|
386
406
|
|
|
387
|
-
const fields = structNode.fields;
|
|
407
|
+
const fields = structNode.fields ?? [];
|
|
388
408
|
|
|
389
409
|
// Visit each field type once and collect manifests for reuse
|
|
390
410
|
const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { getRenderMapVisitor } from "./visitors/getRenderMapVisitor.js";
|
|
|
3
3
|
export { getTypeManifestVisitor } from "./visitors/getTypeManifestVisitor.js";
|
|
4
4
|
export { DartImportMap, DART_EXTERNAL_PACKAGE_MAP } from "./utils/importMap.js";
|
|
5
5
|
export { createDartNameApi } from "./utils/nameTransformers.js";
|
|
6
|
+
export { normalizeRootNode } from "./utils/normalizeRootNode.js";
|
|
6
7
|
export type { DartNameApi } from "./utils/nameTransformers.js";
|
|
7
8
|
export type { RenderOptions, GetRenderMapOptions, RenderScope } from "./utils/options.js";
|
|
8
9
|
export type { TypeManifest } from "./utils/typeManifest.js";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountNode,
|
|
3
|
+
ConstantValueNode,
|
|
4
|
+
InstructionNode,
|
|
5
|
+
StructFieldTypeNode,
|
|
6
|
+
} from "@codama/nodes";
|
|
7
|
+
import { resolveNestedTypeNode } from "@codama/nodes";
|
|
8
|
+
import { visit } from "@codama/visitors-core";
|
|
9
|
+
|
|
10
|
+
import type { Fragment } from "./fragment.js";
|
|
11
|
+
import {
|
|
12
|
+
emptyFragment,
|
|
13
|
+
fragment,
|
|
14
|
+
fragmentFromString,
|
|
15
|
+
mergeFragments,
|
|
16
|
+
use,
|
|
17
|
+
} from "./fragment.js";
|
|
18
|
+
import type { RenderScope } from "./options.js";
|
|
19
|
+
import { getDartValueFragment } from "./valueNodes.js";
|
|
20
|
+
|
|
21
|
+
type DiscriminatedNode = AccountNode | InstructionNode;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate decoder guards for every discriminator declared by a Codama node.
|
|
25
|
+
*/
|
|
26
|
+
export function getDiscriminatorValidationFragment(
|
|
27
|
+
node: DiscriminatedNode,
|
|
28
|
+
scope: RenderScope,
|
|
29
|
+
): Fragment {
|
|
30
|
+
const discriminators = node.discriminators ?? [];
|
|
31
|
+
if (discriminators.length === 0) return emptyFragment();
|
|
32
|
+
|
|
33
|
+
const fields = getDiscriminatorFields(node);
|
|
34
|
+
const fragments = discriminators.map((discriminator): Fragment => {
|
|
35
|
+
switch (discriminator.kind) {
|
|
36
|
+
case "constantDiscriminatorNode":
|
|
37
|
+
return getConstantValidationFragment(
|
|
38
|
+
discriminator.constant,
|
|
39
|
+
discriminator.offset,
|
|
40
|
+
scope,
|
|
41
|
+
);
|
|
42
|
+
case "fieldDiscriminatorNode": {
|
|
43
|
+
const field = fields.find(
|
|
44
|
+
(candidate) => candidate.name === discriminator.name,
|
|
45
|
+
);
|
|
46
|
+
if (field?.defaultValue == null) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Field discriminator "${discriminator.name}" on ${node.kind} "${node.name}" must reference a field with a default value.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return getConstantValidationFragment(
|
|
52
|
+
{
|
|
53
|
+
kind: "constantValueNode",
|
|
54
|
+
type: field.type,
|
|
55
|
+
value: field.defaultValue,
|
|
56
|
+
} as ConstantValueNode,
|
|
57
|
+
discriminator.offset,
|
|
58
|
+
scope,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
case "sizeDiscriminatorNode": {
|
|
62
|
+
if (
|
|
63
|
+
!Number.isSafeInteger(discriminator.size) ||
|
|
64
|
+
discriminator.size < 0
|
|
65
|
+
) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Discriminator size must be a non-negative safe integer, got ${discriminator.size}.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return fragment`if (bytes.length - offset != ${fragmentFromString(String(discriminator.size))}) {
|
|
71
|
+
throw ${use("SolanaError", "solanaErrors")}(
|
|
72
|
+
${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
|
|
73
|
+
{
|
|
74
|
+
'codecDescription': '${fragmentFromString(node.name as string)} discriminator',
|
|
75
|
+
'expected': ${fragmentFromString(String(discriminator.size))},
|
|
76
|
+
'bytesLength': bytes.length - offset,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return mergeFragments(fragments, (contents) => contents.join("\n"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getConstantValidationFragment(
|
|
88
|
+
constant: ConstantValueNode,
|
|
89
|
+
discriminatorOffset: number,
|
|
90
|
+
scope: RenderScope,
|
|
91
|
+
): Fragment {
|
|
92
|
+
if (!Number.isSafeInteger(discriminatorOffset) || discriminatorOffset < 0) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Discriminator offset must be a non-negative safe integer, got ${discriminatorOffset}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const manifest = visit(constant.type, scope.typeManifestVisitor);
|
|
99
|
+
const value = getDartValueFragment(
|
|
100
|
+
constant.value,
|
|
101
|
+
manifest.type.content,
|
|
102
|
+
);
|
|
103
|
+
return fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(
|
|
104
|
+
${manifest.encoder}.encode(${value}),
|
|
105
|
+
).read(bytes, offset + ${fragmentFromString(String(discriminatorOffset))});`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getDiscriminatorFields(
|
|
109
|
+
node: DiscriminatedNode,
|
|
110
|
+
): readonly StructFieldTypeNode[] {
|
|
111
|
+
if (node.kind === "accountNode") {
|
|
112
|
+
return resolveNestedTypeNode(node.data).fields ?? [];
|
|
113
|
+
}
|
|
114
|
+
return (node.arguments ?? []).map(
|
|
115
|
+
(argument) =>
|
|
116
|
+
({
|
|
117
|
+
...argument,
|
|
118
|
+
kind: "structFieldTypeNode",
|
|
119
|
+
}) as StructFieldTypeNode,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Fragment } from "./fragment.js";
|
|
2
|
+
import { fragment, fragmentFromString, use } from "./fragment.js";
|
|
3
|
+
|
|
4
|
+
export interface TopLevelDecoderFragmentOptions {
|
|
5
|
+
readonly typeName: string;
|
|
6
|
+
readonly description: string;
|
|
7
|
+
readonly discriminatorValidation: Fragment;
|
|
8
|
+
readonly fromMapFields: string;
|
|
9
|
+
readonly requireExactConsumption: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a generated top-level struct decoder with discriminator and length
|
|
14
|
+
* validation.
|
|
15
|
+
*
|
|
16
|
+
* The wrapper preserves fixed/variable decoder metadata so it remains
|
|
17
|
+
* compatible with the matching generated encoder when combined into a codec.
|
|
18
|
+
*/
|
|
19
|
+
export function getTopLevelDecoderFragment({
|
|
20
|
+
typeName,
|
|
21
|
+
description,
|
|
22
|
+
discriminatorValidation,
|
|
23
|
+
fromMapFields,
|
|
24
|
+
requireExactConsumption,
|
|
25
|
+
}: TopLevelDecoderFragmentOptions): Fragment {
|
|
26
|
+
const validation = discriminatorValidation.content
|
|
27
|
+
? ` ${discriminatorValidation.content.replaceAll("\n", "\n ")}\n`
|
|
28
|
+
: "";
|
|
29
|
+
|
|
30
|
+
const consumptionCheck = requireExactConsumption
|
|
31
|
+
? ` if (newOffset != bytes.length) {
|
|
32
|
+
throwInvalidByteLength(newOffset - offset, bytes.length - offset);
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
: "";
|
|
36
|
+
const fixedSizeCheck = requireExactConsumption
|
|
37
|
+
? "bytesLength != structDecoder.fixedSize"
|
|
38
|
+
: "bytesLength < structDecoder.fixedSize";
|
|
39
|
+
|
|
40
|
+
const result = fragment` Never throwInvalidByteLength(int expected, int bytesLength) {
|
|
41
|
+
throw ${use("SolanaError", "solanaErrors")}(
|
|
42
|
+
${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
|
|
43
|
+
{
|
|
44
|
+
'codecDescription': '${fragmentFromString(description)}',
|
|
45
|
+
'expected': expected,
|
|
46
|
+
'bytesLength': bytesLength,
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
(${fragmentFromString(typeName)}, int) readTopLevel(Uint8List bytes, int offset) {
|
|
52
|
+
${fragmentFromString(validation)} final (map, newOffset) = structDecoder.read(bytes, offset);
|
|
53
|
+
${fragmentFromString(consumptionCheck)}
|
|
54
|
+
return (
|
|
55
|
+
${fragmentFromString(typeName)}(
|
|
56
|
+
${fragmentFromString(fromMapFields)}
|
|
57
|
+
),
|
|
58
|
+
newOffset,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return switch (structDecoder) {
|
|
63
|
+
${use("FixedSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
|
|
64
|
+
${use("FixedSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
|
|
65
|
+
fixedSize: structDecoder.fixedSize,
|
|
66
|
+
read: (bytes, offset) {
|
|
67
|
+
final bytesLength = bytes.length - offset;
|
|
68
|
+
if (${fragmentFromString(fixedSizeCheck)}) {
|
|
69
|
+
throwInvalidByteLength(structDecoder.fixedSize, bytesLength);
|
|
70
|
+
}
|
|
71
|
+
return readTopLevel(bytes, offset);
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
${use("VariableSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
|
|
75
|
+
${use("VariableSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
|
|
76
|
+
read: readTopLevel,
|
|
77
|
+
maxSize: structDecoder.maxSize,
|
|
78
|
+
),
|
|
79
|
+
};`;
|
|
80
|
+
result.imports.mergeWith(discriminatorValidation.imports);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
package/src/utils/formatCode.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Format Dart code using `dart format`.
|
|
5
|
-
* Falls back to returning the unformatted code if
|
|
5
|
+
* Falls back to returning the unformatted code if Dart is not available.
|
|
6
6
|
*/
|
|
7
7
|
export function formatDartCode(code: string): string {
|
|
8
8
|
try {
|
|
9
|
-
const result = execSync("dart format --
|
|
9
|
+
const result = execSync("dart format --output=show", {
|
|
10
10
|
input: code,
|
|
11
11
|
encoding: "utf-8",
|
|
12
12
|
timeout: 30_000,
|
|
@@ -23,13 +23,9 @@ export function formatDartCode(code: string): string {
|
|
|
23
23
|
* Format all Dart files in a directory using `dart format`.
|
|
24
24
|
*/
|
|
25
25
|
export function formatDartDirectory(dir: string): void {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
// Silently ignore if dart format is not available
|
|
34
|
-
}
|
|
26
|
+
execSync(`dart format "${dir}"`, {
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
timeout: 60_000,
|
|
29
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
});
|
|
35
31
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export * from "./codecs.js";
|
|
2
|
+
export * from "./discriminators.js";
|
|
3
|
+
export * from "./exactDecoder.js";
|
|
2
4
|
export * from "./formatCode.js";
|
|
3
5
|
export * from "./fragment.js";
|
|
4
6
|
export * from "./importMap.js";
|
|
5
7
|
export * from "./nameTransformers.js";
|
|
8
|
+
export * from "./normalizeRootNode.js";
|
|
6
9
|
export * from "./options.js";
|
|
7
10
|
export * from "./typeManifest.js";
|
|
11
|
+
export * from "./valueNodes.js";
|
|
8
12
|
export * from "./wellKnownAddresses.js";
|