11#!/usr/bin/env -S node --experimental-strip-types
22
3- import { type DescField , ScalarType } from "@bufbuild/protobuf" ;
3+ import { type DescField , type DescMessage , ScalarType } from "@bufbuild/protobuf" ;
44import {
55 createEcmaScriptPlugin ,
6+ type GeneratedFile ,
67 runNodeJs ,
78 type Schema ,
89} from "@bufbuild/protoplugin" ;
9- import { normalize as normalizePath } from "path" ;
10+ import { basename , normalize as normalizePath } from "path" ;
1011
1112import { findPathsToCustomField , getCustomType } from "../src/encoding/customTypes/utils.ts" ;
1213
@@ -18,6 +19,7 @@ runNodeJs(
1819 } ) ,
1920) ;
2021
22+ const PROTO_PATH = "../protos" ;
2123function generateTs ( schema : Schema ) : void {
2224 const allPaths : DescField [ ] [ ] = [ ] ;
2325
@@ -31,19 +33,22 @@ function generateTs(schema: Schema): void {
3133 return ;
3234 }
3335
34- const messageToCustomFields : Record < string , Set < DescField > > = { } ;
36+ const messageToCustomFields : Map < DescMessage , Set < DescField > > = new Map ( ) ;
3537 allPaths . forEach ( ( path ) => {
3638 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 ) ;
3943 } ) ;
4044 } ) ;
4145
4246 const patches : string [ ] = [ ] ;
4347 const imports : Record < string , Set < string > > = { } ;
44- const f = schema.generateFile(getOutputFileName(schema));
48+ const fileName = getOutputFileName(schema);
49+ const patchesFile = schema.generateFile(fileName);
4550
46- Object.entries (messageToCustomFields) .forEach(([typeName , fields]) => {
51+ Array.from (messageToCustomFields.entries()) .forEach(([descMessage , fields]) => {
4752 const encoded : string [ ] = [ ] ;
4853
4954 fields . forEach ( ( field ) => {
@@ -58,41 +63,44 @@ function generateTs(schema: Schema): void {
5863 imports [ `../../encoding/binaryEncoding` ] ??= new Set ( [ "encodeBinary" , "decodeBinary" ] ) ;
5964 }
6065
61- encoded . push ( generateNestedFieldSetter ( field , {
66+ encoded . push ( generateFieldTransformation ( field , {
6267 fn : `${ customType . shortName } [transformType]` ,
6368 value : "value" ,
6469 newValue : "newValue" ,
6570 } ) ) ;
6671 } else {
6772 encoded . push ( generateNestedFieldUpdate ( field , {
68- fn : `t ["${ field . message ! . typeName } "]` ,
73+ fn : `p ["${ field . message ! . typeName } "]` ,
6974 value : "value" ,
7075 newValue : "newValue" ,
7176 } ) ) ;
7277 }
7378 } ) ;
7479
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 $ / , "" ) } ` ) ;
7782 imports [ path ] ??= new Set ( [ "type *" ] ) ;
7883
7984 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"
8287 + indent ( `const newValue = { ...value };` ) + "\n"
83- + indent ( encoded . join ( "; \n" ) ) + "\n"
88+ + indent ( encoded . join ( "\n" ) ) + "\n"
8489 + indent ( "return newValue;" )
8590 } \n}`,
8691 ] . join ( "\n" ) ) ;
8792 } );
8893
8994 const importExtension = schema.options.importExtension ? `.${ schema . options . importExtension } ` : "";
9095 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 } ";` ) ;
9297 } );
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);
96104}
97105
98106function getOutputFileName(schema: Schema): string {
@@ -121,13 +129,13 @@ function generateNestedFieldUpdate(field: DescField, vars: VarNames) {
121129 const fieldRef = ` $ { vars . value } . $ { field . localName } `;
122130 const newValueRef = ` $ { vars . newValue } . ${field . localName } `;
123131 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 ) ! ) ; `;
125133 }
126134
127135 if (field.fieldKind === "map") {
128136 return ` if ( $ { fieldRef} ) { \n`
129137 + ` $ { 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`
131139 + ` } `;
132140 }
133141
@@ -144,7 +152,7 @@ function generateNestedFieldUpdate(field: DescField, vars: VarNames) {
144152 return ` if ( $ { fieldRef} != null ) $ { newValueRef } = ${vars . fn } ( $ { fieldRef} , transformType ) ; `;
145153}
146154
147- function generateNestedFieldSetter (field: DescField, vars: VarNames) {
155+ function generateFieldTransformation (field: DescField, vars: VarNames) {
148156 const valueRef = ` $ { vars . value } . ${field . localName } `;
149157 const newValueRef = ` $ { vars . newValue } . ${field . localName } `;
150158
@@ -167,3 +175,54 @@ function generateImportSymbols(path: string, symbols: Set<string>): string {
167175 if (symbols.has("*")) return ` * as $ { dirnameToVar ( path ) } `;
168176 return ` { ${Array . from ( symbols ) . join ( ", " ) } } `;
169177}
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