Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
build-lint-test:
strategy:
fail-fast: true
fail-fast: false
matrix:
# TODO(cretz): Enable Linux ARM. It's not natively supported with setup-ruby (see
# https://github.com/ruby/setup-ruby#supported-platforms and https://github.com/ruby/setup-ruby/issues/577).
Expand Down Expand Up @@ -73,4 +73,6 @@ jobs:

- name: Lint, compile, test Ruby
working-directory: ./temporalio
# Timeout just in case there's a hanging part in rake
timeout-minutes: 20
run: bundle exec rake TESTOPTS="--verbose"
627 changes: 581 additions & 46 deletions README.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions temporalio/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Layout/LeadingCommentSpace:
# Don't need super for activities
Lint/MissingSuper:
AllowedParentClasses:
- Temporalio::Activity
- Temporalio::Activity::Definition
- Temporalio::Workflow::Definition

# Allow tests to nest methods
Lint/NestedMethodDefinition:
Expand Down Expand Up @@ -61,7 +62,7 @@ Metrics/ModuleLength:

# The default is too small
Metrics/PerceivedComplexity:
Max: 25
Max: 40

# We want classes to be documented
Style/Documentation:
Expand Down
2 changes: 2 additions & 0 deletions temporalio/.yardopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--readme README.md
--protected
2 changes: 1 addition & 1 deletion temporalio/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ group :development do
gem 'rbs', '~> 3.5.3'
gem 'rb_sys', '~> 0.9.63'
gem 'rubocop'
gem 'sqlite3', '~> 1.4'
gem 'sqlite3'
gem 'steep', '~> 1.7.1'
gem 'yard'
end
303 changes: 9 additions & 294 deletions temporalio/Rakefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# rubocop:disable Metrics/BlockLength, Lint/MissingCopEnableDirective, Style/DocumentationMethod
# rubocop:disable Lint/MissingCopEnableDirective, Style/DocumentationMethod

require 'bundler/gem_tasks'
require 'rb_sys/cargo/metadata'
Expand Down Expand Up @@ -55,310 +55,25 @@ module CustomizeYardWarnings # rubocop:disable Style/Documentation
super
rescue YARD::Parser::UndocumentableError
# We ignore if it's an API warning
raise unless statement.last.file.start_with?('lib/temporalio/api/') ||
statement.last.file.start_with?('lib/temporalio/internal/bridge/api/')
last_file = statement.last.file
raise unless (last_file.start_with?('lib/temporalio/api/') && last_file.count('/') > 3) ||
last_file.start_with?('lib/temporalio/internal/bridge/api/')
end
end

YARD::Handlers::Ruby::ConstantHandler.prepend(CustomizeYardWarnings)

YARD::Rake::YardocTask.new { |t| t.options = ['--fail-on-warning'] }

require 'fileutils'
require 'google/protobuf'
Rake::Task[:yard].enhance([:copy_parent_files]) do
rm ['LICENSE', 'README.md']
end

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

def generate_protos(api_protos)
# Generate API to temp dir and move
FileUtils.rm_rf('tmp-proto')
FileUtils.mkdir_p('tmp-proto')
sh 'bundle exec grpc_tools_ruby_protoc ' \
'--proto_path=ext/sdk-core/sdk-core-protos/protos/api_upstream ' \
'--proto_path=ext/sdk-core/sdk-core-protos/protos/api_cloud_upstream ' \
'--proto_path=ext/additional_protos ' \
'--ruby_out=tmp-proto ' \
"#{api_protos.join(' ')}"

# Walk all generated Ruby files and cleanup content and filename
Dir.glob('tmp-proto/temporal/api/**/*.rb') do |path|
# Fix up the import
content = File.read(path)
content.gsub!(%r{^require 'temporal/(.*)_pb'$}, "require 'temporalio/\\1'")
File.write(path, content)

# Remove _pb from the filename
FileUtils.mv(path, path.sub('_pb', ''))
end

# Move from temp dir and remove temp dir
FileUtils.cp_r('tmp-proto/temporal/api', 'lib/temporalio')
FileUtils.rm_rf('tmp-proto')
end

