codama-renderers-dart 0.5.0 → 0.5.3
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 → README.md} +38 -42
- package/dist/index.browser.cjs +628 -147
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +628 -148
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +628 -147
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +628 -148
- package/dist/index.node.js.map +1 -1
- package/dist/index.react-native.js +628 -148
- 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/nameTransformers.d.ts +5 -0
- package/dist/types/utils/nameTransformers.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/src/fragments/accountPage.ts +90 -17
- package/src/fragments/instructionPage.ts +112 -79
- package/src/fragments/typePage.ts +134 -31
- 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/nameTransformers.ts +28 -1
- 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 +37 -14
- 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`;
|
|
@@ -116,6 +178,13 @@ export function getInstructionPageFragment(
|
|
|
116
178
|
const hasProgramAddressCollision = argNames.has('programAddress') || accountNames.has('programAddress');
|
|
117
179
|
const instrProgramParam = hasProgramAddressCollision ? 'instructionProgramAddress' : 'programAddress';
|
|
118
180
|
|
|
181
|
+
// The generated local holding the constructed instruction data must not
|
|
182
|
+
// collide with an account or argument parameter name.
|
|
183
|
+
let instructionDataLocal = 'instructionData';
|
|
184
|
+
while (argNames.has(instructionDataLocal) || accountNames.has(instructionDataLocal)) {
|
|
185
|
+
instructionDataLocal = `${instructionDataLocal}_`;
|
|
186
|
+
}
|
|
187
|
+
|
|
119
188
|
// Build the instruction builder function
|
|
120
189
|
const accountParams = accounts
|
|
121
190
|
.map((acc) => {
|
|
@@ -155,7 +224,10 @@ export function getInstructionPageFragment(
|
|
|
155
224
|
const role = getAccountRole(acc);
|
|
156
225
|
const isOptional = acc.isOptional ?? false;
|
|
157
226
|
if (isOptional) {
|
|
158
|
-
|
|
227
|
+
if (optionalAccountStrategy === "omitted") {
|
|
228
|
+
return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}),`;
|
|
229
|
+
}
|
|
230
|
+
return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}) else AccountMeta(address: ${instrProgramParam}, role: AccountRole.readonly),`;
|
|
159
231
|
}
|
|
160
232
|
return ` AccountMeta(address: ${fieldName}, role: ${role}),`;
|
|
161
233
|
})
|
|
@@ -165,19 +237,31 @@ export function getInstructionPageFragment(
|
|
|
165
237
|
const dataConstruction = allArgs
|
|
166
238
|
.map((arg) => {
|
|
167
239
|
const fieldName = camelCase(arg.name as string);
|
|
168
|
-
if (
|
|
240
|
+
if (
|
|
241
|
+
isOmittedDefaultArgument(arg) ||
|
|
242
|
+
isDiscriminatorArg(arg, node)
|
|
243
|
+
) {
|
|
169
244
|
return ""; // Use default
|
|
170
245
|
}
|
|
171
|
-
// Use 'arg_<name>' prefix to reference builder params without shadowing.
|
|
172
|
-
const typeStr2 = argManifestMap.get(arg)?.type.content;
|
|
173
246
|
const skipDefault = arg.defaultValue?.kind === "accountBumpValueNode";
|
|
174
|
-
return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${
|
|
247
|
+
return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${argDefaultValues.get(arg)!.content}` : ""},`;
|
|
175
248
|
})
|
|
176
249
|
.filter(Boolean)
|
|
177
250
|
.join("\n");
|
|
178
251
|
|
|
179
252
|
// Discriminator
|
|
180
253
|
const discFragment = getDiscriminatorConstantsFragment(node, scope);
|
|
254
|
+
const discriminatorValidation = getDiscriminatorValidationFragment(
|
|
255
|
+
node,
|
|
256
|
+
scope,
|
|
257
|
+
);
|
|
258
|
+
const topLevelDecoder = getTopLevelDecoderFragment({
|
|
259
|
+
typeName: dataClassName,
|
|
260
|
+
description: `${name} instruction decoder`,
|
|
261
|
+
discriminatorValidation,
|
|
262
|
+
fromMapFields,
|
|
263
|
+
requireExactConsumption: true,
|
|
264
|
+
});
|
|
181
265
|
|
|
182
266
|
const parts: Fragment[] = [
|
|
183
267
|
fragment`// Auto-generated. Do not edit.
|
|
@@ -205,9 +289,7 @@ ${use("AccountRole", "solanaInstructions")}`,
|
|
|
205
289
|
parts.push(fragment`
|
|
206
290
|
@immutable
|
|
207
291
|
class ${fragmentFromString(dataClassName)} {
|
|
208
|
-
${fragmentFromString(
|
|
209
|
-
${fragmentFromString(dataCtorParams)}
|
|
210
|
-
})${fragmentFromString(dataCtorInitializers ? ` :\n${dataCtorInitializers}` : "")};
|
|
292
|
+
${fragmentFromString(dataCtorSignature)}
|
|
211
293
|
|
|
212
294
|
${fragmentFromString(dataFieldDecls)}
|
|
213
295
|
}`);
|
|
@@ -232,12 +314,7 @@ Decoder<${fragmentFromString(dataClassName)}> ${fragmentFromString(dataDecoderNa
|
|
|
232
314
|
${fragmentFromString(decFields)}
|
|
233
315
|
]);
|
|
234
316
|
|
|
235
|
-
|
|
236
|
-
structDecoder,
|
|
237
|
-
(Map<String, Object?> map, Uint8List bytes, int offset) => ${fragmentFromString(dataClassName)}(
|
|
238
|
-
${fragmentFromString(fromMapFields)}
|
|
239
|
-
),
|
|
240
|
-
);
|
|
317
|
+
${topLevelDecoder}
|
|
241
318
|
}
|
|
242
319
|
|
|
243
320
|
Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}> ${fragmentFromString(dataCodecName)}() {
|
|
@@ -252,7 +329,7 @@ Instruction ${fragmentFromString(instrFnName)}({
|
|
|
252
329
|
${fragmentFromString(accountParams)}
|
|
253
330
|
${fragmentFromString(argParams)}
|
|
254
331
|
}) {
|
|
255
|
-
final
|
|
332
|
+
final ${instructionDataLocal} = ${fragmentFromString(dataClassName)}(
|
|
256
333
|
${fragmentFromString(dataConstruction)}
|
|
257
334
|
);
|
|
258
335
|
|
|
@@ -261,7 +338,7 @@ ${fragmentFromString(dataConstruction)}
|
|
|
261
338
|
accounts: [
|
|
262
339
|
${fragmentFromString(accountMetas)}
|
|
263
340
|
],
|
|
264
|
-
data: ${fragmentFromString(dataEncoderName)}().encode(
|
|
341
|
+
data: ${fragmentFromString(dataEncoderName)}().encode(${instructionDataLocal}),
|
|
265
342
|
);
|
|
266
343
|
}`);
|
|
267
344
|
|
|
@@ -280,6 +357,9 @@ ${fragmentFromString(dataClassName)} ${fragmentFromString(parseFnName)}(Instruct
|
|
|
280
357
|
result.imports.mergeWith(manifest.decoder.imports);
|
|
281
358
|
result.imports.mergeWith(manifest.type.imports);
|
|
282
359
|
}
|
|
360
|
+
for (const defaultValue of argDefaultValues.values()) {
|
|
361
|
+
result.imports.mergeWith(defaultValue.imports);
|
|
362
|
+
}
|
|
283
363
|
|
|
284
364
|
return result;
|
|
285
365
|
}
|
|
@@ -305,23 +385,6 @@ function isDiscriminatorArg(
|
|
|
305
385
|
);
|
|
306
386
|
}
|
|
307
387
|
|
|
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
388
|
function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue"]): boolean {
|
|
326
389
|
if (!defaultValue) return false;
|
|
327
390
|
switch (defaultValue.kind) {
|
|
@@ -335,36 +398,6 @@ function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue
|
|
|
335
398
|
}
|
|
336
399
|
}
|
|
337
400
|
|
|
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
|
-
}
|
|
401
|
+
function isOmittedDefaultArgument(arg: InstructionArgumentNode): boolean {
|
|
402
|
+
return arg.defaultValue != null && arg.defaultValueStrategy === "omitted";
|
|
370
403
|
}
|
|
@@ -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;
|
|
@@ -127,11 +147,12 @@ function getDataEnumPageFragment(
|
|
|
127
147
|
const decoderVariants: string[] = [];
|
|
128
148
|
const encodeCases: string[] = [];
|
|
129
149
|
const decodeCases: string[] = [];
|
|
150
|
+
let pageHasListEnums = false;
|
|
130
151
|
// Collect all field type manifests from variants so we can merge their imports
|
|
131
152
|
const allVariantManifests: { encoder: Fragment; decoder: Fragment; type: Fragment }[] = [];
|
|
132
153
|
|
|
133
|
-
for (let i = 0; i <
|
|
134
|
-
const variant =
|
|
154
|
+
for (let i = 0; i < variants.length; i++) {
|
|
155
|
+
const variant = variants[i];
|
|
135
156
|
const variantName = pascalCase(variant.name as string);
|
|
136
157
|
const variantClassName = scope.nameApi.sealedClassVariant(
|
|
137
158
|
name,
|
|
@@ -163,7 +184,7 @@ function getDataEnumPageFragment(
|
|
|
163
184
|
decodeCases.push(`case ${i}: return const ${variantClassName}();`);
|
|
164
185
|
} else if (variant.kind === "enumStructVariantTypeNode") {
|
|
165
186
|
const resolvedStruct = resolveNestedTypeNode(variant.struct);
|
|
166
|
-
const fields = resolvedStruct.fields;
|
|
187
|
+
const fields = resolvedStruct.fields ?? [];
|
|
167
188
|
|
|
168
189
|
// Visit each field type once and collect manifests
|
|
169
190
|
const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
|
|
@@ -192,12 +213,18 @@ function getDataEnumPageFragment(
|
|
|
192
213
|
fields.length === 0
|
|
193
214
|
? "true"
|
|
194
215
|
: fields
|
|
195
|
-
.map((f: StructFieldTypeNode) =>
|
|
216
|
+
.map((f: StructFieldTypeNode, index) => {
|
|
217
|
+
const fieldName = camelCase(f.name as string);
|
|
218
|
+
return isListManifest(fieldManifests[index].manifest)
|
|
219
|
+
? `_listEquals(${fieldName}, other.${fieldName})`
|
|
220
|
+
: `${fieldName} == other.${fieldName}`;
|
|
221
|
+
})
|
|
196
222
|
.join(" &&\n ");
|
|
197
223
|
|
|
198
|
-
const hashExpression =
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
const hashExpression = getValueHashExpression(fields, fieldManifests);
|
|
225
|
+
if (fieldManifests.some(({ manifest }) => isListManifest(manifest))) {
|
|
226
|
+
pageHasListEnums = true;
|
|
227
|
+
}
|
|
201
228
|
|
|
202
229
|
const toStringFields = fields
|
|
203
230
|
.map((f: StructFieldTypeNode) => `${camelCase(f.name as string)}: $${camelCase(f.name as string)}`)
|
|
@@ -271,7 +298,7 @@ ${fieldDecls}
|
|
|
271
298
|
decodeCases.push(`case ${i}: return ${variantClassName}(${fromMapFields.replace(/\n/g, ' ').replace(/,$/, '')});`);
|
|
272
299
|
} else if (variant.kind === "enumTupleVariantTypeNode") {
|
|
273
300
|
const resolvedTuple = resolveNestedTypeNode(variant.tuple);
|
|
274
|
-
const items = resolvedTuple.items;
|
|
301
|
+
const items = resolvedTuple.items ?? [];
|
|
275
302
|
if (items.length === 1) {
|
|
276
303
|
const manifest = visit(items[0], scope.typeManifestVisitor);
|
|
277
304
|
allVariantManifests.push(manifest);
|
|
@@ -331,7 +358,7 @@ sealed class ${fragmentFromString(typeName)} {
|
|
|
331
358
|
const ${fragmentFromString(typeName)}();
|
|
332
359
|
}
|
|
333
360
|
|
|
334
|
-
${fragmentFromString(variantClasses.join("\n\n"))}
|
|
361
|
+
${fragmentFromString(variantClasses.join("\n\n"))}${pageHasListEnums ? LIST_VALUE_HELPERS : ""}
|
|
335
362
|
|
|
336
363
|
Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
|
|
337
364
|
return transformEncoder<Map<String, Object?>, ${fragmentFromString(typeName)}>(
|
|
@@ -384,7 +411,8 @@ function getStructPageFragment(
|
|
|
384
411
|
const structNode = node.type;
|
|
385
412
|
if (structNode.kind !== "structTypeNode") return emptyFragment();
|
|
386
413
|
|
|
387
|
-
const fields = structNode.fields;
|
|
414
|
+
const fields = structNode.fields ?? [];
|
|
415
|
+
let pageHasListStructs = false;
|
|
388
416
|
|
|
389
417
|
// Visit each field type once and collect manifests for reuse
|
|
390
418
|
const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
|
|
@@ -409,19 +437,17 @@ function getStructPageFragment(
|
|
|
409
437
|
const eqChecks =
|
|
410
438
|
fields.length === 0
|
|
411
439
|
? "true"
|
|
412
|
-
:
|
|
413
|
-
.map(
|
|
414
|
-
(f
|
|
415
|
-
|
|
416
|
-
|
|
440
|
+
: fieldManifests
|
|
441
|
+
.map(({ field: f, manifest }, index) => {
|
|
442
|
+
const fieldName = camelCase(f.name as string);
|
|
443
|
+
return isListManifest(manifest)
|
|
444
|
+
? `_listEquals(${fieldName}, other.${fieldName})`
|
|
445
|
+
: `${fieldName} == other.${fieldName}`;
|
|
446
|
+
})
|
|
417
447
|
.join(" &&\n ");
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
? "runtimeType"
|
|
422
|
-
: fields
|
|
423
|
-
.map((f: StructFieldTypeNode) => camelCase(f.name as string))
|
|
424
|
-
.join(", ");
|
|
448
|
+
if (fieldManifests.some(({ manifest }) => isListManifest(manifest))) {
|
|
449
|
+
pageHasListStructs = true;
|
|
450
|
+
}
|
|
425
451
|
|
|
426
452
|
const toStringFields = fields
|
|
427
453
|
.map(
|
|
@@ -491,12 +517,14 @@ ${fragmentFromString(fieldDecls)}
|
|
|
491
517
|
${fragmentFromString(eqChecks)};
|
|
492
518
|
|
|
493
519
|
@override
|
|
494
|
-
int get hashCode => ${fragmentFromString(
|
|
520
|
+
int get hashCode => ${fragmentFromString(
|
|
521
|
+
getValueHashExpression(fields, fieldManifests),
|
|
522
|
+
)};
|
|
495
523
|
|
|
496
524
|
@override
|
|
497
525
|
String toString() => '${fragmentFromString(typeName)}(${fragmentFromString(toStringFields)})';
|
|
498
526
|
}
|
|
499
|
-
|
|
527
|
+
${pageHasListStructs ? LIST_VALUE_HELPERS : ""}
|
|
500
528
|
Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
|
|
501
529
|
final structEncoder = getStructEncoder(<(String, Encoder<Object?>)>[
|
|
502
530
|
${fragmentFromString(encFields)}
|
|
@@ -575,6 +603,81 @@ Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragme
|
|
|
575
603
|
}`;
|
|
576
604
|
}
|
|
577
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Returns true when the rendered field type is a Dart list or byte sequence,
|
|
608
|
+
* whose reference equality must be replaced with a deep comparison in
|
|
609
|
+
* generated value classes.
|
|
610
|
+
*/
|
|
611
|
+
function isListManifest(manifest: { type: Fragment }): boolean {
|
|
612
|
+
return manifest.type.content.startsWith("List<") ||
|
|
613
|
+
manifest.type.content === "Uint8List";
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Private Dart helpers emitted into pages whose value classes contain
|
|
618
|
+
* list-typed fields, so `==` and `hashCode` match recursive structural value
|
|
619
|
+
* equality instead of list reference identity. The recursive branch matters
|
|
620
|
+
* for generated types such as `List<Uint8List>`.
|
|
621
|
+
*/
|
|
622
|
+
const LIST_VALUE_HELPERS = `
|
|
623
|
+
bool _listEquals<T>(List<T>? a, List<T>? b) {
|
|
624
|
+
if (identical(a, b)) return true;
|
|
625
|
+
if (a == null || b == null) return a == b;
|
|
626
|
+
if (a.length != b.length) return false;
|
|
627
|
+
for (var i = 0; i < a.length; i++) {
|
|
628
|
+
final left = a[i];
|
|
629
|
+
final right = b[i];
|
|
630
|
+
if (left is List<Object?> && right is List<Object?>) {
|
|
631
|
+
if (!_listEquals(left, right)) return false;
|
|
632
|
+
} else if (left != right) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
Object? _deepHash(Object? value) {
|
|
640
|
+
if (value is List<Object?>) {
|
|
641
|
+
return Object.hashAll(value.map(_deepHash));
|
|
642
|
+
}
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
int _listHashCode<T>(List<T>? a) {
|
|
647
|
+
if (a == null) return 0;
|
|
648
|
+
return Object.hashAll(a.map(_deepHash));
|
|
649
|
+
}
|
|
650
|
+
`;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Builds the `hashCode` expression for a class whose fields render as the
|
|
654
|
+
* given lowercase names. List-typed fields compare structurally through the
|
|
655
|
+
* private `_listHashCode` helper emitted alongside the class.
|
|
656
|
+
*/
|
|
657
|
+
function getValueHashExpression(
|
|
658
|
+
fields: StructFieldTypeNode[],
|
|
659
|
+
fieldManifests: { field: StructFieldTypeNode; manifest: { type: Fragment } }[],
|
|
660
|
+
): string {
|
|
661
|
+
const fieldNames = fields.map((f: StructFieldTypeNode) =>
|
|
662
|
+
camelCase(f.name as string),
|
|
663
|
+
);
|
|
664
|
+
const hasListFields = fieldManifests.some(({ manifest }) =>
|
|
665
|
+
isListManifest(manifest),
|
|
666
|
+
);
|
|
667
|
+
if (!hasListFields) {
|
|
668
|
+
return getHashExpression(fieldNames);
|
|
669
|
+
}
|
|
670
|
+
if (fieldNames.length === 1) {
|
|
671
|
+
return `_listHashCode(${fieldNames[0]})`;
|
|
672
|
+
}
|
|
673
|
+
const hashArgs = fields.map((f: StructFieldTypeNode, index) =>
|
|
674
|
+
isListManifest(fieldManifests[index].manifest)
|
|
675
|
+
? `_listHashCode(${camelCase(f.name as string)})`
|
|
676
|
+
: camelCase(f.name as string),
|
|
677
|
+
);
|
|
678
|
+
return `Object.hash(${hashArgs.join(", ")})`;
|
|
679
|
+
}
|
|
680
|
+
|
|
578
681
|
function getHashExpression(fields: string[]): string {
|
|
579
682
|
if (fields.length === 0) return "runtimeType.hashCode";
|
|
580
683
|
if (fields.length === 1) return `${fields[0]}.hashCode`;
|
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";
|