gapic-generator 0.51.1 → 0.53.0

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/lib/gapic/formatting_utils.rb +85 -18
  4. data/lib/gapic/generator/version.rb +1 -1
  5. data/lib/gapic/generators/default_generator.rb +6 -0
  6. data/lib/gapic/generators/default_generator_parameters.rb +2 -0
  7. data/lib/gapic/model/method/resumable_upload.rb +143 -0
  8. data/lib/gapic/presenters/gem_presenter.rb +1 -1
  9. data/lib/gapic/presenters/method_presenter.rb +35 -0
  10. data/lib/gapic/presenters/service_presenter.rb +65 -0
  11. data/lib/gapic/presenters/service_rest_presenter.rb +12 -0
  12. data/lib/gapic/presenters/snippet/response_handling_presenters.rb +60 -0
  13. data/lib/gapic/presenters/snippet_presenter.rb +16 -6
  14. data/templates/default/lib/_service.text.erb +3 -0
  15. data/templates/default/lib/rest/_rest.text.erb +3 -0
  16. data/templates/default/service/client/_client.text.erb +18 -0
  17. data/templates/default/service/client/_config.text.erb +8 -0
  18. data/templates/default/service/client/method/_def.text.erb +5 -1
  19. data/templates/default/service/client/method/def/_options_defaults.text.erb +5 -0
  20. data/templates/default/service/client/method/def/_response.text.erb +3 -1
  21. data/templates/default/service/client/method/def/_response_resumable_upload.text.erb +13 -0
  22. data/templates/default/service/client/method/def/_upload_error_handler.text.erb +2 -0
  23. data/templates/default/service/client/method/docs/_request_normal.text.erb +19 -0
  24. data/templates/default/service/client/method/docs/_response.text.erb +8 -0
  25. data/templates/default/service/rest/client/_client.text.erb +18 -0
  26. data/templates/default/service/rest/client/_config.text.erb +8 -0
  27. data/templates/default/service/rest/client/method/_def.text.erb +5 -1
  28. data/templates/default/service/rest/client/method/def/_response.text.erb +3 -1
  29. data/templates/default/service/rest/client/method/def/_response_resumable_upload.text.erb +13 -0
  30. data/templates/default/service/rest/client/method/def/_upload_error_handler.text.erb +2 -0
  31. data/templates/default/service/rest/client/method/docs/_request.text.erb +19 -0
  32. data/templates/default/service/rest/client/method/docs/_result.text.erb +7 -1
  33. data/templates/default/service/rest/service_stub/_service_stub.text.erb +2 -2
  34. data/templates/default/service/rest/service_stub/grpc_transcoding_method/_bindings.text.erb +15 -0
  35. data/templates/default/service/rest/service_stub/grpc_transcoding_method/_def.text.erb +1 -14
  36. data/templates/default/service/rest/test/client.text.erb +1 -1
  37. data/templates/default/service/resumable_upload_stub/_resumable_upload_stub.text.erb +64 -0
  38. data/templates/default/service/resumable_upload_stub/_transcoding_method.text.erb +22 -0
  39. data/templates/default/service/resumable_upload_stub.text.erb +6 -0
  40. data/templates/default/service/test/client.text.erb +1 -1
  41. data/templates/default/service/test/resumable_upload.text.erb +132 -0
  42. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eacc61ce38027f685621213ffaca3db69125b98c6a1d4d377984ee87d3b5d300
4
- data.tar.gz: 72ff91b581965488f97e8e521bb38847234d32c3ffb1a3b229e056f33129fa1c
3
+ metadata.gz: 3264cfe286069bf2e3bed64ae2246c0b8e48d97851809c99c5520e072fa38889
4
+ data.tar.gz: d9503be57f76aedd856a46159e138673fc83f2912fa51018d16e5cd1e8c71f90
5
5
  SHA512:
