Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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"
631 changes: 584 additions & 47 deletions README.md

Large diffs are not rendered by default.

9 changes: 7 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 All @@ -80,6 +81,10 @@ Style/GlobalVars:
Exclude:
- test/**/*

# We're ok with two compound comparisons before doing Array.include?
Style/MultipleComparison:
ComparisonsThreshold: 3

# We want our require lists to be in order
Style/RequireOrder:
Enabled: true
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
304 changes: 10 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 All @@ -379,6 +94,7 @@ Rake::Task[:build].enhance([:copy_parent_files]) do
end

task :rust_lint do
# TODO(cretz): Add "-- -Dwarnings" to clippy when SDK core passes with it
sh 'cargo', 'clippy'
sh 'cargo', 'fmt', '--check'
end
Expand Down
Loading
Loading