# Generate from API with Google ones removed
generate_protos(Dir.glob('ext/sdk-core/sdk-core-protos/protos/api_upstream/**/*.proto').reject do |proto|
proto.include?('google')
end)

# Generate from Cloud API
generate_protos(Dir.glob('ext/sdk-core/sdk-core-protos/protos/api_cloud_upstream/**/*.proto'))

# Generate additional protos
generate_protos(Dir.glob('ext/additional_protos/**/*.proto'))

# Write files that will help with imports. We are requiring the
# request_response and not the service because the service depends on Google
# API annotations we don't want to have to depend on.
File.write(
'lib/temporalio/api/cloud/cloudservice.rb',
<<~TEXT
# frozen_string_literal: true

require 'temporalio/api/cloud/cloudservice/v1/request_response'
TEXT
)
File.write(
'lib/temporalio/api/workflowservice.rb',
<<~TEXT
# frozen_string_literal: true

require 'temporalio/api/workflowservice/v1/request_response'
TEXT
)
File.write(
'lib/temporalio/api/operatorservice.rb',
<<~TEXT
# frozen_string_literal: true

require 'temporalio/api/operatorservice/v1/request_response'
TEXT
)
File.write(
'lib/temporalio/api.rb',
<<~TEXT
# frozen_string_literal: true

require 'temporalio/api/cloud/cloudservice'
require 'temporalio/api/common/v1/grpc_status'
require 'temporalio/api/errordetails/v1/message'
require 'temporalio/api/operatorservice'
require 'temporalio/api/workflowservice'

module Temporalio
# Raw protocol buffer models.
module Api
end
end
TEXT
)

# Write the service classes that have the RPC calls
def write_service_file(qualified_service_name:, file_name:, class_name:, service_enum:)
# Do service lookup
desc = Google::Protobuf::DescriptorPool.generated_pool.lookup(qualified_service_name)
raise 'Failed finding service descriptor' unless desc

# Open file to generate Ruby code
File.open("lib/temporalio/client/connection/#{file_name}.rb", 'w') do |file|
file.puts <<~TEXT
# frozen_string_literal: true

# Generated code. DO NOT EDIT!

require 'temporalio/api'
require 'temporalio/client/connection/service'
require 'temporalio/internal/bridge/client'

module Temporalio
class Client
class Connection
# #{class_name} API.
class #{class_name} < Service
# @!visibility private
def initialize(connection)
super(connection, Internal::Bridge::Client::#{service_enum})
end
TEXT

desc.each do |method|
# Camel case to snake case
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
file.puts <<-TEXT

# Calls #{class_name}.#{method.name} API call.
#
# @param request [#{method.input_type.msgclass}] API request.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
# @return [#{method.output_type.msgclass}] API response.
def #{rpc}(request, rpc_options: nil)
invoke_rpc(
rpc: '#{rpc}',
request_class: #{method.input_type.msgclass},
response_class: #{method.output_type.msgclass},
request:,
rpc_options:
)
end
TEXT
end

file.puts <<~TEXT
end
end
end
end
TEXT
end

# Open file to generate RBS code
# TODO(cretz): Improve this when RBS proto is supported
File.open("sig/temporalio/client/connection/#{file_name}.rbs", 'w') do |file|
file.puts <<~TEXT
# Generated code. DO NOT EDIT!

module Temporalio
class Client
class Connection
class #{class_name} < Service
def initialize: (Connection) -> void
TEXT

desc.each do |method|
# Camel case to snake case
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
file.puts <<-TEXT
def #{rpc}: (
untyped request,
?rpc_options: RPCOptions?
) -> untyped
TEXT
end

file.puts <<~TEXT
end
end
end
end
TEXT
end
end

require './lib/temporalio/api/workflowservice/v1/service'
write_service_file(
qualified_service_name: 'temporal.api.workflowservice.v1.WorkflowService',
file_name: 'workflow_service',
class_name: 'WorkflowService',
service_enum: 'SERVICE_WORKFLOW'
)
require './lib/temporalio/api/operatorservice/v1/service'
write_service_file(
qualified_service_name: 'temporal.api.operatorservice.v1.OperatorService',
file_name: 'operator_service',
class_name: 'OperatorService',
service_enum: 'SERVICE_OPERATOR'
)
require './lib/temporalio/api/cloud/cloudservice/v1/service'
write_service_file(
qualified_service_name: 'temporal.api.cloud.cloudservice.v1.CloudService',
file_name: 'cloud_service',
class_name: 'CloudService',
service_enum: 'SERVICE_CLOUD'
)

