1
1
#!/usr/bin/env -S node --experimental-strip-types
2
2
3
- import { type DescField , ScalarType } from "@bufbuild/protobuf" ;
3
+ import { type DescField , type DescMessage , ScalarType } from "@bufbuild/protobuf" ;
4
4
import {
5
5
createEcmaScriptPlugin ,
6
+ type GeneratedFile ,
6
7
runNodeJs ,
7
8
type Schema ,
8
9
} from "@bufbuild/protoplugin" ;
9
- import { normalize as normalizePath } from "path" ;
10
+ import { basename , normalize as normalizePath } from "path" ;
10
11
11
12
import { findPathsToCustomField , getCustomType } from "../src/encoding/customTypes/utils.ts" ;
12
13
@@ -18,6 +19,7 @@ runNodeJs(
18
19
} ) ,
19
20
) ;
20
21
22
+ const PROTO_PATH = "../protos" ;
21
23
function generateTs ( schema : Schema ) : void {
22
24
const allPaths : DescField [ ] [ ] = [ ] ;
23
25
@@ -31,19 +33,22 @@ function generateTs(schema: Schema): void {
31
33
return ;
32
34
}
33
35
34
- const messageToCustomFields : Record < string , Set < DescField > > = { } ;
36
+ const messageToCustomFields : Map < DescMessage , Set < DescField > > = new Map ( ) ;
35
37
allPaths . forEach ( ( path ) => {
36
38
path . forEach ( ( field ) => {
37
- messageToCustomFields [ field . parent . typeName ] ??= new Set ( ) ;
38
- messageToCustomFields [ field . parent . typeName ] . add ( field ) ;
39
+ if ( ! messageToCustomFields . has ( field . parent ) ) {
40
+ messageToCustomFields . set ( field . parent , new Set ( ) ) ;
41
+ }
42
+ messageToCustomFields . get ( field . parent ) ! . add ( field ) ;
39
43
} ) ;
40
44
} ) ;
41
45
42
46
const patches : string [ ] = [ ] ;
43
47
const imports : Record < string , Set < string > > = { } ;
44
- const f = schema.generateFile(getOutputFileName(schema));
48
+ const fileName = getOutputFileName(schema);
49
+ const patchesFile = schema.generateFile(fileName);
45
50
46
- Object.entries (messageToCustomFields) .forEach(([typeName , fields]) => {
51
+ Array.from (messageToCustomFields.entries()) .forEach(([descMessage , fields]) => {
47
52
const encoded : string [ ] = [ ] ;
48
53
49
54
fields . forEach ( ( field ) => {
@@ -58,41 +63,44 @@ function generateTs(schema: Schema): void {
58
63
imports [ `../../encoding/binaryEncoding` ] ??= new Set ( [ "encodeBinary" , "decodeBinary" ] ) ;
59
64
}
60
65
61
- encoded . push ( generateNestedFieldSetter ( field , {
66
+ encoded . push ( generateFieldTransformation ( field , {
62
67
fn : `${ customType . shortName } [transformType]` ,
63
68
value : "value" ,
64
69
newValue : "newValue" ,
65
70
} ) ) ;
66
71
} else {
67
72
encoded . push ( generateNestedFieldUpdate ( field , {
68
- fn : `t ["${ field . message ! . typeName } "]` ,
73
+ fn : `p ["${ field . message ! . typeName } "]` ,
69
74
value : "value" ,
70
75
newValue : "newValue" ,
71
76
} ) ) ;
72
77
}
73
78
} ) ;
74
79
75
- const shapeImport = f . importShape ( fields . values ( ) . next ( ) . value . parent ) ;
76
- const path = normalizePath ( `../protos /${ shapeImport . from . replace ( / \. j s $ / , "" ) } ` ) ;
80
+ const shapeImport = patchesFile . importShape ( fields . values ( ) . next ( ) . value . parent ) ;
81
+ const path = normalizePath ( `${ PROTO_PATH } /${ shapeImport . from . replace ( / \. j s $ / , "" ) } ` ) ;
77
82
imports [ path ] ??= new Set ( [ "type *" ] ) ;
78
83
79
84
patches . push ( [
80
- `"${ typeName } "(value: ${ dirnameToVar ( path ) } .${ shapeImport . name } , transformType: 'encode' | 'decode') {\n${
81
- indent ( `if (value == null) return value ;` ) + "\n"
85
+ `"${ descMessage . typeName } "(value: ${ dirnameToVar ( path ) } .${ shapeImport . name } | undefined | null , transformType: 'encode' | 'decode') {\n${
86
+ indent ( `if (value == null) return;` ) + "\n"
82
87
+ indent ( `const newValue = { ...value };` ) + "\n"
83
- + indent ( encoded . join ( "; \n" ) ) + "\n"
88
+ + indent ( encoded . join ( "\n" ) ) + "\n"
84
89
+ indent ( "return newValue;" )
85
90
} \n}`,
86
91
] . join ( "\n" ) ) ;
87
92
} );
88
93
89
94
const importExtension = schema.options.importExtension ? `.${ schema . options . importExtension } ` : "";
90
95
Object.entries(imports).forEach(([path, symbols]) => {
91
- f . print ( `import ${ generateImportSymbols ( path , symbols ) } from "${ path } ${ importExtension } ";` ) ;
96
+ patchesFile . print ( `import ${ generateImportSymbols ( path , symbols ) } from "${ path } ${ importExtension } ";` ) ;
92
97
} );
93
- f.print("");
94
- f.print(`const t = { \n${indent ( patches . join ( ",\n" ) ) } \n} ; \n`) ;
95
- f . print ( `export const transformations = t ; `);
98
+ patchesFile.print("");
99
+ patchesFile.print(`const p = { \n${indent ( patches . join ( ",\n" ) ) } \n} ; \n`) ;
100
+ patchesFile . print ( `export const patches = p ; `);
101
+
102
+ const testsFile = schema.generateFile(fileName.replace(/\.ts$/, ".spec.ts"));
103
+ generateTests(basename(fileName), testsFile, messageToCustomFields);
96
104
}
97
105
98
106
function getOutputFileName(schema: Schema): string {
@@ -121,13 +129,13 @@ function generateNestedFieldUpdate(field: DescField, vars: VarNames) {
121
129
const fieldRef = ` $ { vars . value } . $ { field . localName } `;
122
130
const newValueRef = ` $ { vars . newValue } . ${field . localName } `;
123
131
if (field.fieldKind === "list") {
124
- return ` if ( $ { fieldRef} ) $ { newValueRef } = ${fieldRef } . map ( ( item ) => $ { vars . fn } ( item , transformType ) ) ; `;
132
+ return ` if ( $ { fieldRef} ) $ { newValueRef } = ${fieldRef } . map ( ( item ) => $ { vars . fn } ( item , transformType ) ! ) ; `;
125
133
}
126
134
127
135
if (field.fieldKind === "map") {
128
136
return ` if ( $ { fieldRef} ) { \n`
129
137
+ ` $ { newValueRef } = { } ; \n`
130
- + ` Object . keys ( $ { fieldRef} ) . forEach ( k => $ { newValueRef } [ k ] = $ { vars . fn } ( $ { fieldRef} [ k ] , transformType ) ) ; \n`
138
+ + ` Object . keys ( $ { fieldRef} ) . forEach ( k => $ { newValueRef } [ k ] = $ { vars . fn } ( $ { fieldRef} [ k ] , transformType ) ! ) ; \n`
131
139
+ ` } `;
132
140
}
133
141
@@ -144,7 +152,7 @@ function generateNestedFieldUpdate(field: DescField, vars: VarNames) {
144
152
return ` if ( $ { fieldRef} != null ) $ { newValueRef } = ${vars . fn } ( $ { fieldRef} , transformType ) ; `;
145
153
}
146
154
147
- function generateNestedFieldSetter (field: DescField, vars: VarNames) {
155
+ function generateFieldTransformation (field: DescField, vars: VarNames) {
148
156
const valueRef = ` $ { vars . value } . ${field . localName } `;
149
157
const newValueRef = ` $ { vars . newValue } . ${field . localName } `;
150
158
@@ -167,3 +175,54 @@ function generateImportSymbols(path: string, symbols: Set<string>): string {
167
175
if (symbols.has("*")) return ` * as $ { dirnameToVar ( path ) } `;
168
176
return ` { ${Array . from ( symbols ) . join ( ", " ) } } `;
169
177
}
178
+
179
+ function generateTests(fileName: string, testsFile: GeneratedFile, messageToCustomFields: Map<DescMessage, Set<DescField>>) {
180
+ testsFile.print(` import { expect , describe , it } from "@jest/globals" ; `);
181
+ testsFile.print(` import type { DescMessage } from "@bufbuild/protobuf" ; `);
182
+ testsFile.print(` import { patches } from "./${basename(fileName)}" ; `);
183
+ testsFile.print(` import { generateMessage } from "@test/helpers/generateMessage" ; `);
184
+ testsFile.print(` import type { TypePatches } from "../../utils/applyPatches" ; `);
185
+ testsFile.print("");
186
+ testsFile.print(` type MessageTypes = Record < string , { fields : string [ ] , schema : DescMessage } > ; `);
187
+ testsFile.print(` const messageTypes: MessageTypes = { `);
188
+ for (const [message, fields] of messageToCustomFields.entries()) {
189
+ const importedSchema = testsFile.importSchema(message);
190
+ testsFile.print(` "${message.typeName}" : { `);
191
+ testsFile.print(` fields: $ { JSON . stringify ( Array . from ( fields , ( f ) => f . localName ) ) } , `);
192
+ testsFile.print(` schema: `, {
193
+ ...importedSchema,
194
+ from: normalizePath(` $ { PROTO_PATH } / $ { importedSchema.f r o m } ` ) ,
195
+ } ) ;
196
+ testsFile . print ( ` },` ) ;
197
+ }
198
+ testsFile . print ( `};` ) ;
199
+ testsFile . print ( `describe("${ fileName } ", () => {` ) ;
200
+ testsFile . print ( ` describe.each(Object.entries(patches))('patch %s', (typeName, patch: TypePatches[keyof TypePatches]) => {` ) ;
201
+ testsFile . print ( ` it('returns undefined if receives null or undefined', () => {` ) ;
202
+ testsFile . print ( ` expect(patch(null, 'encode')).toBe(undefined);` ) ;
203
+ testsFile . print ( ` expect(patch(null, 'decode')).toBe(undefined);` ) ;
204
+ testsFile . print ( ` expect(patch(undefined, 'encode')).toBe(undefined);` ) ;
205
+ testsFile . print ( ` expect(patch(undefined, 'decode')).toBe(undefined);` ) ;
206
+ testsFile . print ( ` });` ) ;
207
+ testsFile . print ( "" ) ;
208
+ testsFile . print ( ` it.each(generateTestCases(typeName, messageTypes))('patches and returns cloned value: %s', (name, value) => {` ) ;
209
+ testsFile . print ( ` const transformedValue = patch(patch(value, 'encode'), 'decode');` ) ;
210
+ testsFile . print ( ` expect(value).toEqual(transformedValue);` ) ;
211
+ testsFile . print ( ` expect(value).not.toBe(transformedValue);` ) ;
212
+ testsFile . print ( ` });` ) ;
213
+ testsFile . print ( ` });` ) ;
214
+ testsFile . print ( "" ) ;
215
+ testsFile . print ( ` function generateTestCases(typeName: string, messageTypes: MessageTypes) {` ) ;
216
+ testsFile . print ( ` const type = messageTypes[typeName];` ) ;
217
+ testsFile . print ( ` const cases = type.fields.map((name) => ["single " + name + " field", generateMessage(type.schema, {` ) ;
218
+ testsFile . print ( ` ...messageTypes,` ) ;
219
+ testsFile . print ( ` [typeName]: {` ) ;
220
+ testsFile . print ( ` ...type,` ) ;
221
+ testsFile . print ( ` fields: [name],` ) ;
222
+ testsFile . print ( ` }` ) ;
223
+ testsFile . print ( ` })]);` ) ;
224
+ testsFile . print ( ` cases.push(["all fields", generateMessage(type.schema, messageTypes)]);` ) ;
225
+ testsFile . print ( ` return cases;` ) ;
226
+ testsFile . print ( ` }` ) ;
227
+ testsFile . print ( "});" ) ;
228
+ }
0 commit comments