Skip to content

Commit ff47b2e

Browse files
authored
More client features (#157)
1 parent 5d17d1d commit ff47b2e

File tree

112 files changed

+5691
-374
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+5691
-374
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,19 @@ jobs:
7171
working-directory: ./temporalio
7272
run: cargo clippy && cargo fmt --check
7373

74-
# TODO(cretz): For checkTarget, regen protos and ensure no diff
74+
- name: Install bundle
75+
working-directory: ./temporalio
76+
run: bundle install
77+
78+
- name: Check generated protos
79+
if: ${{ matrix.checkTarget }}
80+
working-directory: ./temporalio
81+
run: |
82+
bundle exec rake proto:generate
83+
[[ -z $(git status --porcelain lib/temporalio/api) ]] || (git diff lib/temporalio/api; echo "Protos changed" 1>&2; exit 1)
7584
7685
- name: Lint, compile, test Ruby
7786
working-directory: ./temporalio
78-
run: bundle install && bundle exec rake TESTOPTS="--verbose"
87+
run: bundle exec rake TESTOPTS="--verbose"
7988

8089
# TODO(cretz): Build gem and smoke test against separate dir

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ Prerequisites:
2121

2222
To build shared library for development use:
2323

24-
bundle exec rake compile:dev
24+
bundle exec rake compile
25+
26+
Note, this is not `compile:dev` because debug-mode in Rust has
27+
[an issue](https://github.com/rust-lang/rust/issues/34283) that causes runtime stack size problems.
2528

2629
To build and test release:
2730

temporalio/.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Metrics/BlockLength:
3434

3535
# The default is too small
3636
Metrics/ClassLength:
37-
Max: 400
37+
Max: 1000
3838

3939
# The default is too small
4040
Metrics/CyclomaticComplexity:

temporalio/Rakefile

Lines changed: 246 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
# rubocop:disable Metrics/BlockLength, Lint/MissingCopEnableDirective, Style/DocumentationMethod
4+
35
require 'bundler/gem_tasks'
46
require 'rb_sys/cargo/metadata'
57
require 'rb_sys/extensiontask'
@@ -34,57 +36,272 @@ require 'yard'
3436
YARD::Rake::YardocTask.new
3537

3638
require 'fileutils'
39+
require 'google/protobuf'
3740

3841
namespace :proto do
3942
desc 'Generate API and Core protobufs'
4043
task :generate do
4144
# Remove all existing
4245
FileUtils.rm_rf('lib/temporalio/api')
4346

44-
# Collect set of API protos with Google ones removed
45-
api_protos = Dir.glob('ext/sdk-core/sdk-core-protos/protos/api_upstream/**/*.proto').reject do |proto|
46-
proto.include?('google')
47-
end
47+
def generate_protos(api_protos)
48+
# Generate API to temp dir and move
49+
FileUtils.rm_rf('tmp-proto')
50+
FileUtils.mkdir_p('tmp-proto')
51+
sh 'bundle exec grpc_tools_ruby_protoc ' \
52+
'--proto_path=ext/sdk-core/sdk-core-protos/protos/api_upstream ' \
53+
'--proto_path=ext/sdk-core/sdk-core-protos/protos/api_cloud_upstream ' \
54+
'--proto_path=ext/additional_protos ' \
55+
'--ruby_out=tmp-proto ' \
56+
"#{api_protos.join(' ')}"
57+
58+
# Walk all generated Ruby files and cleanup content and filename
59+
Dir.glob('tmp-proto/temporal/api/**/*.rb') do |path|
60+
# Fix up the import
61+
content = File.read(path)
62+
content.gsub!(%r{^require 'temporal/(.*)_pb'$}, "require 'temporalio/\\1'")
63+
File.write(path, content)
64+
65+
# Remove _pb from the filename
66+
FileUtils.mv(path, path.sub('_pb', ''))
67+
end
4868

49-
# Generate API to temp dir and move
50-
FileUtils.rm_rf('tmp-proto')
51-
FileUtils.mkdir_p('tmp-proto')
52-
sh 'bundle exec grpc_tools_ruby_protoc ' \
53-
'--proto_path=ext/sdk-core/sdk-core-protos/protos/api_upstream ' \
54-
'--ruby_out=tmp-proto ' \
55-
"#{api_protos.join(' ')}"
56-
57-
# Walk all generated Ruby files and cleanup content and filename
58-
Dir.glob('tmp-proto/temporal/api/**/*.rb') do |path|
59-
# Fix up the import
60-
content = File.read(path)
61-
content.gsub!(%r{^require 'temporal/(.*)_pb'$}, "require 'temporalio/\\1'")
62-
File.write(path, content)
63-
64-
# Remove _pb from the filename
65-
FileUtils.mv(path, path.sub('_pb', ''))
69+
# Move from temp dir and remove temp dir
70+
FileUtils.cp_r('tmp-proto/temporal/api', 'lib/temporalio')
71+
FileUtils.rm_rf('tmp-proto')
6672
end
6773

68-
# Move from temp dir and remove temp dir
69-
FileUtils.mv('tmp-proto/temporal/api', 'lib/temporalio')
70-
FileUtils.rm_rf('tmp-proto')
74+
# Generate from API with Google ones removed
75+
generate_protos(Dir.glob('ext/sdk-core/sdk-core-protos/protos/api_upstream/**/*.proto').reject do |proto|
76+
proto.include?('google')
77+
end)
78+
79+
# Generate from Cloud API
80+
generate_protos(Dir.glob('ext/sdk-core/sdk-core-protos/protos/api_cloud_upstream/**/*.proto'))
81+
82+
# Generate additional protos
83+
generate_protos(Dir.glob('ext/additional_protos/**/*.proto'))
7184

7285
# Write files that will help with imports. We are requiring the
7386
# request_response and not the service because the service depends on Google
7487
# API annotations we don't want to have to depend on.
75-
string_lit = "# frozen_string_literal: true\n\n"
88+
File.write(
89+
'lib/temporalio/api/cloud/cloudservice.rb',
90+
<<~TEXT
91+
# frozen_string_literal: true
92+
93+
require 'temporalio/api/cloud/cloudservice/v1/request_response'
94+
TEXT
95+
)
7696
File.write(
7797
'lib/temporalio/api/workflowservice.rb',
78-
"#{string_lit}require 'temporalio/api/workflowservice/v1/request_response'\n"
98+
<<~TEXT
99+
# frozen_string_literal: true
100+
101+
require 'temporalio/api/workflowservice/v1/request_response'
102+
TEXT
79103
)
80104
File.write(
81105
'lib/temporalio/api/operatorservice.rb',
82-
"#{string_lit}require 'temporalio/api/operatorservice/v1/request_response'\n"
106+
<<~TEXT
107+
# frozen_string_literal: true
108+
109+
require 'temporalio/api/operatorservice/v1/request_response'
110+
TEXT
83111
)
84112
File.write(
85113
'lib/temporalio/api.rb',
86-
"#{string_lit}require 'temporalio/api/operatorservice'\nrequire 'temporalio/api/workflowservice'\n"
114+
<<~TEXT
115+
# frozen_string_literal: true
116+
117+
require 'temporalio/api/cloud/cloudservice'
118+
require 'temporalio/api/common/v1/grpc_status'
119+
require 'temporalio/api/errordetails/v1/message'
120+
require 'temporalio/api/operatorservice'
121+
require 'temporalio/api/workflowservice'
122+
123+
module Temporalio
124+
# Raw protocol buffer models.
125+
module Api
126+
end
127+
end
128+
TEXT
87129
)
130+
131+
# Write the service classes that have the RPC calls
132+
def write_service_file(qualified_service_name:, file_name:, class_name:, service_enum:)
133+
# Do service lookup
134+
desc = Google::Protobuf::DescriptorPool.generated_pool.lookup(qualified_service_name)
135+
raise 'Failed finding service descriptor' unless desc
136+
137+
# Open file to generate Ruby code
138+
File.open("lib/temporalio/client/connection/#{file_name}.rb", 'w') do |file|
139+
file.puts <<~TEXT
140+
# frozen_string_literal: true
141+
142+
# Generated code. DO NOT EDIT!
143+
144+
require 'temporalio/api'
145+
require 'temporalio/client/connection/service'
146+
require 'temporalio/internal/bridge/client'
147+
148+
module Temporalio
149+
class Client
150+
class Connection
151+
# #{class_name} API.
152+
class #{class_name} < Service
153+
# @!visibility private
154+
def initialize(connection)
155+
super(connection, Internal::Bridge::Client::#{service_enum})
156+
end
157+
TEXT
158+
159+
desc.each do |method|
160+
# Camel case to snake case
161+
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
162+
file.puts <<-TEXT
163+
164+
# Calls #{class_name}.#{method.name} API call.
165+
#
166+
# @param request [#{method.input_type.msgclass}] API request.
167+
# @param rpc_retry [Boolean] Whether to implicitly retry known retryable errors.
168+
# @param rpc_metadata [Hash<String, String>, nil] Headers to include on the RPC call.
169+
# @param rpc_timeout [Float, nil] Number of seconds before timeout.
170+
# @return [#{method.output_type.msgclass}] API response.
171+
def #{rpc}(request, rpc_retry: false, rpc_metadata: nil, rpc_timeout: nil)
172+
invoke_rpc(
173+
rpc: '#{rpc}',
174+
request_class: #{method.input_type.msgclass},
175+
response_class: #{method.output_type.msgclass},
176+
request:,
177+
rpc_retry:,
178+
rpc_metadata:,
179+
rpc_timeout:
180+
)
181+
end
182+
TEXT
183+
end
184+
185+
file.puts <<~TEXT
186+
end
187+
end
188+
end
189+
end
190+
TEXT
191+
end
192+
193+
# Open file to generate RBS code
194+
# TODO(cretz): Improve this when RBS proto is supported
195+
File.open("sig/temporalio/client/connection/#{file_name}.rbs", 'w') do |file|
196+
file.puts <<~TEXT
197+
# Generated code. DO NOT EDIT!
198+
199+
module Temporalio
200+
class Client
201+
class Connection
202+
class #{class_name} < Service
203+
def initialize: (Connection) -> void
204+
TEXT
205+
206+
desc.each do |method|
207+
# Camel case to snake case
208+
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
209+
file.puts <<-TEXT
210+
def #{rpc}: (untyped request, ?rpc_retry: bool, ?rpc_metadata: Hash[String, String]?, ?rpc_timeout: Float?) -> untyped
211+
TEXT
212+
end
213+
214+
file.puts <<~TEXT
215+
end
216+
end
217+
end
218+
end
219+
TEXT
220+
end
221+
end
222+
223+
require './lib/temporalio/api/workflowservice/v1/service'
224+
write_service_file(
225+
qualified_service_name: 'temporal.api.workflowservice.v1.WorkflowService',
226+
file_name: 'workflow_service',
227+
class_name: 'WorkflowService',
228+
service_enum: 'SERVICE_WORKFLOW'
229+
)
230+
require './lib/temporalio/api/operatorservice/v1/service'
231+
write_service_file(
232+
qualified_service_name: 'temporal.api.operatorservice.v1.OperatorService',
233+
file_name: 'operator_service',
234+
class_name: 'OperatorService',
235+
service_enum: 'SERVICE_OPERATOR'
236+
)
237+
require './lib/temporalio/api/cloud/cloudservice/v1/service'
238+
write_service_file(
239+
qualified_service_name: 'temporal.api.cloud.cloudservice.v1.CloudService',
240+
file_name: 'cloud_service',
241+
class_name: 'CloudService',
242+
service_enum: 'SERVICE_CLOUD'
243+
)
244+
245+
# Generate Rust code
246+
def generate_rust_match_arm(file:, qualified_service_name:, service_enum:, trait:)
247+
# Do service lookup
248+
desc = Google::Protobuf::DescriptorPool.generated_pool.lookup(qualified_service_name)
249+
file.puts <<~TEXT
250+
#{service_enum} => match call.rpc.as_str() {
251+
TEXT
252+
253+
desc.to_a.sort_by(&:name).each do |method|
254+
# Camel case to snake case
255+
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
256+
file.puts <<~TEXT
257+
"#{rpc}" => rpc_call!(self, block, call, #{trait}, #{rpc}),
258+
TEXT
259+
end
260+
file.puts <<~TEXT
261+
_ => Err(error!("Unknown RPC call {}", call.rpc)),
262+
},
263+
TEXT
264+
end
265+
File.open('ext/src/client_rpc_generated.rs', 'w') do |file|
266+
file.puts <<~TEXT
267+
// Generated code. DO NOT EDIT!
268+
269+
use magnus::{block::Proc, value::Opaque, Error, Ruby};
270+
use temporal_client::{CloudService, OperatorService, WorkflowService};
271+
272+
use super::{error, rpc_call};
273+
use crate::client::{Client, RpcCall, SERVICE_CLOUD, SERVICE_OPERATOR, SERVICE_WORKFLOW};
274+
275+
impl Client {
276+
pub fn invoke_rpc(&self, service: u8, block: Opaque<Proc>, call: RpcCall) -> Result<(), Error> {
277+
match service {
278+
TEXT
279+
generate_rust_match_arm(
280+
file:,
281+
qualified_service_name: 'temporal.api.workflowservice.v1.WorkflowService',
282+
service_enum: 'SERVICE_WORKFLOW',
283+
trait: 'WorkflowService'
284+
)
285+
generate_rust_match_arm(
286+
file:,
287+
qualified_service_name: 'temporal.api.operatorservice.v1.OperatorService',
288+
service_enum: 'SERVICE_OPERATOR',
289+
trait: 'OperatorService'
290+
)
291+
generate_rust_match_arm(
292+
file:,
293+
qualified_service_name: 'temporal.api.cloud.cloudservice.v1.CloudService',
294+
service_enum: 'SERVICE_CLOUD',
295+
trait: 'CloudService'
296+
)
297+
file.puts <<~TEXT
298+
_ => Err(error!("Unknown service")),
299+
}
300+
}
301+
}
302+
TEXT
303+
end
304+
sh 'cargo', 'fmt', '--', 'ext/src/client_rpc_generated.rs'
88305
end
89306
end
90307

@@ -104,4 +321,4 @@ Rake::Task[:build].enhance([:copy_parent_files]) do
104321
rm ['LICENSE', 'README.md']
105322
end
106323

107-
task default: [:rubocop, 'rbs:install_collection', :steep, :compile, :test]
324+
task default: ['rubocop', 'compile', 'rbs:install_collection', 'steep', 'test']

temporalio/Steepfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ target :lib do
1212
library 'uri'
1313

1414
configure_code_diagnostics do |hash|
15+
# TODO(cretz): Fix as more protos are generated
1516
hash[D::Ruby::UnknownConstant] = :information
16-
hash[D::Ruby::NoMethod] = :information
1717
end
1818
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
syntax = "proto3";
2+
3+
package temporal.api.common.v1;
4+
5+
option ruby_package = "Temporalio::Api::Common::V1";
6+
7+
import "google/protobuf/any.proto";
8+
9+
// From https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto
10+
// since we don't import grpc but still need the status info
11+
message GrpcStatus {
12+
int32 code = 1;
13+
string message = 2;
14+
repeated google.protobuf.Any details = 3;
15+
}

0 commit comments

Comments
 (0)