6
- metadata.gz: b8509223c3abf3617ef9bd7c6ea6aef239b57f8e60e774a33df471d131106ecb2a1942e6be9a374c2540753fa0d50ad91e2519ed5fa096ef391c946b35129c4f
7
- data.tar.gz: ff480bcbf75f4181647600ed686822911628f6cc98c3f048d91ba46af89cb31fb2db6faf7dfc3332dd2e30ce671574e8347d1149cd6cb25c874feb05788bc8af
6
+ metadata.gz: d8109c1036d6b95c2bf2fe201de7e2cf26516e4d315c6d4ca3331b52f0d312d27884416bd6ce7db8209ae78c13ffab4120b444e229fc914d3d329ca801dd6837
7
+ data.tar.gz: 1ef6ae68e1fc76dfc19bbe947d7f4340918403494d57bd3d1ba40540b953921470a5090b7ed7f6d2fe80519066c28d92e8a0a3115acbf4d498fc43c8580debd9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Release History for gapic-generator
2
2
 
3
+ ### 0.53.0 / 2026-09-30
4
+
5
+ * Feature: update gapic-common for generated libraries to 1.4
6
+ * Feature: resumable upload support in gapic-generator
7
+
8
+ ### 0.52.0 / 2026-09-04
9
+
10
+ * Fix: escape multi-line braces and backtick unknown doc tags in yard formatting
11
+ * Fix: rejoin split doc URLs (b/153077040) and strip non-existent message links (b/158466893)
12
+ * Feature: add ruby-cloud-renamed-from for renamed wrapper gems
13
+
3
14
  ### 0.51.1 / 2026-08-05
4
15
 
5
16
  No significant changes
@@ -21,16 +21,30 @@ module Gapic
21
21
  # Various string formatting utils
22
22
  #
23
23
  module FormattingUtils