# Generate Rust code
def generate_rust_match_arm(file:, qualified_service_name:, service_enum:, trait:)
# Do service lookup
desc = Google::Protobuf::DescriptorPool.generated_pool.lookup(qualified_service_name)
file.puts <<~TEXT
#{service_enum} => match call.rpc.as_str() {
TEXT

desc.to_a.sort_by(&:name).each do |method|
# Camel case to snake case
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
file.puts <<~TEXT
"#{rpc}" => rpc_call!(self, callback, call, #{trait}, #{rpc}),
TEXT
end
file.puts <<~TEXT
_ => Err(error!("Unknown RPC call {}", call.rpc)),
},
TEXT
end
File.open('ext/src/client_rpc_generated.rs', 'w') do |file|
file.puts <<~TEXT
// Generated code. DO NOT EDIT!

use magnus::{Error, Ruby};
use temporal_client::{CloudService, OperatorService, WorkflowService};

use super::{error, rpc_call};
use crate::{
client::{Client, RpcCall, SERVICE_CLOUD, SERVICE_OPERATOR, SERVICE_WORKFLOW},
util::AsyncCallback,
};

impl Client {
pub fn invoke_rpc(&self, service: u8, callback: AsyncCallback, call: RpcCall) -> Result<(), Error> {
match service {
TEXT
generate_rust_match_arm(
file:,
qualified_service_name: 'temporal.api.workflowservice.v1.WorkflowService',
service_enum: 'SERVICE_WORKFLOW',
trait: 'WorkflowService'
)
generate_rust_match_arm(
file:,
qualified_service_name: 'temporal.api.operatorservice.v1.OperatorService',
service_enum: 'SERVICE_OPERATOR',
trait: 'OperatorService'
)
generate_rust_match_arm(
file:,
qualified_service_name: 'temporal.api.cloud.cloudservice.v1.CloudService',
service_enum: 'SERVICE_CLOUD',
trait: 'CloudService'
)
file.puts <<~TEXT
_ => Err(error!("Unknown service")),
}
}
}
TEXT
end
sh 'cargo', 'fmt', '--', 'ext/src/client_rpc_generated.rs'

# Generate core protos
FileUtils.rm_rf('lib/temporalio/internal/bridge/api')
# Generate API to temp dir
FileUtils.rm_rf('tmp-proto')
FileUtils.mkdir_p('tmp-proto')
sh 'bundle exec grpc_tools_ruby_protoc ' \
'--proto_path=ext/sdk-core/sdk-core-protos/protos/api_upstream ' \
'--proto_path=ext/sdk-core/sdk-core-protos/protos/local ' \
'--ruby_out=tmp-proto ' \
"#{Dir.glob('ext/sdk-core/sdk-core-protos/protos/local/**/*.proto').join(' ')}"
# Walk all generated Ruby files and cleanup content and filename
Dir.glob('tmp-proto/temporal/sdk/**/*.rb') do |path|
# Fix up the imports
content = File.read(path)
content.gsub!(%r{^require 'temporal/(.*)_pb'$}, "require 'temporalio/\\1'")
content.gsub!(%r{^require 'temporalio/sdk/core/(.*)'$}, "require 'temporalio/internal/bridge/api/\\1'")
File.write(path, content)

# Remove _pb from the filename
FileUtils.mv(path, path.sub('_pb', ''))
end
# Move from temp dir and remove temp dir
FileUtils.mkdir_p('lib/temporalio/internal/bridge/api')
FileUtils.cp_r(Dir.glob('tmp-proto/temporal/sdk/core/*'), 'lib/temporalio/internal/bridge/api')
FileUtils.rm_rf('tmp-proto')
require_relative 'extra/proto_gen'
ProtoGen.new.run
end
end

Expand Down
Loading
Loading