|
| 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. |
0 commit comments