24
- @brace_detector = /\A(?<pre>[^`]*(?:`[^`]*`[^`]*)*[^`\\])?\{(?<inside>[^\s][^}]*)\}(?<post>.*)\z/m
25
24
  @xref_detector = /\A(?<pre>[^`]*(?:`[^`]*`[^`]*)*)?\[(?<text>[\w. `-]+)\]\[(?<addr>[\w.]+)\](?<post>.*)\z/m
26
25
  @list_element_detector = /\A\s*(?:\*|\+|-|[0-9a-zA-Z]+\.)\s/
27
26
  @omit_lines = ["@InputOnly\n", "@OutputOnly\n"]
27
+ # Built-in YARD meta-data tags as documented in:
28
+ # https://rubydoc.info/gems/yard/file/docs/Tags.md#Tag_List
29
+ @known_yard_tags = [
30
+ "abstract", "api", "attr", "attr_reader", "attr_writer", "author", "deprecated", "example",
31
+ "note", "option", "overload", "param", "private", "raise", "return", "see", "since", "todo",
32
+ "version", "yield", "yieldparam", "yieldreturn"
33
+ ].freeze
34
+
35
+ # Non-existent messages referenced in proto documentation comments (b/158466893).
36
+ # Cross-references to these messages (or their fields) cannot be resolved, so the links
37
+ # are stripped to avoid broken documentation references.
38
+ @non_existent_messages = [
39
+ "google.cloud.automl.v1.ColumnSpec"
40
+ ].freeze
28
41
 
29
42
  class << self
30
43
  ##
31
44
  # Given an enumerable of lines, performs yardoc formatting, including:
32
45
  # * Interpreting cross-references identified as described in AIP 192
33
46
  # * Escaping literal braces that look like yardoc type links
47
+ # * Backticking unknown doc tags so they are not parsed as YARD tags
34
48
  #
35
49
  # Tries to be smart about exempting preformatted text blocks.
36
50
  #
@@ -45,23 +59,26 @@ module Gapic
45
59
  #
46
60
  def format_doc_lines api, lines, disable_xrefs: false, transport: nil
47
61
  transport ||= api&.default_transport || :grpc
48
- # To detect preformatted blocks, this tracks the "expected" base indent
49
- # according to Markdown. Specifically, this is the effective indent of
50
- # previous block, which is normally 0 except if we're in a list item.
51
- # Then, if a block is indented at least 4 spaces past that expected
52
- # indent (and as long as it remains so), those lines are considered
53
- # preformatted.
62
+ lines = rejoin_split_urls lines
63
+ in_fence = in_code_span = false
54
64
  in_block = nil
55
65
  base_indent = 0
56
66
  (lines - @omit_lines).map do |line|
57
- indent = line_indent line
58
- if indent.nil?
67
+ if line =~ /^\s*(?:```|~~~)/
68
+ in_fence = !in_fence
69
+ in_code_span = false
59
70
  in_block = nil
60
- else
61
- in_block, base_indent = update_indent_state in_block, base_indent, line, indent
62
- if in_block == false
63
- line = escape_line_braces line
64
- line = format_line_xrefs api, line, disable_xrefs, transport
71
+ elsif !in_fence
72
+ indent = line_indent line
73
+ if indent.nil?
74
+ in_block = nil
75
+ in_code_span = false
76
+ else
77
+ in_block, base_indent = update_indent_state in_block, base_indent, line, indent
78
+ if in_block == false
79
+ line, in_code_span = format_line_content line, in_code_span
80
+ line = format_line_xrefs api, line, disable_xrefs, transport
81
+ end
65
82
  end
66
83
  end
67
84
  line
@@ -88,6 +105,21 @@ module Gapic
88
105
 
89
106
  private
90
107
 
108
+ def rejoin_split_urls lines
109
+ return lines if lines.empty?
110
+
111
+ # Fix for misformatted markdown links across line breaks (b/153077040).
112
+ # Callers may pass lines with trailing newlines (e.g., from String#each_line in schema wrappers)
113
+ # or without trailing newlines (e.g., from String#split("\n") in GemPresenter#readme_description).
114
+ # We must preserve the presence or absence of trailing newlines on each element.
115
+ has_newlines = lines.any? { |l| l.end_with? "\n" }
116
+ if has_newlines
117
+ lines.join.gsub(%r{https:\n\s*//}, "https://").each_line.to_a
118
+ else
119
+ lines.join("\n").gsub(%r{https:\n\s*//}, "https://").split("\n", -1)
120
+ end
121
+ end
122
+
91
123
  def update_indent_state in_block, base_indent, line, indent
92
124
  if in_block != true && @list_element_detector =~ line
93
125
  in_block = false
@@ -106,15 +138,50 @@ module Gapic
106
138
  m[1].length
107
139
  end
108
140
 
109
- def escape_line_braces line
110
- while (m = @brace_detector.match line)
111
- line = "#{m[:pre]}\\\\{#{m[:inside]}}#{m[:post]}"
141
+ def format_line_content line, in_code_span
142
+ parts = line.split("`", -1)
143
+ formatted_parts = parts.each_with_index.map do |part, idx|
144
+ if in_code_span
145
+ in_code_span = false if idx < parts.length - 1
146
+ part
147
+ else
148
+ is_followed_by_backtick = idx < parts.length - 1
149
+ in_code_span = true if is_followed_by_backtick
150
+ formatted = escape_prose_braces part, is_followed_by_backtick: is_followed_by_backtick
151
+ sanitize_prose_tags formatted
152
+ end
153
+ end
154
+ [formatted_parts.join("`"), in_code_span]
155
+ end
156
+
157
+ def escape_prose_braces text, is_followed_by_backtick: false
158
+ # Matches unescaped `{` outside backtick spans followed by non-whitespace.
159
+ # If `{` is at the end of a non-code chunk (is_followed_by_backtick: true), it is followed
160
+ # immediately by a backticked code span (starting with a non-whitespace backtick),
161
+ # so \z (end of string) is also matched.
162
+ pattern = is_followed_by_backtick ? /(?<!\\)\{(?=[^\s]|\z)/ : /(?<!\\)\{(?=[^\s])/
163
+ text.gsub(pattern) { "\\\\{" }
164
+ end
165
+
166
+ def sanitize_prose_tags text
167
+ # Matches doc tags starting with `@` at the start of a line or preceded by whitespace.
168
+ # Avoids matching `@` within email addresses (e.g. user@example.com) or quotes.
169
+ # Any tag not in the YARD recognized list (or starting with `!`) is wrapped in backticks
170
+ # so YARD renders it as literal text rather than an unrecognized tag directive.
171
+ text.gsub(/(?<=\A|\s)@([a-zA-Z_]\w*)/) do |match|
172
+ tag = Regexp.last_match 1
173
+ @known_yard_tags.include?(tag) || tag.start_with?("!") ? match : "`#{match}`"
112
174
  end
113
- line
114
175
  end
115
176
 
116
177
  def format_line_xrefs api, line, disable_xrefs, transport
117
178
  while (m = @xref_detector.match line)
179
+ # Remove links to known non-existent messages (b/158466893)
180
+ if @non_existent_messages.any? { |msg| m[:addr] == msg || m[:addr].start_with?("#{msg}.") }
181
+ line = "#{m[:pre]}#{m[:text]}#{m[:post]}"
182
+ next
183
+ end
184
+
118
185
  entity = api.lookup m[:addr]
119
186
  is_mixin_field_addr = Gapic::Model::Mixins.mixin_message_field_address?(
120
187
  m[:addr],
@@ -16,6 +16,6 @@
16
16
 
17
17
  module Gapic
18
18
  module Generator
19
- VERSION = "0.51.1"
19
+ VERSION = "0.53.0"
20
20
  end
21
21
  end
@@ -92,10 +92,16 @@ module Gapic
92
92
  # Rest-only `service.stub` file
93
93
  files << g("service/rest/service_stub", "lib/#{service.rest.service_stub_file_path}", service: service) if should_generate_rest
94
94
 
95
+ # Resumable upload stub, shared by both transports because uploads always travel over REST
96
+ files << g("service/resumable_upload_stub", "lib/#{service.resumable_upload_stub_file_path}", service: service) if service.resumable_upload?
97
+
95
98
  # Unit tests for `client.rb`
96
99
  files << g("service/test/client", "test/#{service.test_client_file_path}", service: service) if should_generate_grpc
97
100
  files << g("service/rest/test/client", "test/#{service.rest.test_client_file_path}", service: service) if should_generate_rest
98
101
 
102
+ # Unit tests for resumable upload RPCs, which the client tests above skip
103
+ files << g("service/test/resumable_upload", "test/#{service.test_resumable_upload_file_path}", service: service) if service.resumable_upload?
104
+
99
105
  # Unit tests for `paths.rb`
100
106
  files << g("service/test/client_paths", "test/#{service.test_paths_file_path}", service: service) if service.paths? && should_generate_grpc
101
107
 
@@ -40,6 +40,7 @@ module Gapic
40
40
  ":gem.:homepage",
41
41
  ":gem.:env_prefix",
42
42
  ":gem.:version_dependencies",
43
+ ":gem.:renamed_from",
43
44
  ":gem.:migration_version",
44
45
  ":gem.:product_documentation_url",
45
46
  ":gem.:issue_tracker_url",
@@ -83,6 +84,7 @@ module Gapic
83
84
  "gem-homepage" => ":gem.:homepage",
84
85
  "gem-env-prefix" => ":gem.:env_prefix",
85
86
  "gem-wrapper-of" => ":gem.:version_dependencies",
87
+ "gem-renamed-from" => ":gem.:renamed_from",
86
88
  "gem-migration-version" => ":gem.:migration_version",
87
89
  "gem-product-url" => ":gem.:product_documentation_url",
88
90
  "gem-issues-url" => ":gem.:issue_tracker_url",
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "gapic/model/model_error"
18
+
19
+ module Gapic
20
+ module Model
21
+ module Method
22
+ ##
23
+ # Resumable upload method-level model.
24
+ #
25
+ # A resumable upload RPC does not send its payload in the initiation request. The request it
26
+ # describes only creates an upload session; the bytes travel afterwards, over REST, in chunks
27
+ # addressed to a URL the server hands back. Generated clients therefore return an upload handle
28
+ # from such a method rather than a response message.
29
+ #
30
+ # Until the upload annotation is published, the set of such RPCs is carried here as a table, and
31
+ # so is the URL prefix each one's initiation request is sent under. When the annotation lands,
32
+ # {.url_prefix_for} keeps the table and detection moves to `http.media_upload.enabled`.
33
+ #
34
+ # @!attribute [r] url_prefix
35
+ # @return [String] The path prefix prepended to the transcoded initiation URL, without
36
+ # surrounding slashes, e.g. `resumable/upload`.
37
+ #
38
+ class ResumableUpload
39
+ # @return [String]
40
+ attr_reader :url_prefix
41
+
42
+ ##
43
+ # @param url_prefix [String] The upload URL prefix for the matched RPC.
44
+ #
45
+ def initialize url_prefix
46
+ @url_prefix = url_prefix
47
+ end
48
+
49
+ ##
50
+ # Exact matches, keyed by the full gRPC name of the RPC.
51
+ #
52
+ EXACT_PREFIXES = {
53
+ "google.showcase.v1beta1.ResumableUploadService.UploadMedia" => "resumable/upload"
54
+ }.freeze
55
+
56
+ ##
57
+ # Version-family matches, for protos that are republished under a new version regularly.
58
+ # Anchored on the left at the package and on the right at the service and method, with the
59
+ # intervening segments (e.g. `.services.`) unconstrained.
60
+ #
61
+ VERSIONED_PREFIXES = [
62
+ {
63
+ left: /\Agoogle\.ads\.googleads\.v[0-9_]+\./,
64
+ right: ".YouTubeVideoUploadService.CreateYouTubeVideoUpload",
65
+ prefix: "resumable/upload"
66
+ }
67
+ ].freeze
68
+
69
+ class << self
70
+ ##
71
+ # Inspects a method and returns its resumable upload model, or `nil` if it does not perform
72
+ # resumable uploads.
73
+ #
74
+ # @param method [Gapic::Presenters::MethodPresenter]
75
+ #
76
+ # @raise [Gapic::Model::ModelError] if the method is a resumable upload RPC that the
77
+ # generator cannot generate an upload surface for.
78
+ #
79
+ # @return [Gapic::Model::Method::ResumableUpload, nil]
80
+ #
81
+ def create method
82
+ prefix = url_prefix_for method.grpc_full_name
83
+ return nil if prefix.nil?
84
+ validate! method
85
+ new prefix
86
+ end
87
+
88
+ ##
89
+ # The upload URL prefix for an RPC, or `nil` if the RPC does not perform resumable uploads.
90
+ #
91
+ # @param full_name [String] The full gRPC name of the RPC,
92
+ # e.g. `google.showcase.v1beta1.ResumableUploadService.UploadMedia`.
93
+ #
94
+ # @return [String, nil]
95
+ #
96
+ def url_prefix_for full_name
97
+ EXACT_PREFIXES[full_name] ||
98
+ VERSIONED_PREFIXES.find do |match|
99
+ match[:left].match?(full_name) && full_name.end_with?(match[:right])
100
+ end&.fetch(:prefix)
101
+ end
102
+
103
+ ##
104
+ # Verifies that an upload surface can be generated for the given method. A resumable upload
105
+ # is a single unary POST that carries a body, and anything else in the table is a
106
+ # misconfiguration that must fail the build rather than generate code that cannot work.
107
+ #
108
+ # @param method [Gapic::Presenters::MethodPresenter]
109
+ #
110
+ # @raise [Gapic::Model::ModelError]
111
+ #
112
+ # @return [void]
113
+ #
114
+ def validate! method
115
+ reason = unsupported_reason method
116
+ return if reason.nil?
117
+ raise ModelError, "The method #{method.grpc_full_name} performs resumable uploads, " \
118
+ "which the generator supports only for #{reason}."
119
+ end
120
+
121
+ private
122
+
123
+ ##
124
+ # @param method [Gapic::Presenters::MethodPresenter]
125
+ # @return [String, nil] What the method would have had to be, or `nil` if it is supported.
126
+ #
127
+ def unsupported_reason method
128
+ return "non-streaming methods" if method.client_streaming? || method.server_streaming?
129
+ return "non-paginated methods" if method.paged?
130
+ return "methods that are not long-running operations" if method.lro? || method.nonstandard_lro?
131
+
132
+ binding = method.http_bindings.first
133
+ return "methods with an HTTP binding" if binding.nil?
134
+ return "methods bound to POST" unless binding.verb == :post
135
+ return "methods whose HTTP binding has a body" unless binding.body?
136
+
137
+ nil
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -260,7 +260,7 @@ module Gapic
260
260
 
261
261
  def dependencies
262
262
  @dependencies ||= begin
263
- deps = { "gapic-common" => "~> 1.3" }
263
+ deps = { "gapic-common" => "~> 1.5" }
264
264
  deps["grpc-google-iam-v1"] = "~> 1.11" if iam_dependency?
265
265
  extra_deps = gem_config_dependencies
266
266
  deps.merge! mixins_model.dependencies if mixins_model.mixins?
@@ -17,6 +17,7 @@
17
17
  require "active_support/inflector"
18
18
  require "gapic/ruby_info"
19
19
  require "gapic/helpers/namespace_helper"
20
+ require "gapic/model/method/resumable_upload"
20
21
 
21
22
  module Gapic
22
23
  module Presenters
@@ -71,6 +72,10 @@ module Gapic
71
72
  @lro = Gapic::Model::Method.parse_lro @method, @api
72
73
 
73
74
  @rest = MethodRestPresenter.new self, @api
75
+
76
+ # Built last: detection is cheap but its validation reads the LRO model, the HTTP bindings
77
+ # and the pagination check, all of which have to exist first.
78
+ @resumable_upload = Gapic::Model::Method::ResumableUpload.create self
74
79
  end
75
80
 
76
81
  ##
@@ -279,6 +284,36 @@ module Gapic
279
284
  service.nonstandard_lros.find { |model| model.service == @lro.service_full_name }
280
285
  end
281
286
 
287
+ ##
288
+ # Whether this method performs a resumable upload. Such a method returns an upload handle
289
+ # rather than a response, and its payload travels over REST in chunks after the request this
290
+ # method describes has created the upload session.
291
+ #
292
+ # @return [Boolean]
293
+ #
294
+ def resumable_upload?
295
+ !@resumable_upload.nil?
296
+ end
297
+
298
+ ##
299
+ # The path prefix prepended to this method's transcoded initiation URL, without surrounding
300
+ # slashes, e.g. `resumable/upload`. `nil` unless this method performs a resumable upload.
301
+ #
302
+ # @return [String, nil]
303
+ #
304
+ def upload_url_prefix
305
+ @resumable_upload&.url_prefix
306
+ end
307
+
308
+ ##
309
+ # The name of the constant the generated upload stub holds this method's URL prefix in.
310
+ #
311
+ # @return [String]
312
+ #
313
+ def upload_url_prefix_const_name
314
+ "#{name.upcase}_URL_PREFIX"
315
+ end
316
+
282
317
  def client_streaming?
283
318
  @method.client_streaming
284
319
  end
@@ -422,6 +422,15 @@ module Gapic
422
422
  service_file_path.sub ".rb", "_operations_test.rb"
423
423
  end
424
424
 
425
+ ##
426
+ # Path of the generated tests covering this service's resumable upload RPCs. Those RPCs are
427
+ # excluded from the ordinary client tests, which assume a call returns a response.
428
+ #
429
+ # @return [String]
430
+ def test_resumable_upload_file_path
431
+ service_file_path.sub ".rb", "_resumable_upload_test.rb"
432
+ end
433
+
425
434
  def stub_name
426
435
  "#{ActiveSupport::Inflector.underscore name}_stub"
427
436
  end
@@ -489,6 +498,62 @@ module Gapic
489
498
  ServicePresenter.new @gem_presenter, @api, lro.services.first, parent_service: self unless lro.nil?
490
499
  end
491
500
 
501
+ ##
502
+ # Whether any of this service's RPCs perform resumable uploads, and therefore whether an upload
503
+ # stub has to be generated for it and built by its clients.
504
+ #
505
+ # @return [Boolean]
506
+ def resumable_upload?
507
+ methods.any?(&:resumable_upload?)
508
+ end
509
+
510
+ ##
511
+ # Presenters for the RPCs of this service that perform resumable uploads.
512
+ #
513
+ # @return [Enumerable<Gapic::Presenters::MethodPresenter>]
514
+ def resumable_upload_methods
515
+ methods.select(&:resumable_upload?)
516
+ end
517
+
518
+ ##
519
+ # The class name of the generated upload stub. One per service, shared by both transports, and
520
+ # deliberately not nested under `Rest::`: the gRPC client builds it too, because the upload
521
+ # itself always travels over REST.
522
+ #
523
+ # @return [String]
524
+ def resumable_upload_stub_name
525
+ "ResumableUploadStub"
526
+ end
527
+
528
+ # @return [String]
529
+ def resumable_upload_stub_name_full
530
+ fix_namespace @api, "#{service_name_full}::#{resumable_upload_stub_name}"
531
+ end
532
+
533
+ # @return [String]
534
+ def resumable_upload_stub_require
535
+ ruby_file_path @api, resumable_upload_stub_name_full
536
+ end
537
+
538
+ # @return [String]
539
+ def resumable_upload_stub_file_path
540
+ "#{resumable_upload_stub_require}.rb"
541
+ end
542
+
543
+ # @return [String]
544
+ def resumable_upload_stub_file_name
545
+ resumable_upload_stub_file_path.split("/").last
546
+ end
547
+
548
+ ##
549
+ # An instance variable name used for the generated upload stub. The clients keep the stub here
550
+ # and expose no reader for it.
551
+ #
552
+ # @return [String]
553
+ def resumable_upload_stub_ivar
554
+ "@resumable_upload_stub"
555
+ end
556
+
492
557
  def config_channel_args
493
558
  { "grpc.service_config_disable_resolution" => 1 }
494
559
  end
@@ -357,6 +357,18 @@ module Gapic
357
357
  main_service.methods.select(&:can_generate_rest?)
358
358
  end
359
359
 
360
+ ##
361
+ # Presenters for methods that the REST service stub carries an implementation for. An upload
362
+ # RPC has no ordinary REST path: the REST client delegates to the upload handle exactly as the
363
+ # gRPC client does, so a plain call method and transcoder here would be dead code that also
364
+ # happens to be wrong — a non-resumable POST of the whole payload.
365
+ #
366
+ # @return [Enumerable<Gapic::Presenters::MethodPresenter>]
367
+ #
368
+ def service_stub_methods
369
+ methods.reject(&:resumable_upload?)
370
+ end
371
+
360
372
  ##
361
373
  # Require string for the helpers file
362
374
  #
@@ -80,6 +80,66 @@ module Gapic
80
80
  attr_reader :response_name
81
81
  end
82
82
 
83
+ ##
84
+ # Presentation information about resumable upload response handling.
85
+ #
86
+ # A resumable upload RPC returns a {::Gapic::ResumableUpload} handle rather than a
87
+ # response message, so the snippet names the call result `upload` and goes on to
88
+ # start the upload, which is what actually produces the response message.
89
+ #
90
+ class ResumableUploadResponseHandlingPresenter
91
+ include ResponseHandlingPresenterCommon
92
+
93
+ ##
94
+ # Create a resumable upload response handling presenter
95
+ #
96
+ # @param proto [Google::Cloud::Tools::SnippetGen::ConfigLanguage::V1::Snippet::SimpleResponseHandling]
97
+ # The protobuf representation
98
+ # @param json [String]
99
+ # The JSON representation
100
+ # @param response_type [String] The fully qualified response message class
101
+ # @param phase1 [Boolean] True if this is a phase 1 snippet without config
102
+ #
103
+ def initialize proto, _json, response_type:, phase1:
104
+ @response_name = phase1 ? "upload" : compute_response_name(proto, phase1)
105
+ @render_lines = phase1 ? upload_lines(response_type) : []
106
+ @render = @render_lines.join "\n"
107
+ end
108
+
109
+ ##
110
+ # The lines of rendered code
111
+ # @return [Array<String>]
112
+ #
113
+ attr_reader :render_lines
114
+
115
+ ##
116
+ # The rendered code as a single string, possibly with line breaks
117
+ # @return [String]
118
+ #
119
+ attr_reader :render
120
+
121
+ ##
122
+ # The name of the response variable, or nil for no response handling
123
+ # @return [String,nil]
124
+ #
125
+ attr_reader :response_name
126
+
127
+ private
128
+
129
+ def upload_lines response_type
130
+ [
131
+ "# The returned object is a handle for a resumable upload. Nothing has been",
132
+ "# uploaded yet, and the timeout and retry policy of the call above cover only",
133
+ "# the request that creates the upload session, not the upload as a whole.",
134
+ "stream = File.open \"input.bin\", \"rb\"",
135
+ "result = #{@response_name}.start stream: stream, content_type: \"application/octet-stream\"",
136
+ "",
137
+ "# The returned object is of type #{response_type}.",
138
+ "p result"
139
+ ]
140
+ end
141
+ end
142
+
83
143
  ##
84
144
  # Presentation information about LRO response handling
85
145
  #
@@ -63,6 +63,8 @@ module Gapic
63
63
  :paged
64
64
  elsif @method_presenter.lro?
65
65
  :lro
66
+ elsif @method_presenter.resumable_upload?
67
+ :resumable_upload
66
68
  else
67
69
  :simple
68
70
  end
@@ -244,13 +246,12 @@ module Gapic
244
246
  LroResponseHandlingPresenter.new call_proto&.lro_handling,
245
247
  call_json&.fetch("lroHandling", nil),
246
248
  phase1: phase1
249
+ when :resumable_upload
250
+ ResumableUploadResponseHandlingPresenter.new call_proto&.response_handling,
251
+ call_json&.fetch("responseHandling", nil),
252
+ response_type: return_type, phase1: phase1
247
253
  when :streaming
248
- response_name = phase1 ? "output" : call_proto&.server_stream_name
249
- response_name = nil if response_name == ""
250
- StreamingResponseHandlingPresenter.new call_proto&.response_handling,
251
- call_json&.fetch("responseHandling", nil),
252
- response_name: response_name, base_response_type: base_response_type,
253
- phase1: phase1
254
+ build_streaming_response_handling_presenter call_proto, call_json, phase1
254
255
  else
255
256
  SimpleResponseHandlingPresenter.new call_proto&.response_handling,
256
257
  call_json&.fetch("responseHandling", nil),
@@ -258,6 +259,15 @@ module Gapic
258
259
  end
259
260
  end
260
261
 
262
+ def build_streaming_response_handling_presenter call_proto, call_json, phase1
263
+ response_name = phase1 ? "output" : call_proto&.server_stream_name
264
+ response_name = nil if response_name == ""
265
+ StreamingResponseHandlingPresenter.new call_proto&.response_handling,
266
+ call_json&.fetch("responseHandling", nil),
267
+ response_name: response_name, base_response_type: base_response_type,
268
+ phase1: phase1
269
+ end
270
+
261
271
  def build_client_call_presenter call_proto, call_json, request_name, response_name
262
272
  phase1 = !config?
263
273
  if response_kind == :paged
@@ -15,6 +15,9 @@ require "<%= service.credentials_require %>"
15
15
  <%- if service.paths? -%>
16
16
  require "<%= service.paths_require %>"
17
17
  <%- end -%>
18
+ <%- if service.resumable_upload? -%>
19
+ require "<%= service.resumable_upload_stub_require %>"
20
+ <%- end -%>
18
21
  <%- if service.generate_grpc_clients? -%>
19
22
  <%- if service.lro? -%>
20
23
  require "<%= service.operations_require %>"
@@ -15,6 +15,9 @@ require "<%= service.credentials_require %>"
15
15
  <%- if service.paths? -%>
16
16
  require "<%= service.paths_require %>"
17
17
  <%- end -%>
18
+ <%- if service.resumable_upload? -%>
19
+ require "<%= service.resumable_upload_stub_require %>"
20
+ <%- end -%>
18
21
  <%- if service.rest.lro? -%>
19
22
  require "<%= service.rest.operations_require %>"
20
23
  <%- end -%>