Repository: stephenh/ts-proto
Pull request #1085 (2024-08-09 13:21:34)

feat: Add options to limit generation of encode and decode methods to only specific message types
I created this PR as a possible to solution to the feature request submitted in #1084.

I added integration tests for test coverage, and updated the documentation to include the two new options.

stephenh: Hi @akrennmair , thanks for updating the PR! Looks like there is a build error with `MessageFns`...maybe we need two/separate input/output `MessageFns` types?
akrennmair: @stephenh I didn't want to touch the `MessageFns` type as I didn't know what it would break. What I did instead was to generate a tiny dummy encode or decode method that throws an error if the presence of the method is required. 
stephenh: Sounds great, thanks @akrennmair !

Patch:
diff --git a/README.markdown b/README.markdown
index 47b344c6..7726615a 100644
--- a/README.markdown
+++ b/README.markdown
@@ -408,6 +408,14 @@ Generated code will be placed in the Gradle build directory.
 
   This is useful if you want "only types".
 
+- With `--ts_proto_opt=outputEncodeIncludeTypes=regex`, the `Message.encode` method will only be output for types whose name matches the provided regular expression.
+
+  This is useful if you want to limit `encode` methods to only select types.
+
+- With `--ts_proto_opt=outputDecodeIncludeTypes=regex`, the `Message.decode` method will only be output for types whose name matches the provided regular expression.
+
+  This is useful if you want to limit `decode` methods to only select types.
+
 - With `--ts_proto_opt=outputJsonMethods=false`, the `Message.fromJSON` and `Message.toJSON` methods for working with JSON-coded data will not be output.
 
   This is also useful if you want "only types".
