From d7e9de953a3086d64dcf84cb40aad7b33107d48d Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:16:32 -0400 Subject: [PATCH 1/2] feat(generator): select resumable upload methods Adds the `resumable_upload_methods` generator parameter: a semicolon-separated list of `Service.Method` pairs. Selected methods are augmented with a resumable upload descriptor, removed from the simple methods list, and exposed on the service as `resumableUploads`, which the templates use to generate the resumable upload clients. Plumbs the parameter through the CLI, the bazel rule and the naming options, and refreshes the pubsub api dump baselines for the new `resumableUploads` field. --- .../pubsub-api-dump-esm/api.json.baseline | 3 + .../pubsub-api-dump/api.json.baseline | 3 + .../typescript_gapic.bzl | 3 + .../src/gapic-generator-typescript.ts | 14 +++ .../typescript/src/generator.ts | 12 +++ .../typescript/src/schema/api.ts | 2 + .../typescript/src/schema/naming.ts | 1 + .../typescript/src/schema/proto.ts | 48 +++++++++- .../typescript/test/unit/api.ts | 34 +++++++ .../typescript/test/unit/proto.ts | 93 +++++++++++++++++++ 10 files changed, 208 insertions(+), 5 deletions(-) diff --git a/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump-esm/api.json.baseline b/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump-esm/api.json.baseline index 6de2dfd0024e..c72641534d0a 100644 --- a/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump-esm/api.json.baseline +++ b/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump-esm/api.json.baseline @@ -5105,6 +5105,7 @@ ] } ], + "resumableUploads": [], "longRunning": [], "diregapicLRO": [], "streaming": [], @@ -10252,6 +10253,7 @@ ] } ], + "resumableUploads": [], "longRunning": [], "diregapicLRO": [], "streaming": [], @@ -16698,6 +16700,7 @@ ] } ], + "resumableUploads": [], "longRunning": [], "diregapicLRO": [], "streaming": [ diff --git a/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump/api.json.baseline b/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump/api.json.baseline index 6de2dfd0024e..c72641534d0a 100644 --- a/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump/api.json.baseline +++ b/core/generator/gapic-generator-typescript/baselines/pubsub-api-dump/api.json.baseline @@ -5105,6 +5105,7 @@ ] } ], + "resumableUploads": [], "longRunning": [], "diregapicLRO": [], "streaming": [], @@ -10252,6 +10253,7 @@ ] } ], + "resumableUploads": [], "longRunning": [], "diregapicLRO": [], "streaming": [], @@ -16698,6 +16700,7 @@ ] } ], + "resumableUploads": [], "longRunning": [], "diregapicLRO": [], "streaming": [ diff --git a/core/generator/gapic-generator-typescript/rules_typescript_gapic/typescript_gapic.bzl b/core/generator/gapic-generator-typescript/rules_typescript_gapic/typescript_gapic.bzl index b76a85508c26..0385513a47a3 100644 --- a/core/generator/gapic-generator-typescript/rules_typescript_gapic/typescript_gapic.bzl +++ b/core/generator/gapic-generator-typescript/rules_typescript_gapic/typescript_gapic.bzl @@ -30,6 +30,7 @@ def typescript_gapic_library( legacy_proto_load = None, rest_numeric_enums = None, mixins = None, + resumable_upload_methods = None, format = None, extra_protoc_parameters = [], extra_protoc_file_parameters = {}, @@ -56,6 +57,8 @@ def typescript_gapic_library( plugin_args_dict["rest-numeric-enums"] = "true" if mixins: plugin_args_dict["mixins"] = mixins + if resumable_upload_methods: + plugin_args_dict["resumable-upload-methods"] = resumable_upload_methods file_args = {} # note: keys are filenames, values are parameter name, aligned with the prior art for key, value in extra_protoc_file_parameters: diff --git a/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts b/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts index 86f94791dc57..4f59b199fe1c 100755 --- a/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts @@ -136,6 +136,12 @@ async function main(processArgv: string[]) { 'Override the list of mixins to use. Semicolon-separated list of API names to mixin, e.g. google.longrunning.Operations. Use "none" to disable all mixins.', ) .string('mixins') + .describe( + 'resumable_upload_methods', + 'Semicolon-separated list of ServiceName.MethodName pairs that should be treated as resumable upload methods, e.g. ResumableUploadService.CreateResumableUpload.', + ) + .string('resumable_upload_methods') + .alias('resumable_upload_methods', 'resumable-upload-methods') .describe('protoc', 'Path to protoc binary') .usage('Usage: $0 -I /path/to/googleapis') .usage(' --output_dir /path/to/output_directory') @@ -158,6 +164,9 @@ async function main(processArgv: string[]) { const legacyProtoLoad = argv.legacyProtoLoad as boolean | undefined; const restNumericEnums = argv.restNumericEnums as boolean | undefined; const mixins = argv.mixins as string | undefined; + const resumableUploadMethods = argv.resumableUploadMethods as + | string + | undefined; // --protoc can be taken from environment or from the command line let protocParameter = argv.protoc as string | string[] | undefined; @@ -247,6 +256,11 @@ async function main(processArgv: string[]) { if (mixins) { protocCommand.push(`--typescript_gapic_opt="mixins=${mixins}"`); } + if (resumableUploadMethods) { + protocCommand.push( + `--typescript_gapic_opt="resumable-upload-methods=${resumableUploadMethods}"`, + ); + } protocCommand.push(...protoDirsArg); protocCommand.push(...protoFiles); protocCommand.push(`-I${commonProtoPath}`); diff --git a/core/generator/gapic-generator-typescript/typescript/src/generator.ts b/core/generator/gapic-generator-typescript/typescript/src/generator.ts index b66849bed378..4f0c068d2a93 100644 --- a/core/generator/gapic-generator-typescript/typescript/src/generator.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/generator.ts @@ -85,6 +85,7 @@ export class Generator { legacyProtoLoad?: boolean; restNumericEnums?: boolean; mixinsOverride?: string[]; + resumableUploadMethods?: string[]; format?: string | string[]; enableTelemetryTracing?: boolean; @@ -252,6 +253,15 @@ export class Generator { } } + private readResumableUploadMethods() { + if (this.paramMap['resumable-upload-methods']) { + this.resumableUploadMethods = this.paramMap['resumable-upload-methods'] + .split(';') + .map(name => name.trim()) + .filter(name => name.length > 0); + } + } + async initializeFromStdin() { const inputBuffer = await getStdin(); const CodeGeneratorRequest = this.root.lookupType('CodeGeneratorRequest'); @@ -282,6 +292,7 @@ export class Generator { this.readRestNumericEnums(); this.readFormat(); this.readEnableTelemetryTracing(); + this.readResumableUploadMethods(); } } @@ -343,6 +354,7 @@ export class Generator { restNumericEnums: this.restNumericEnums, mixinsOverridden: this.mixinsOverride !== undefined, enableTelemetryTracing: this.enableTelemetryTracing, + resumableUploadMethods: this.resumableUploadMethods, }); return api; } diff --git a/core/generator/gapic-generator-typescript/typescript/src/schema/api.ts b/core/generator/gapic-generator-typescript/typescript/src/schema/api.ts index 2e8c02aadc69..e424cc2c36f5 100644 --- a/core/generator/gapic-generator-typescript/typescript/src/schema/api.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/schema/api.ts @@ -43,6 +43,7 @@ export class API { handwrittenLayer?: boolean; legacyProtoLoad: boolean; restNumericEnums: boolean; + resumableUploadMethods: string[]; documentationUri: any; newIssueUri: string; enableTelemetryTracing?: boolean; @@ -110,6 +111,7 @@ export class API { this.diregapic = options.diregapic ?? false; this.legacyProtoLoad = options.legacyProtoLoad ?? false; this.restNumericEnums = options.restNumericEnums ?? false; + this.resumableUploadMethods = options.resumableUploadMethods ?? []; this.documentationUri = options.serviceYaml?.publishing?.documentation_uri ?? ''; this.newIssueUri = options.serviceYaml?.publishing?.new_issue_uri ?? ''; diff --git a/core/generator/gapic-generator-typescript/typescript/src/schema/naming.ts b/core/generator/gapic-generator-typescript/typescript/src/schema/naming.ts index 49c90c9f1b63..06cc8a18c473 100644 --- a/core/generator/gapic-generator-typescript/typescript/src/schema/naming.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/schema/naming.ts @@ -31,6 +31,7 @@ export interface Options { restNumericEnums?: boolean; mixinsOverridden?: boolean; enableTelemetryTracing?: boolean; + resumableUploadMethods?: string[]; } export class Naming { diff --git a/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts b/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts index 60c6673b67bd..04a89800c73f 100644 --- a/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts @@ -79,6 +79,9 @@ export interface MethodDescriptorProto bundleConfig?: BundleConfig; toJSON: Function | undefined; isDiregapicLRO?: boolean; + // If set, the method opts into the resumable upload protocol + // via the resumable_upload_methods generator parameter. + resumableUpload?: {uploadPrefix: string} | undefined; // if wrappers are allowed and there is a maxResultsParamter, return true maxResultsParameter?: boolean; } @@ -138,6 +141,7 @@ export interface ServiceDescriptorProto longRunningOperationsMixinFlags?: OperationsMixinConfig; protoFile: string; diregapicLRO?: MethodDescriptorProto[]; + resumableUploads: MethodDescriptorProto[]; httpRules?: protos.google.api.IHttpRule[]; selectiveGapic: SelectiveGapicConfig; } @@ -303,6 +307,24 @@ function streaming(method: MethodDescriptorProto) { return undefined; } +// Methods selected with the resumable_upload_methods generator parameter use +// the default upload prefix, since the parameter only identifies methods. +const DEFAULT_RESUMABLE_UPLOAD_PREFIX = '/resumable/upload'; + +function resumableUploadMethodNames( + serviceName: string, + resumableUploadMethods: string[] | undefined, +): Set { + const servicePrefix = `${serviceName}.`; + const methodNames = new Set(); + for (const methodName of resumableUploadMethods ?? []) { + if (methodName.startsWith(servicePrefix)) { + methodNames.add(methodName.substring(servicePrefix.length)); + } + } + return methodNames; +} + // returns true if the method has wrappers for UInt32Value enabled // and is a paginated call with a maxResults parameter instead of pageSize // as of its creation, this should only be true for BigQuery @@ -1031,9 +1053,13 @@ export function augmentService(parameters: AugmentServiceParameters) { augmentedService.bundleConfigs = parameters.options.bundleConfigs?.filter( bc => bc.serviceName === parameters.service.name, ); + const resumableUploadMethods = resumableUploadMethodNames( + parameters.service.name!, + parameters.options.resumableUploadMethods, + ); augmentedService.method = - augmentedService.method?.map(method => - augmentMethod( + augmentedService.method?.map(method => { + const augmentedMethod = augmentMethod( { allMessages: parameters.allMessages, localMessages: parameters.localMessages, @@ -1041,8 +1067,14 @@ export function augmentService(parameters: AugmentServiceParameters) { diregapic: parameters.options.diregapic, }, method, - ), - ) ?? []; + ); + if (resumableUploadMethods.has(augmentedMethod.name!)) { + augmentedMethod.resumableUpload = { + uploadPrefix: DEFAULT_RESUMABLE_UPLOAD_PREFIX, + }; + } + return augmentedMethod; + }) ?? []; /* Selective GAPIC method handling. */ augmentedService.method = augmentedService.method.filter( @@ -1076,7 +1108,13 @@ export function augmentService(parameters: AugmentServiceParameters) { ); augmentedService.simpleMethods = augmentedService.method.filter( method => - !method.longRunning && !method.streaming && !method.pagingFieldName, + !method.longRunning && + !method.streaming && + !method.pagingFieldName && + !method.resumableUpload, + ); + augmentedService.resumableUploads = augmentedService.method.filter( + method => method.resumableUpload, ); augmentedService.longRunning = augmentedService.method.filter( method => method.longRunning, diff --git a/core/generator/gapic-generator-typescript/typescript/test/unit/api.ts b/core/generator/gapic-generator-typescript/typescript/test/unit/api.ts index 0fddfb7f3e71..e0d67fe1e97a 100644 --- a/core/generator/gapic-generator-typescript/typescript/test/unit/api.ts +++ b/core/generator/gapic-generator-typescript/typescript/test/unit/api.ts @@ -36,6 +36,40 @@ describe('src/schema/api.ts', () => { ]); }); + it('should expose resumable upload methods passed as options', () => { + const fd = {} as protos.google.protobuf.FileDescriptorProto; + fd.name = 'google/cloud/test/v1/test.proto'; + fd.package = 'google.cloud.test.v1'; + fd.service = [{} as protos.google.protobuf.ServiceDescriptorProto]; + fd.service[0].name = 'ZService'; + fd.service[0].options = { + '.google.api.defaultHost': 'hostname.example.com:443', + }; + const api = new API([fd], 'google.cloud.test.v1', { + grpcServiceConfig: {} as protos.grpc.service_config.ServiceConfig, + resumableUploadMethods: ['ZService.Upload', 'ZService.Resume'], + }); + assert.deepStrictEqual(api.resumableUploadMethods, [ + 'ZService.Upload', + 'ZService.Resume', + ]); + }); + + it('should default resumable upload methods to an empty array', () => { + const fd = {} as protos.google.protobuf.FileDescriptorProto; + fd.name = 'google/cloud/test/v1/test.proto'; + fd.package = 'google.cloud.test.v1'; + fd.service = [{} as protos.google.protobuf.ServiceDescriptorProto]; + fd.service[0].name = 'ZService'; + fd.service[0].options = { + '.google.api.defaultHost': 'hostname.example.com:443', + }; + const api = new API([fd], 'google.cloud.test.v1', { + grpcServiceConfig: {} as protos.grpc.service_config.ServiceConfig, + }); + assert.deepStrictEqual(api.resumableUploadMethods, []); + }); + it('should correctly derive a valid logging name', () => { const fd = {} as protos.google.protobuf.FileDescriptorProto; fd.name = 'google/cloud/test/v1/test.proto'; diff --git a/core/generator/gapic-generator-typescript/typescript/test/unit/proto.ts b/core/generator/gapic-generator-typescript/typescript/test/unit/proto.ts index fd1427cbe458..ab17a984aae0 100644 --- a/core/generator/gapic-generator-typescript/typescript/test/unit/proto.ts +++ b/core/generator/gapic-generator-typescript/typescript/test/unit/proto.ts @@ -1794,3 +1794,96 @@ describe('src/schema/proto.ts', () => { }); }); }); + +describe('src/schema/proto.ts - resumable upload methods', () => { + function augmentResumableTestService(resumableUploadMethods?: string[]) { + const fd = {} as protos.google.protobuf.FileDescriptorProto; + fd.package = 'google.samples.resumable.v1'; + fd.service = [{} as protos.google.protobuf.ServiceDescriptorProto]; + fd.service[0].name = 'ResumableUploadService'; + fd.service[0].method = [ + { + name: 'CreateResumableUpload', + inputType: '.google.samples.resumable.v1.CreateResumableUploadRequest', + outputType: + '.google.samples.resumable.v1.CreateResumableUploadResponse', + }, + { + name: 'GetUploadStatus', + inputType: '.google.samples.resumable.v1.GetUploadStatusRequest', + outputType: '.google.samples.resumable.v1.GetUploadStatusResponse', + }, + ] as protos.google.protobuf.MethodDescriptorProto[]; + + const options: Options = { + grpcServiceConfig: {} as protos.grpc.service_config.ServiceConfig, + resumableUploadMethods, + }; + return augmentService({ + allMessages: {}, + localMessages: {}, + packageName: 'google.samples.resumable.v1', + service: fd.service[0], + commentsMap: new CommentsMap([fd]), + allResourceDatabase: new ResourceDatabase(), + resourceDatabase: new ResourceDatabase(), + options, + protoFile: 'fd', + }); + } + + it('marks methods listed in resumableUploadMethods', () => { + const augmentedService = augmentResumableTestService([ + 'OtherService.DoNotEnable', + 'ResumableUploadService.CreateResumableUpload', + ]); + + assert.strictEqual(augmentedService.resumableUploads.length, 1); + assert.deepStrictEqual( + augmentedService.resumableUploads[0].resumableUpload, + {uploadPrefix: '/resumable/upload'}, + ); + assert.strictEqual( + augmentedService.method.find(m => m.name === 'GetUploadStatus') + ?.resumableUpload, + undefined, + ); + assert.strictEqual( + augmentedService.simpleMethods.some( + m => m.name === 'CreateResumableUpload', + ), + false, + ); + assert.strictEqual( + augmentedService.simpleMethods.some(m => m.name === 'GetUploadStatus'), + true, + ); + }); + + it('does not enable methods that are not listed for the service', () => { + const augmentedService = augmentResumableTestService([ + 'ResumableUploadService.CreateResumableUpload', + ]); + + assert.strictEqual( + augmentedService.method.find(m => m.name === 'GetUploadStatus') + ?.resumableUpload, + undefined, + ); + assert.strictEqual( + augmentedService.simpleMethods.some(m => m.name === 'GetUploadStatus'), + true, + ); + }); + + it('does not enable resumable uploads when the option is omitted', () => { + const augmentedService = augmentResumableTestService(); + + assert.strictEqual(augmentedService.resumableUploads.length, 0); + assert.strictEqual( + augmentedService.method.find(m => m.name === 'CreateResumableUpload') + ?.resumableUpload, + undefined, + ); + }); +}); From 6a80fd076d5b744d03d157eeeacf8674141adadd Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:37:29 -0400 Subject: [PATCH 2/2] fix(generator): format resumable upload parameter code The monorepo linter's prettier disagrees with two spots touched by this change: - the resumableUploadMethods cast in gapic-generator-typescript.ts - the streaming union in schema/proto.ts, which is pre-existing formatting from main that the newer prettier prints on one line Pure formatting; no behavior change. --- .../typescript/src/gapic-generator-typescript.ts | 3 +-- .../typescript/src/schema/proto.ts | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts b/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts index 4f59b199fe1c..133a2d47cf79 100755 --- a/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/gapic-generator-typescript.ts @@ -165,8 +165,7 @@ async function main(processArgv: string[]) { const restNumericEnums = argv.restNumericEnums as boolean | undefined; const mixins = argv.mixins as string | undefined; const resumableUploadMethods = argv.resumableUploadMethods as - | string - | undefined; + string | undefined; // --protoc can be taken from environment or from the command line let protocParameter = argv.protoc as string | string[] | undefined; diff --git a/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts b/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts index 04a89800c73f..5f3f4eed1272 100644 --- a/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts +++ b/core/generator/gapic-generator-typescript/typescript/src/schema/proto.ts @@ -48,10 +48,7 @@ export interface MethodDescriptorProto longRunningResponseType?: string; longRunningMetadataType?: string; streaming: - | 'CLIENT_STREAMING' - | 'SERVER_STREAMING' - | 'BIDI_STREAMING' - | undefined; + 'CLIENT_STREAMING' | 'SERVER_STREAMING' | 'BIDI_STREAMING' | undefined; pagingFieldName: string | undefined; pagingResponseType?: string; pagingMapResponseType?: string;