|
| 1 | +# Rust SDK User Interface |
| 2 | + |
| 3 | +Status: proposed through compile contracts. |
| 4 | + |
| 5 | +## Principles |
| 6 | + |
| 7 | +- User handlers are synchronous. |
| 8 | +- Flow start input, Step input, RPC input/output, Attributes, and Channels retain |
| 9 | + their Rust types. |
| 10 | +- User code derives `serde::Serialize` and `serde::Deserialize`; it does not |
| 11 | + provide codecs at each SDK call site. |
| 12 | +- Durable Flow and Step names default to Rust type names and may be overridden. |
| 13 | +- Runtime internals may erase types only after the public API checks them. |
| 14 | +- Calls should read naturally in Rust rather than reproduce Java syntax. |
| 15 | + |
| 16 | +## Flow and Step |
| 17 | + |
| 18 | +`Flow::StartInput` binds a Flow to its start Step at compile time: |
| 19 | + |
| 20 | +```rust |
| 21 | +struct OrderFlow { |
| 22 | + validate: ValidateOrder, |
| 23 | + charge: ChargeOrder, |
| 24 | +} |
| 25 | + |
| 26 | +impl Flow for OrderFlow { |
| 27 | + type StartInput = Order; |
| 28 | + |
| 29 | + fn steps(&self) -> StepList<Self::StartInput> { |
| 30 | + StepList::start(&self.validate).and(&self.charge) |
| 31 | + } |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +`StepDecision::go_to` accepts only the target Step's input type: |
| 36 | + |
| 37 | +```rust |
| 38 | +impl Step for ValidateOrder { |
| 39 | + type Input = Order; |
| 40 | + |
| 41 | + fn execute( |
| 42 | + &self, |
| 43 | + context: &mut Context, |
| 44 | + order: Order, |
| 45 | + ) -> HandlerResult<StepDecision> { |
| 46 | + self.status.set(context, "validated".to_string())?; |
| 47 | + Ok(StepDecision::go_to(&self.charge, ChargeInput::from(order))) |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +Execute-only Steps omit `wait_for`. Wait expressions use domain nouns: |
| 53 | + |
| 54 | +```rust |
| 55 | +fn wait_for(&self, _context: &mut Context, _input: OrderId) -> HandlerResult<Wait> { |
| 56 | + Ok(Wait::until(self.approved.for_one())) |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +Aggregate factories remain available for multiple or dynamic conditions: |
| 61 | + |
| 62 | +```rust |
| 63 | +fn wait_for(&self, _context: &mut Context, _input: OrderId) -> HandlerResult<Wait> { |
| 64 | + Ok(Wait::any_of([ |
| 65 | + self.approved.for_one(), |
| 66 | + Timer::by_duration(Duration::from_secs(30)).with_id("approval-timeout"), |
| 67 | + ])) |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +## Persistence |
| 72 | + |
| 73 | +Definitions carry their value types. Persistence operations therefore do not |
| 74 | +take runtime type tokens or codecs: |
| 75 | + |
| 76 | +```rust |
| 77 | +struct OrderFlow { |
| 78 | + status: Attribute<String>, |
| 79 | + attempts: AttributeMap<u32>, |
| 80 | + commands: Channel<Command>, |
| 81 | +} |
| 82 | + |
| 83 | +fn persistence(&self) -> PersistenceSchema { |
| 84 | + PersistenceSchema::new() |
| 85 | + .attribute(&self.status) |
| 86 | + .attribute_map(&self.attempts) |
| 87 | + .channel(&self.commands) |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +Indexed Attributes use named factories such as `AttributeIndex::keyword()` and |
| 92 | +`AttributeIndex::full_text()` instead of a separate public index-kind enum. |
| 93 | + |
| 94 | +Reads return `Option<T>` because an Attribute may be absent. Handlers can use |
| 95 | +`get_required` when absence is an application error. |
| 96 | + |
| 97 | +## RPC |
| 98 | + |
| 99 | +Rust uses typed RPC tokens instead of reflection or proxy stubs: |
| 100 | + |
| 101 | +```rust |
| 102 | +impl OrderFlow { |
| 103 | + const GET_ORDER: Rpc<OrderId, OrderView> = Rpc::new("get_order"); |
| 104 | + |
| 105 | + fn get_order( |
| 106 | + &self, |
| 107 | + context: &mut Context, |
| 108 | + order_id: OrderId, |
| 109 | + ) -> HandlerResult<RpcResult<OrderView>> { |
| 110 | + Ok(RpcResult::new(self.load(context, order_id)?)) |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +fn rpcs(&self) -> RpcList<Self> { |
| 115 | + RpcList::new().function( |
| 116 | + Self::GET_ORDER.lock(self.status.lock()), |
| 117 | + Self::get_order, |
| 118 | + ) |
| 119 | +} |
| 120 | + |
| 121 | +let order: OrderView = client.invoke_rpc(flow_id, OrderFlow::GET_ORDER, order_id)?; |
| 122 | +``` |
| 123 | + |
| 124 | +RPC registration checks that the method signature agrees with the token. |
| 125 | +Timeouts and locks chain directly from the token without an intermediate |
| 126 | +options type. |
| 127 | + |
| 128 | +Flow IDs and run IDs remain strings, matching the server protocol and the other |
| 129 | +SDKs. Rust newtypes are reserved for IDs whose APIs benefit from distinct types. |
| 130 | + |
| 131 | +## Errors |
| 132 | + |
| 133 | +Client operations return domain-specific `SdkError` variants. Applications |
| 134 | +match the outcome directly instead of branching on gRPC codes and server |
| 135 | +sub-status metadata: |
| 136 | + |
| 137 | +```rust |
| 138 | +match client.start_flow(&orders, "order-123", order) { |
| 139 | + Ok(run_id) => println!("started {run_id}"), |
| 140 | + Err(SdkError::FlowAlreadyStarted { .. }) => println!("already started"), |
| 141 | + Err(error) => return Err(error), |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +`FlowNotFound` is used by operations requiring an existing Flow; |
| 146 | +`FlowNotActive` is used by mutations and RPCs requiring a running Flow. |
| 147 | +`RpcLockConflict` and `LongPollTimeout` can be retried explicitly, while |
| 148 | +`WorkerInvocation` retains the original Worker error metadata. |
| 149 | + |
| 150 | +`HandlerError` remains separate because it crosses the user Step/RPC boundary; |
| 151 | +`SdkError` represents Client, registration, mapping, and service failures. |
| 152 | + |
| 153 | +## Runtime construction |
| 154 | + |
| 155 | +Client and Worker share an `Arc<BlobCache>` and cloned Registry metadata: |
| 156 | + |
| 157 | +```rust |
| 158 | +let registry = Registry::new().register(OrderFlow::new()); |
| 159 | +let cache = Arc::new(BlobCache::open(cache_config)?); |
| 160 | +let client = Client::new(registry.clone(), cache.clone(), ClientOptions::new()); |
| 161 | +let worker = Worker::new(registry, cache, WorkerOptions::new()); |
| 162 | +``` |
| 163 | + |
| 164 | +## Current scope |
| 165 | + |
| 166 | +The current crate defines and type-checks the public contracts. Client transport, |
| 167 | +error translation, Registry erasure, WorkerService dispatch, and value encoding |
| 168 | +deliberately return not-implemented errors until their runtime phases are |
| 169 | +implemented. |
| 170 | + |
| 171 | +## Tests |
| 172 | + |
| 173 | +The `dex-sdk` IWF compatibility target ports the Java workflow definitions and |
| 174 | +client assertions as compile contracts. `cargo test --no-run` validates the user |
| 175 | +experience without claiming that the unfinished runtime passes E2E scenarios. |
| 176 | + |
| 177 | +## Documentation |
| 178 | + |
| 179 | +Keep this document and `sdk-rust/README.md` synchronized with public API changes. |
| 180 | + |
| 181 | +## UI/UX |
| 182 | + |
| 183 | +N/A: no in-repo web UI. |
0 commit comments