Skip to content

Commit e31565d

Browse files
authored
Advanced Sorbet generic sample (#27)
Fixes #3
1 parent ebaa2fa commit e31565d

35 files changed

Lines changed: 97328 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ jobs:
2929

3030
- name: Lint and test
3131
run: bundle exec rake TESTOPTS="--verbose"
32+
33+
- name: Type check Sorbet sample
34+
working-directory: sorbet_generic
35+
run: |
36+
bundle install
37+
bundle exec srb tc

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Gemfile.lock
1+
.bundle
2+
.vscode

Gemfile.lock

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
ast (2.4.2)
5+
bigdecimal (3.1.9)
6+
google-protobuf (4.29.3)
7+
bigdecimal
8+
rake (>= 13)
9+
google-protobuf (4.29.3-aarch64-linux)
10+
bigdecimal
11+
rake (>= 13)
12+
google-protobuf (4.29.3-arm64-darwin)
13+
bigdecimal
14+
rake (>= 13)
15+
google-protobuf (4.29.3-x86-linux)
16+
bigdecimal
17+
rake (>= 13)
18+
google-protobuf (4.29.3-x86_64-darwin)
19+
bigdecimal
20+
rake (>= 13)
21+
google-protobuf (4.29.3-x86_64-linux)
22+
bigdecimal
23+
rake (>= 13)
24+
json (2.9.1)
25+
language_server-protocol (3.17.0.3)
26+
minitest (5.25.4)
27+
parallel (1.26.3)
28+
parser (3.3.6.0)
29+
ast (~> 2.4.1)
30+
racc
31+
racc (1.8.1)
32+
rainbow (3.1.1)
33+
rake (13.2.1)
34+
regexp_parser (2.10.0)
35+
rubocop (1.70.0)
36+
json (~> 2.3)
37+
language_server-protocol (>= 3.17.0)
38+
parallel (~> 1.10)
39+
parser (>= 3.3.0.2)
40+
rainbow (>= 2.2.2, < 4.0)
41+
regexp_parser (>= 2.9.3, < 3.0)
42+
rubocop-ast (>= 1.36.2, < 2.0)
43+
ruby-progressbar (~> 1.7)
44+
unicode-display_width (>= 2.4.0, < 4.0)
45+
rubocop-ast (1.37.0)
46+
parser (>= 3.3.1.0)
47+
ruby-progressbar (1.13.0)
48+
temporalio (0.3.0)
49+
google-protobuf (>= 3.27.0)
50+
temporalio (0.3.0-aarch64-linux)
51+
google-protobuf (>= 3.27.0)
52+
temporalio (0.3.0-arm64-darwin)
53+
google-protobuf (>= 3.27.0)
54+
temporalio (0.3.0-x86_64-darwin)
55+
google-protobuf (>= 3.27.0)
56+
temporalio (0.3.0-x86_64-linux)
57+
google-protobuf (>= 3.27.0)
58+
unicode-display_width (3.1.4)
59+
unicode-emoji (~> 4.0, >= 4.0.4)
60+
unicode-emoji (4.0.4)
61+
62+
PLATFORMS
63+
aarch64-linux
64+
arm64-darwin
65+
ruby
66+
x86-linux
67+
x86_64-darwin
68+
x86_64-linux
69+
70+
DEPENDENCIES
71+
minitest
72+
rake
73+
rubocop
74+
temporalio
75+
76+
BUNDLED WITH
77+
2.5.17

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Prerequisites:
2424
* [context_propagation](context_propagation) - Use interceptors to propagate thread/fiber local data from clients
2525
through workflows/activities.
2626
* [message_passing_simple](message_passing_simple) - Simple workflow that accepts signals, queries, and updates.
27+
* [sorbet_generic](sorbet_generic) - Proof of concept of how to do _advanced_ Sorbet typing with the SDK.
2728

2829
## Development
2930

sorbet_generic/Gemfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
source 'https://rubygems.org'
4+
5+
# Temporal dependency
6+
gem 'temporalio'
7+
8+
# Sorbet dependencies
9+
gem 'sorbet', group: :development
10+
gem 'sorbet-runtime'
11+
gem 'tapioca', require: false, group: %i[development test]
12+
gem 'yard', group: :development

sorbet_generic/Gemfile.lock

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
benchmark (0.4.0)
5+
bigdecimal (3.1.9)
6+
erubi (1.13.1)
7+
google-protobuf (4.29.3-aarch64-linux)
8+
bigdecimal
9+
rake (>= 13)
10+
google-protobuf (4.29.3-arm64-darwin)
11+
bigdecimal
12+
rake (>= 13)
13+
google-protobuf (4.29.3-x86_64-darwin)
14+
bigdecimal
15+
rake (>= 13)
16+
google-protobuf (4.29.3-x86_64-linux)
17+
bigdecimal
18+
rake (>= 13)
19+
netrc (0.11.0)
20+
parallel (1.26.3)
21+
prism (1.3.0)
22+
rake (13.2.1)
23+
rbi (0.2.4)
24+
prism (~> 1.0)
25+
sorbet-runtime (>= 0.5.9204)
26+
sorbet (0.5.11856)
27+
sorbet-static (= 0.5.11856)
28+
sorbet-runtime (0.5.11856)
29+
sorbet-static (0.5.11856-aarch64-linux)
30+
sorbet-static (0.5.11856-universal-darwin)
31+
sorbet-static (0.5.11856-x86_64-linux)
32+
sorbet-static-and-runtime (0.5.11856)
33+
sorbet (= 0.5.11856)
34+
sorbet-runtime (= 0.5.11856)
35+
spoom (1.5.4)
36+
erubi (>= 1.10.0)
37+
prism (>= 0.28.0)
38+
rbi (>= 0.2.3)
39+
sorbet-static-and-runtime (>= 0.5.10187)
40+
thor (>= 0.19.2)
41+
tapioca (0.16.11)
42+
benchmark
43+
bundler (>= 2.2.25)
44+
netrc (>= 0.11.0)
45+
parallel (>= 1.21.0)
46+
rbi (~> 0.2)
47+
sorbet-static-and-runtime (>= 0.5.11087)
48+
spoom (>= 1.2.0)
49+
thor (>= 1.2.0)
50+
yard-sorbet
51+
temporalio (0.3.0-aarch64-linux)
52+
google-protobuf (>= 3.27.0)
53+
temporalio (0.3.0-arm64-darwin)
54+
google-protobuf (>= 3.27.0)
55+
temporalio (0.3.0-x86_64-darwin)
56+
google-protobuf (>= 3.27.0)
57+
temporalio (0.3.0-x86_64-linux)
58+
google-protobuf (>= 3.27.0)
59+
temporalio (0.3.0-x86_64-linux-musl)
60+
google-protobuf (>= 3.27.0)
61+
thor (1.3.2)
62+
yard (0.9.37)
63+
yard-sorbet (0.9.0)
64+
sorbet-runtime
65+
yard
66+
67+
PLATFORMS
68+
aarch64-linux
69+
arm64-darwin
70+
universal-darwin
71+
x86_64-darwin
72+
x86_64-linux
73+
x86_64-linux-musl
74+
75+
DEPENDENCIES
76+
sorbet
77+
sorbet-runtime
78+
tapioca
79+
temporalio
80+
yard
81+
82+
BUNDLED WITH
83+
2.6.2

sorbet_generic/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Sorbet Generic
2+
3+
This sample shows a proof of concept on how to use advanced Sorbet generics for activities, workflows, signals, queries,
4+
and updates. The Temporal Ruby SDK does not have Sorbet signatures natively, so this is just a proof of concept on how
5+
Sorbet users could consume the library if they have advanced needs.
6+
7+
⚠️ Users that are not concerned with advanced generic use cases can just use traditional `tapioca` generation to
8+
generate the RBI files for Temporal Ruby SDK. This sample is only here to demonstrate **advanced generic use cases**.
9+
10+
See the "Issues" section for details on issues encountered during development.
11+
12+
To run, first see [README.md](../README.md) for prerequisites. Then, in this directory:
13+
14+
bundle install
15+
16+
To check types, run:
17+
18+
bundle exec srb tc
19+
20+
Note how you can change a parameter type when calling activity, workflow, signal, query, or update and type checking
21+
will fail if it doesn't match expectation.
22+
23+
To actually run the code, from a terminal, start the worker:
24+
25+
bundle exec ruby worker.rb
26+
27+
Then, from another terminal, run the workflow:
28+
29+
bundle exec ruby starter.rb
30+
31+
## Issues
32+
33+
There are a few issues with Sorbet and advanced use that had to be worked around. See each section below.
34+
35+
### Generated Code Parameter Arity
36+
37+
The Temporal Ruby SDK does not use Sorbet itself and does not take a `sorbet-runtime` dependency or have inline
38+
signatures. Therefore, Temporal must be treated like any other non-Sorbet Ruby library. To generate the signatures
39+
herein, we:
40+
41+
* Ran `bundle exec tapioca init`
42+
* Ran `bundle exec tapioca require`
43+
* Added `require 'google/protobuf'` to `tapioca/require.rb`
44+
* Ran `bundle exec tapioca gem temporalio google-protobuf`
45+
46+
Unfortunately, due to the fact that the SDK supports splatted positional parameters (e.g. for a workflow) where a user
47+
may want a well-typed limited set of parameters, we have to alter the generated code. As documented at
48+
https://srb.help/4010:
49+
50+
> In cases like these, usually the solution is to remove the `foo` definition from `autogenerated/some_gem.rbi`
51+
52+
This means we had to hand-mutate the `tapioca`-generated RBI files, specifically we had to comment out all methods we
53+
wanted to refine parameter arity on. Look for "NOTE: Manually removed" comments in
54+
[sorbet/rbi/gems/temporalio@0.3.0.rbi](sorbet/rbi/gems/temporalio@0.3.0.rbi).
55+
56+
### Arity Mismatch
57+
58+
For generic reasons we had to define the base classes for workflows and activities as having a single input (which
59+
Temporal encourages anyways). However, it is also normal to have _no_ input, but Sorbet disallows overrides to change
60+
the parameter count, so a `_` placeholder param with a default has to be used to ignore it.
61+
62+
### Use of Decorator-Like Approach
63+
64+
When a signal, query, or update is defined on a method, the Ruby SDK also makes an associated class method for the
65+
"definition" of that handler for use by clients. So when this is present:
66+
67+
```ruby
68+
workflow_signal
69+
sig { params(some_value: String).void }
70+
def my_signal(some_value)
71+
@some_value = some_value
72+
end
73+
```
74+
75+
That creates a `my_signal` _class_ method on the workflow class dynamically. However, due to an
76+
[issue in Sorbet](https://github.com/sorbet/sorbet/issues/8592) defining singleton methods in instance
77+
`on_method_added`, this needs to change to:
78+
79+
```ruby
80+
workflow_signal
81+
T::Sig::WithoutRuntime.sig { params(some_value: String).void }
82+
def my_signal(some_value)
83+
@some_value = some_value
84+
end
85+
```
86+
87+
This disables runtime behavior to prevent the bug.
88+
89+
### Referencing Generic Classes
90+
91+
In Sorbet a generic class is referenced using brackets. For example, to define a class method stub on a workflow to
92+
represent the above-section-mentioned class methods created, one might have:
93+
94+
```ruby
95+
sig { returns(Temporalio::Workflow::Definition::Signal[String]) }
96+
def self.my_signal = T.unsafe(nil)
97+
```
98+
99+
But this will fail at runtime because the Temporal Ruby SDK doesn't define `[]`. So you have to change this to:
100+
101+
```ruby
102+
T::Sig::WithoutRuntime.sig { returns(Temporalio::Workflow::Definition::Signal[String]) }
103+
def self.my_signal = T.unsafe(nil)
104+
```
105+
106+
This will avoid processing the invalid body.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require 'sorbet-runtime'
5+
require 'temporalio/activity'
6+
7+
module SorbetGeneric
8+
class SayHelloActivity < Temporalio::Activity::Definition
9+
extend T::Sig
10+
extend T::Generic
11+
12+
Input = type_member { { fixed: String } }
13+
Output = type_member { { fixed: String } }
14+
15+
sig { override.params(name: Input).returns(Output) }
16+
def execute(name)
17+
"Hello, #{name}!"
18+
end
19+
end
20+
end

0 commit comments

Comments
 (0)