diff --git a/integration/output-decode-include-types/encode.proto b/integration/output-decode-include-types/encode.proto
new file mode 100644
index 00000000..78048a56
--- /dev/null
+++ b/integration/output-decode-include-types/encode.proto
@@ -0,0 +1,11 @@
+syntax = "proto3";
+
+import "google/protobuf/wrappers.proto";
+
+message EncodeWithDecodeMethod {
+  string encode = 1;
+}
+
+message EncodeDontGenerateDecodeMethod {
+  string encode_dont_generate_decode_method = 1;
+}
\ No newline at end of file
diff --git a/integration/output-decode-include-types/encode.ts b/integration/output-decode-include-types/encode.ts
new file mode 100644
index 00000000..86327542
--- /dev/null
+++ b/integration/output-decode-include-types/encode.ts
@@ -0,0 +1,49 @@
+// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
+// source: encode.proto
+
+/* eslint-disable */
+import { BinaryReader } from "@bufbuild/protobuf/wire";
+
+export const protobufPackage = "";
+
+export interface EncodeWithDecodeMethod {
+  encode: string;
+}
+
+export interface EncodeDontGenerateDecodeMethod {
+  encodeDontGenerateDecodeMethod: string;
+}
+
+function createBaseEncodeWithDecodeMethod(): EncodeWithDecodeMethod {
+  return { encode: "" };
+}
+
+export const EncodeWithDecodeMethod: MessageFns<EncodeWithDecodeMethod> = {
+  decode(input: BinaryReader | Uint8Array, length?: number): EncodeWithDecodeMethod {
+    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseEncodeWithDecodeMethod();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1: {
+          if (tag !== 10) {
+            break;
+          }
+
+          message.encode = reader.string();
+          continue;
+        }
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skip(tag & 7);
+    }
+    return message;
+  },
+};
+
+export interface MessageFns<T> {
+  decode(input: BinaryReader | Uint8Array, length?: number): T;
+}
diff --git a/integration/output-decode-include-types/parameters.txt b/integration/output-decode-include-types/parameters.txt
new file mode 100644
index 00000000..5507187b
--- /dev/null
+++ b/integration/output-decode-include-types/parameters.txt
@@ -0,0 +1,1 @@
+outputEncodeMethods=decode-only,outputPartialMethods=false,outputJsonMethods=false,nestJs=false,outputDecodeIncludeTypes=^EncodeWithDecodeMethod
\ No newline at end of file
diff --git a/integration/output-encode-decode-include-types-combined/parameters.txt b/integration/output-encode-decode-include-types-combined/parameters.txt
new file mode 100644
index 00000000..bfc76977
--- /dev/null
+++ b/integration/output-encode-decode-include-types-combined/parameters.txt
@@ -0,0 +1,1 @@
+outputEncodeMethods=true,outputPartialMethods=false,outputJsonMethods=false,nestJs=false,outputEncodeIncludeTypes=^Request,outputDecodeIncludeTypes=^Response$
diff --git a/integration/output-encode-decode-include-types-combined/request_response.proto b/integration/output-encode-decode-include-types-combined/request_response.proto
new file mode 100644
index 00000000..d31dd35c
--- /dev/null
+++ b/integration/output-encode-decode-include-types-combined/request_response.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+import "google/protobuf/wrappers.proto";
+
+message Request {
+  string request_id = 1;
+
+  message Nested {
+    string more_data = 1;
+  }
+
+  Nested nested = 2;
+}
+
+message Response {
+  string response_id = 1;
+}
diff --git a/integration/output-encode-decode-include-types-combined/request_response.ts b/integration/output-encode-decode-include-types-combined/request_response.ts
new file mode 100644
index 00000000..33466897
--- /dev/null
+++ b/integration/output-encode-decode-include-types-combined/request_response.ts
@@ -0,0 +1,88 @@
+// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
+// source: request_response.proto
+
+/* eslint-disable */
+import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
+
+export const protobufPackage = "";
+
+export interface Request {
+  requestId: string;
+  nested: Request_Nested | undefined;
+}
+
+export interface Request_Nested {
+  moreData: string;
+}
+
+export interface Response {
+  responseId: string;
+}
+
+export const Request: MessageFns<Request> = {
+  encode(message: Request, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+    if (message.requestId !== "") {
+      writer.uint32(10).string(message.requestId);
+    }
+    if (message.nested !== undefined) {
+      Request_Nested.encode(message.nested, writer.uint32(18).fork()).join();
+    }
+    return writer;
+  },
+
+  decode(_: BinaryReader | Uint8Array, length?: number): Request {
+    throw new Error("decode not generated for Request");
+  },
+};
+
+export const Request_Nested: MessageFns<Request_Nested> = {
+  encode(message: Request_Nested, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+    if (message.moreData !== "") {
+      writer.uint32(10).string(message.moreData);
+    }
+    return writer;
+  },
+
+  decode(_: BinaryReader | Uint8Array, length?: number): Request_Nested {
+    throw new Error("decode not generated for Request_Nested");
+  },
+};
+
+function createBaseResponse(): Response {
+  return { responseId: "" };
+}
+
+export const Response: MessageFns<Response> = {
+  encode(_: Response, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+    throw new Error("encode not generated for Response");
+  },
+
+  decode(input: BinaryReader | Uint8Array, length?: number): Response {
+    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseResponse();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1: {
+          if (tag !== 10) {
+            break;
+          }
+
+          message.responseId = reader.string();
+          continue;
+        }
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skip(tag & 7);
+    }
+    return message;
+  },
+};
+
+export interface MessageFns<T> {
+  encode(message: T, writer?: BinaryWriter): BinaryWriter;
+  decode(input: BinaryReader | Uint8Array, length?: number): T;
+}
diff --git a/integration/output-encode-include-types/encode.proto b/integration/output-encode-include-types/encode.proto
new file mode 100644
index 00000000..7e4909a9
--- /dev/null
+++ b/integration/output-encode-include-types/encode.proto
@@ -0,0 +1,11 @@
+syntax = "proto3";
+
+import "google/protobuf/wrappers.proto";
+
+message Encode {
+  string encode = 1;
+}
+
+message DontGenerateEncode {
+  string dont_generate_encode = 1;
+}
diff --git a/integration/output-encode-include-types/encode.ts b/integration/output-encode-include-types/encode.ts
new file mode 100644
index 00000000..5cfd950e
--- /dev/null
+++ b/integration/output-encode-include-types/encode.ts
@@ -0,0 +1,28 @@
+// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
+// source: encode.proto
+
+/* eslint-disable */
+import { BinaryWriter } from "@bufbuild/protobuf/wire";
+
+export const protobufPackage = "";
+
+export interface Encode {
+  encode: string;
+}
+
+export interface DontGenerateEncode {
+  dontGenerateEncode: string;
+}
+
+export const Encode: MessageFns<Encode> = {
+  encode(message: Encode, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+    if (message.encode !== "") {
+      writer.uint32(10).string(message.encode);
+    }
+    return writer;
+  },
+};
+
+export interface MessageFns<T> {
+  encode(message: T, writer?: BinaryWriter): BinaryWriter;
+}
diff --git a/integration/output-encode-include-types/parameters.txt b/integration/output-encode-include-types/parameters.txt
new file mode 100644
index 00000000..dfcbb45b
--- /dev/null
+++ b/integration/output-encode-include-types/parameters.txt
@@ -0,0 +1,1 @@
+outputEncodeMethods=encode-only,outputPartialMethods=false,outputJsonMethods=false,nestJs=false,outputEncodeIncludeTypes=^Encode$
diff --git a/integration/output-encode-only/encode.ts b/integration/output-encode-only/encode.ts
index 76c271b1..c5127560 100644
--- a/integration/output-encode-only/encode.ts
+++ b/integration/output-encode-only/encode.ts
@@ -10,10 +10,6 @@ export interface Encode {
   encode: string;
 }
 
-function createBaseEncode(): Encode {
-  return { encode: "" };
-}
-
 export const Encode: MessageFns<Encode> = {
   encode(message: Encode, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.encode !== "") {
diff --git a/integration/output-encode-only/google/protobuf/wrappers.ts b/integration/output-encode-only/google/protobuf/wrappers.ts
index 06356c05..033edf61 100644
--- a/integration/output-encode-only/google/protobuf/wrappers.ts
+++ b/integration/output-encode-only/google/protobuf/wrappers.ts
@@ -96,10 +96,6 @@ export interface BytesValue {
   value: Uint8Array;
 }
 
-function createBaseDoubleValue(): DoubleValue {
-  return { value: 0 };
-}
-
 export const DoubleValue: MessageFns<DoubleValue> = {
   encode(message: DoubleValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== 0) {
@@ -109,10 +105,6 @@ export const DoubleValue: MessageFns<DoubleValue> = {
   },
 };
 
-function createBaseFloatValue(): FloatValue {
-  return { value: 0 };
-}
-
 export const FloatValue: MessageFns<FloatValue> = {
   encode(message: FloatValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== 0) {
@@ -122,10 +114,6 @@ export const FloatValue: MessageFns<FloatValue> = {
   },
 };
 
-function createBaseInt64Value(): Int64Value {
-  return { value: 0 };
-}
-
 export const Int64Value: MessageFns<Int64Value> = {
   encode(message: Int64Value, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== 0) {
@@ -135,10 +123,6 @@ export const Int64Value: MessageFns<Int64Value> = {
   },
 };
 
-function createBaseUInt64Value(): UInt64Value {
-  return { value: 0 };
-}
-
 export const UInt64Value: MessageFns<UInt64Value> = {
   encode(message: UInt64Value, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== 0) {
@@ -148,10 +132,6 @@ export const UInt64Value: MessageFns<UInt64Value> = {
   },
 };
 
-function createBaseInt32Value(): Int32Value {
-  return { value: 0 };
-}
-
 export const Int32Value: MessageFns<Int32Value> = {
   encode(message: Int32Value, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== 0) {
@@ -161,10 +141,6 @@ export const Int32Value: MessageFns<Int32Value> = {
   },
 };
 
-function createBaseUInt32Value(): UInt32Value {
-  return { value: 0 };
-}
-
 export const UInt32Value: MessageFns<UInt32Value> = {
   encode(message: UInt32Value, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== 0) {
@@ -174,10 +150,6 @@ export const UInt32Value: MessageFns<UInt32Value> = {
   },
 };
 
-function createBaseBoolValue(): BoolValue {
-  return { value: false };
-}
-
 export const BoolValue: MessageFns<BoolValue> = {
   encode(message: BoolValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== false) {
@@ -187,10 +159,6 @@ export const BoolValue: MessageFns<BoolValue> = {
   },
 };
 
-function createBaseStringValue(): StringValue {
-  return { value: "" };
-}
-
 export const StringValue: MessageFns<StringValue> = {
   encode(message: StringValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value !== "") {
@@ -200,10 +168,6 @@ export const StringValue: MessageFns<StringValue> = {
   },
 };
 
-function createBaseBytesValue(): BytesValue {
-  return { value: new Uint8Array(0) };
-}
-
 export const BytesValue: MessageFns<BytesValue> = {
   encode(message: BytesValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
     if (message.value.length !== 0) {
diff --git a/src/main.ts b/src/main.ts
index 4fdc1215..35b133d2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -210,7 +210,11 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
 
         // Only decode, fromPartial, and wrap use the createBase method
         if (
-          (options.outputEncodeMethods && options.outputEncodeMethods !== "encode-no-creation") ||
+          (options.outputEncodeMethods &&
+            options.outputEncodeMethods !== "encode-no-creation" &&
+            options.outputEncodeMethods !== "encode-only" &&
+            (options.outputDecodeIncludeTypes === "" ||
+              new RegExp(options.outputDecodeIncludeTypes).test(fullTypeName))) ||
           options.outputPartialMethods ||
           outputWrapAndUnwrap
         ) {
@@ -238,20 +242,36 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
             options.outputEncodeMethods === "encode-only" ||
             options.outputEncodeMethods === "encode-no-creation"
           ) {
-            staticMembers.push(generateEncode(ctx, fullName, message));
-
-            if (options.outputExtensions && options.unknownFields && message.extensionRange.length) {
-              staticMembers.push(generateSetExtension(ctx, fullName));
+            if (
+              options.outputEncodeIncludeTypes === "" ||
+              new RegExp(options.outputEncodeIncludeTypes).test(fullTypeName)
+            ) {
+              staticMembers.push(generateEncode(ctx, fullName, message));
+
+              if (options.outputExtensions && options.unknownFields && message.extensionRange.length) {
+                staticMembers.push(generateSetExtension(ctx, fullName));
+              }
+            } else if (options.outputEncodeMethods === true) {
+              staticMembers.push(generateEmptyEncode(fullName));
             }
           }
-          if (options.outputEncodeMethods === true || options.outputEncodeMethods === "decode-only") {
-            staticMembers.push(generateDecode(ctx, fullName, message));
 
-            if (options.outputExtensions && options.unknownFields && message.extensionRange.length) {
-              staticMembers.push(generateGetExtension(ctx, fullName));
+          if (options.outputEncodeMethods === true || options.outputEncodeMethods === "decode-only") {
+            if (
+              options.outputDecodeIncludeTypes === "" ||
+              new RegExp(options.outputDecodeIncludeTypes).test(fullTypeName)
+            ) {
+              staticMembers.push(generateDecode(ctx, fullName, message));
+
+              if (options.outputExtensions && options.unknownFields && message.extensionRange.length) {
+                staticMembers.push(generateGetExtension(ctx, fullName));
+              }
+            } else if (options.outputEncodeMethods === true) {
+              staticMembers.push(generateEmptyDecode(fullName));
             }
           }
         }
+
         if (options.useAsyncIterable) {
           staticMembers.push(generateEncodeTransform(ctx.utils, fullName));
           staticMembers.push(generateDecodeTransform(ctx.utils, fullName));
@@ -1350,6 +1370,19 @@ function getDecodeReadSnippet(ctx: Context, field: FieldDescriptorProto) {
   return readSnippet;
 }
 
+function generateEmptyDecode(fullName: string): Code {
+  const BinaryReader = imp("BinaryReader@@bufbuild/protobuf/wire");
+
+  return code`
+    decode(
+      _: ${BinaryReader} | Uint8Array,
+      length?: number,
+    ): ${fullName} {
+      throw new Error('decode not generated for ${fullName}');
+    }
+  `;
+}
+
 /** Creates a function to decode a message by loop overing the tags. */
 function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorProto): Code {
   const { options, currentFile } = ctx;
@@ -1632,6 +1665,19 @@ function getEncodeWriteSnippet(ctx: Context, field: FieldDescriptorProto): (plac
   }
 }
 
+function generateEmptyEncode(fullName: string): Code {
+  const BinaryWriter = imp("BinaryWriter@@bufbuild/protobuf/wire");
+
+  return code`
+  encode(
+    _: ${fullName},
+    writer: ${BinaryWriter} = new ${BinaryWriter}(),
+  ): ${BinaryWriter} {
+    throw new Error('encode not generated for ${fullName}');
+  }
+  `;
+}
+
 /** Creates a function to encode a message by loop overing the tags. */
 function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorProto): Code {
   const { options, utils, typeMap, currentFile } = ctx;
diff --git a/src/options.ts b/src/options.ts
index fd687c7c..ac6bfb85 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -61,6 +61,8 @@ export type Options = {
   fileSuffix: string;
   importSuffix: string;
   outputEncodeMethods: true | false | "encode-only" | "decode-only" | "encode-no-creation";
+  outputEncodeIncludeTypes: string;
+  outputDecodeIncludeTypes: string;
   outputJsonMethods: true | false | "to-only" | "from-only";
   outputPartialMethods: boolean;
   outputTypeAnnotations: boolean | "static-only" | "optional";
@@ -132,6 +134,8 @@ export function defaultOptions(): Options {
     importSuffix: "",
     lowerCaseServiceMethods: false,
     outputEncodeMethods: true,
+    outputEncodeIncludeTypes: "",
+    outputDecodeIncludeTypes: "",
     outputJsonMethods: true,
     outputPartialMethods: true,
     outputTypeAnnotations: false,


Test patch:
diff --git a/tests/options-test.ts b/tests/options-test.ts
index e30b4a37..5eea15dd 100644
--- a/tests/options-test.ts
+++ b/tests/options-test.ts
@@ -32,6 +32,8 @@ describe("options", () => {
         "oneof": "properties",
         "onlyTypes": false,
         "outputClientImpl": false,
+        "outputDecodeIncludeTypes": "",
+        "outputEncodeIncludeTypes": "",
         "outputEncodeMethods": false,
         "outputExtensions": false,
         "outputIndex": false,