Skip to main content

protoc_gen_rust_temporal/
render.rs

1//! `ServiceModel` -> Rust source emission.
2//!
3//! Output shape (`<package>_<service>_temporal` module):
4//!
5//! * One `<RPC>_WORKFLOW_NAME` constant per workflow.
6//! * One `<Service>Client` struct holding a `temporal_runtime::TemporalClient`,
7//!   plus one `<rpc>(input, opts) -> <Workflow>Handle` start method and one
8//!   `<rpc>_handle(workflow_id)` attach method per workflow.
9//! * One `<Workflow>StartOptions` per workflow.
10//! * One `<Workflow>Handle` per workflow, exposing only the signals, queries,
11//!   and updates wired in via `WorkflowOptions.{signal,query,update}`.
12//! * One `<signal>_with_start` / `<update>_with_start` free function per
13//!   start-flagged ref.
14//!
15//! Generated code refers to runtime helpers via `crate::temporal_runtime::*`
16//! — the consumer crate provides that module. This mirrors the PoC's wiring
17//! and lets the plugin stay agnostic to whichever Temporal SDK shape the
18//! consumer pins.
19
20use std::fmt::Write;
21
22use heck::{ToShoutySnakeCase, ToSnakeCase};
23
24use crate::model::{
25    IdTemplateSegment, QueryModel, ServiceModel, SignalModel, UpdateModel, WorkflowModel,
26};
27
28/// Render the `<package>_<service>_temporal.rs` source for one service.
29pub fn render(svc: &ServiceModel) -> String {
30    let mut out = String::new();
31    let mod_name = mod_name(svc);
32    let proto_mod = proto_module_path(&svc.package);
33    let client_struct = format!("{}Client", svc.service);
34
35    let _ = writeln!(
36        out,
37        "// Code generated by protoc-gen-rust-temporal. DO NOT EDIT."
38    );
39    let _ = writeln!(out, "// source: {}", svc.source_file);
40    let _ = writeln!(out);
41    let _ = writeln!(out, "#[allow(clippy::all, unused_imports, dead_code)]");
42    let _ = writeln!(out, "pub mod {mod_name} {{");
43    let _ = writeln!(out, "    use anyhow::Result;");
44    let _ = writeln!(out, "    use std::time::Duration;");
45    let _ = writeln!(out, "    use crate::temporal_runtime;");
46    let _ = writeln!(out, "    use {proto_mod}::*;");
47    let _ = writeln!(out);
48
49    render_message_type_impls(&mut out, svc);
50    render_constants(&mut out, svc);
51    render_id_fns(&mut out, svc);
52    render_client_struct(&mut out, svc, &client_struct);
53    for wf in &svc.workflows {
54        render_start_options(&mut out, wf);
55        render_handle(&mut out, svc, wf);
56    }
57    render_with_start_functions(&mut out, svc);
58
59    let _ = writeln!(out, "}}");
60    out
61}
62
63/// Emit one `impl temporal_runtime::TemporalProtoMessage for <Ty>` per
64/// distinct prost message type referenced by this service. The runtime
65/// facade re-exports the trait from `temporal-proto-runtime`, so this is
66/// what lets `start_workflow_proto::<I>` / `signal_proto::<I>` etc. resolve
67/// at the call site. `google.protobuf.Empty` types are skipped — the
68/// `_empty` runtime variants take no payload generic.
69fn render_message_type_impls(out: &mut String, svc: &ServiceModel) {
70    use std::collections::BTreeMap;
71
72    let mut by_rust_name: BTreeMap<String, String> = BTreeMap::new();
73    let mut record = |pt: &crate::model::ProtoType| {
74        if pt.is_empty {
75            return;
76        }
77        by_rust_name
78            .entry(pt.rust_name().to_string())
79            .or_insert_with(|| pt.full_name.clone());
80    };
81
82    for wf in &svc.workflows {
83        record(&wf.input_type);
84        record(&wf.output_type);
85    }
86    for s in &svc.signals {
87        record(&s.input_type);
88        record(&s.output_type);
89    }
90    for q in &svc.queries {
91        record(&q.input_type);
92        record(&q.output_type);
93    }
94    for u in &svc.updates {
95        record(&u.input_type);
96        record(&u.output_type);
97    }
98    // Activities are validate-only — their message types are not used by the
99    // emitted client surface, so they do not need impls.
100
101    if by_rust_name.is_empty() {
102        return;
103    }
104
105    for (rust_name, full_name) in &by_rust_name {
106        let _ = writeln!(
107            out,
108            "    impl temporal_runtime::TemporalProtoMessage for {rust_name} {{"
109        );
110        let _ = writeln!(
111            out,
112            "        const MESSAGE_TYPE: &'static str = \"{full_name}\";"
113        );
114        let _ = writeln!(out, "    }}");
115    }
116    let _ = writeln!(out);
117}
118
119fn render_constants(out: &mut String, svc: &ServiceModel) {
120    for wf in &svc.workflows {
121        let const_name = format!("{}_WORKFLOW_NAME", wf.rpc_method.to_shouty_snake_case());
122        let _ = writeln!(
123            out,
124            "    pub const {const_name}: &str = \"{}\";",
125            wf.registered_name
126        );
127        if let Some(tq) = effective_task_queue(svc, wf) {
128            let tq_const = format!("{}_TASK_QUEUE", wf.rpc_method.to_shouty_snake_case());
129            let _ = writeln!(out, "    pub const {tq_const}: &str = \"{tq}\";");
130        }
131    }
132    if !svc.workflows.is_empty() {
133        let _ = writeln!(out);
134    }
135}
136
137/// Emit one private `<wf>_id` function per workflow that has an `id`
138/// template, materialising the template into a `format!` against the
139/// input message's fields at codegen time. The start path (and the
140/// `<signal>_with_start` / `<update>_with_start` free functions) call
141/// this instead of a generic runtime template helper — there's no
142/// runtime template engine to maintain and the field lookups are
143/// statically type-checked.
144fn render_id_fns(out: &mut String, svc: &ServiceModel) {
145    let mut emitted_any = false;
146    for wf in &svc.workflows {
147        let Some(segments) = wf.id_expression.as_ref() else {
148            continue;
149        };
150        emitted_any = true;
151        let fn_name = format!("{}_id", wf.rpc_method.to_snake_case());
152        let (fmt, args) = compile_id_template(segments);
153
154        if wf.input_type.is_empty {
155            // Validation in parse.rs guarantees a Field segment cannot
156            // refer to a field on Empty, so `args` is empty here. The fn
157            // takes no input.
158            let _ = writeln!(out, "    fn {fn_name}() -> String {{");
159            if fmt.is_empty() {
160                let _ = writeln!(out, "        String::new()");
161            } else {
162                let _ = writeln!(out, "        \"{fmt}\".to_string()");
163            }
164            let _ = writeln!(out, "    }}");
165            let _ = writeln!(out);
166            continue;
167        }
168
169        let input_ty = wf.input_type.rust_name();
170        let _ = writeln!(out, "    fn {fn_name}(input: &{input_ty}) -> String {{");
171        if args.is_empty() {
172            let _ = writeln!(out, "        let _ = input;");
173            let _ = writeln!(out, "        \"{fmt}\".to_string()");
174        } else {
175            let _ = writeln!(out, "        format!(\"{fmt}\", {})", args.join(", "));
176        }
177        let _ = writeln!(out, "    }}");
178        let _ = writeln!(out);
179    }
180    if emitted_any {
181        // No trailing blank — `render_client_struct` adds its own.
182    }
183}
184
185/// Pick the call site that produces a workflow id when the caller did
186/// not supply one. With a proto-declared `id` template we route through
187/// the codegen-emitted `<wf>_id(...)` function; without one we fall
188/// back to `temporal_runtime::random_workflow_id()`.
189///
190/// `in_self_method` is true inside `<Service>Client::<rpc>` (where the
191/// workflow input is named `input`) and false inside the
192/// `<signal>_with_start` / `<update>_with_start` free functions (where
193/// the workflow input is named `workflow_input`).
194fn id_fallback_call(wf: &WorkflowModel, in_self_method: bool) -> String {
195    if wf.id_expression.is_none() {
196        return "temporal_runtime::random_workflow_id()".to_string();
197    }
198    let fn_name = format!("{}_id", wf.rpc_method.to_snake_case());
199    if wf.input_type.is_empty {
200        // Validated in parse.rs: an Empty workflow input cannot host a
201        // Field segment, so the id fn takes no arguments.
202        return format!("{fn_name}()");
203    }
204    if in_self_method {
205        format!("{fn_name}(&input)")
206    } else {
207        format!("{fn_name}(&workflow_input)")
208    }
209}
210
211/// Walk template segments into a `format!` string + arg list. Literal `{`
212/// and `}` characters in the template are doubled so they survive the
213/// `format!` parse.
214fn compile_id_template(segments: &[IdTemplateSegment]) -> (String, Vec<String>) {
215    let mut fmt = String::new();
216    let mut args = Vec::new();
217    for seg in segments {
218        match seg {
219            IdTemplateSegment::Literal(s) => {
220                fmt.push_str(&s.replace('{', "{{").replace('}', "}}"));
221            }
222            IdTemplateSegment::Field(rust_name) => {
223                fmt.push_str("{}");
224                args.push(format!("input.{rust_name}"));
225            }
226        }
227    }
228    (fmt, args)
229}
230
231fn render_client_struct(out: &mut String, svc: &ServiceModel, client_struct: &str) {
232    let _ = writeln!(out, "    pub struct {client_struct} {{");
233    let _ = writeln!(out, "        client: temporal_runtime::TemporalClient,");
234    let _ = writeln!(out, "    }}");
235    let _ = writeln!(out);
236
237    let _ = writeln!(out, "    impl {client_struct} {{");
238    let _ = writeln!(
239        out,
240        "        pub fn new(client: temporal_runtime::TemporalClient) -> Self {{"
241    );
242    let _ = writeln!(out, "            Self {{ client }}");
243    let _ = writeln!(out, "        }}");
244    let _ = writeln!(out);
245    let _ = writeln!(
246        out,
247        "        pub fn inner(&self) -> &temporal_runtime::TemporalClient {{"
248    );
249    let _ = writeln!(out, "            &self.client");
250    let _ = writeln!(out, "        }}");
251    let _ = writeln!(out);
252
253    for wf in &svc.workflows {
254        render_client_workflow_methods(out, svc, wf);
255    }
256
257    let _ = writeln!(out, "    }}");
258    let _ = writeln!(out);
259}
260
261fn render_client_workflow_methods(out: &mut String, svc: &ServiceModel, wf: &WorkflowModel) {
262    let method_snake = wf.rpc_method.to_snake_case();
263    let handle_struct = format!("{}Handle", wf.rpc_method);
264    let opts_struct = format!("{}StartOptions", wf.rpc_method);
265    let const_name = format!("{}_WORKFLOW_NAME", wf.rpc_method.to_shouty_snake_case());
266
267    let _ = writeln!(
268        out,
269        "        /// Start a new `{}` workflow.",
270        wf.registered_name
271    );
272    let _ = writeln!(out, "        pub async fn {method_snake}(");
273    let _ = writeln!(out, "            &self,");
274    if !wf.input_type.is_empty {
275        let _ = writeln!(out, "            input: {},", wf.input_type.rust_name());
276    }
277    let _ = writeln!(out, "            opts: {opts_struct},");
278    let _ = writeln!(out, "        ) -> Result<{handle_struct}> {{");
279    render_start_body(
280        out,
281        svc,
282        wf,
283        &const_name,
284        /* leading_indent: */ "            ",
285    );
286    let _ = writeln!(out, "            Ok({handle_struct} {{ inner }})");
287    let _ = writeln!(out, "        }}");
288    let _ = writeln!(out);
289
290    let _ = writeln!(
291        out,
292        "        /// Attach to a running `{}` workflow by id.",
293        wf.registered_name
294    );
295    let _ = writeln!(
296        out,
297        "        pub fn {method_snake}_handle(&self, workflow_id: impl Into<String>) -> {handle_struct} {{"
298    );
299    let _ = writeln!(out, "            {handle_struct} {{");
300    let _ = writeln!(
301        out,
302        "                inner: temporal_runtime::attach_handle(&self.client, workflow_id.into()),"
303    );
304    let _ = writeln!(out, "            }}");
305    let _ = writeln!(out, "        }}");
306    let _ = writeln!(out);
307}
308
309/// Emit the body of the regular start method: resolves workflow_id +
310/// task_queue and dispatches `start_workflow_proto`. `leading_indent` is the
311/// whitespace prefix applied to each emitted line (12 spaces inside an
312/// `impl` method, 8 spaces inside a free function).
313fn render_start_body(
314    out: &mut String,
315    svc: &ServiceModel,
316    wf: &WorkflowModel,
317    const_name: &str,
318    leading_indent: &str,
319) {
320    let ind = leading_indent;
321    let _ = writeln!(
322        out,
323        "{ind}let workflow_id = opts.workflow_id.unwrap_or_else(|| {{"
324    );
325    let _ = writeln!(
326        out,
327        "{ind}    {}",
328        id_fallback_call(wf, /* in_self_method: */ true)
329    );
330    let _ = writeln!(out, "{ind}}});");
331
332    if let Some(tq) = effective_task_queue(svc, wf) {
333        let _ = writeln!(
334            out,
335            "{ind}let task_queue = opts.task_queue.unwrap_or_else(|| \"{tq}\".to_string());"
336        );
337    } else {
338        let _ = writeln!(
339            out,
340            "{ind}let task_queue = opts.task_queue.expect(\"workflow has no proto-level task_queue; opts.task_queue is required\");"
341        );
342    }
343
344    if wf.input_type.is_empty {
345        // Empty workflow input — route to a separate runtime function that
346        // skips the payload arg. Avoids `&()` not impl'ing
347        // TemporalProtoMessage at the call site.
348        let _ = writeln!(
349            out,
350            "{ind}let inner = temporal_runtime::start_workflow_proto_empty("
351        );
352        let _ = writeln!(out, "{ind}    &self.client,");
353        let _ = writeln!(out, "{ind}    {const_name},");
354        let _ = writeln!(out, "{ind}    &workflow_id,");
355        let _ = writeln!(out, "{ind}    &task_queue,");
356    } else {
357        let _ = writeln!(
358            out,
359            "{ind}let inner = temporal_runtime::start_workflow_proto("
360        );
361        let _ = writeln!(out, "{ind}    &self.client,");
362        let _ = writeln!(out, "{ind}    {const_name},");
363        let _ = writeln!(out, "{ind}    &workflow_id,");
364        let _ = writeln!(out, "{ind}    &task_queue,");
365        let _ = writeln!(out, "{ind}    &input,");
366    }
367    let _ = writeln!(out, "{ind}    opts.id_reuse_policy,");
368    let _ = writeln!(out, "{ind}    opts.execution_timeout,");
369    let _ = writeln!(out, "{ind}    opts.run_timeout,");
370    let _ = writeln!(out, "{ind}    opts.task_timeout,");
371    let _ = writeln!(out, "{ind}).await?;");
372}
373
374fn render_start_options(out: &mut String, wf: &WorkflowModel) {
375    let opts_struct = format!("{}StartOptions", wf.rpc_method);
376    let _ = writeln!(out, "    #[derive(Debug, Default, Clone)]");
377    let _ = writeln!(out, "    pub struct {opts_struct} {{");
378    let _ = writeln!(out, "        pub workflow_id: Option<String>,");
379    let _ = writeln!(out, "        pub task_queue: Option<String>,");
380    let _ = writeln!(
381        out,
382        "        pub id_reuse_policy: Option<temporal_runtime::WorkflowIdReusePolicy>,"
383    );
384    let _ = writeln!(out, "        pub execution_timeout: Option<Duration>,");
385    let _ = writeln!(out, "        pub run_timeout: Option<Duration>,");
386    let _ = writeln!(out, "        pub task_timeout: Option<Duration>,");
387    let _ = writeln!(out, "    }}");
388    let _ = writeln!(out);
389
390    let mut defaults: Vec<(&'static str, String, &'static str)> = Vec::new();
391    if let Some(p) = wf.id_reuse_policy {
392        defaults.push((
393            "default_id_reuse_policy",
394            format!(
395                "temporal_runtime::WorkflowIdReusePolicy::{}",
396                p.rust_variant()
397            ),
398            "temporal_runtime::WorkflowIdReusePolicy",
399        ));
400    }
401    if let Some(d) = wf.execution_timeout {
402        defaults.push(("default_execution_timeout", duration_literal(d), "Duration"));
403    }
404    if let Some(d) = wf.run_timeout {
405        defaults.push(("default_run_timeout", duration_literal(d), "Duration"));
406    }
407    if let Some(d) = wf.task_timeout {
408        defaults.push(("default_task_timeout", duration_literal(d), "Duration"));
409    }
410    if !defaults.is_empty() {
411        let _ = writeln!(out, "    impl {opts_struct} {{");
412        for (name, value, return_ty) in &defaults {
413            let _ = writeln!(out, "        pub fn {name}() -> {return_ty} {{");
414            let _ = writeln!(out, "            {value}");
415            let _ = writeln!(out, "        }}");
416        }
417        let _ = writeln!(out, "    }}");
418        let _ = writeln!(out);
419    }
420}
421
422fn render_handle(out: &mut String, svc: &ServiceModel, wf: &WorkflowModel) {
423    let handle_struct = format!("{}Handle", wf.rpc_method);
424    let _ = writeln!(out, "    pub struct {handle_struct} {{");
425    let _ = writeln!(out, "        inner: temporal_runtime::WorkflowHandle,");
426    let _ = writeln!(out, "    }}");
427    let _ = writeln!(out);
428
429    let _ = writeln!(out, "    impl {handle_struct} {{");
430    let _ = writeln!(out, "        pub fn workflow_id(&self) -> &str {{");
431    let _ = writeln!(out, "            self.inner.workflow_id()");
432    let _ = writeln!(out, "        }}");
433    let _ = writeln!(out);
434
435    // result()
436    let _ = writeln!(
437        out,
438        "        /// Wait for the workflow to complete and return its output."
439    );
440    if wf.output_type.is_empty {
441        let _ = writeln!(out, "        pub async fn result(&self) -> Result<()> {{");
442        let _ = writeln!(
443            out,
444            "            temporal_runtime::wait_result_unit(&self.inner).await"
445        );
446        let _ = writeln!(out, "        }}");
447    } else {
448        let output_ty = wf.output_type.rust_name();
449        let _ = writeln!(
450            out,
451            "        pub async fn result(&self) -> Result<{output_ty}> {{"
452        );
453        let _ = writeln!(
454            out,
455            "            temporal_runtime::wait_result_proto::<{output_ty}>(&self.inner).await"
456        );
457        let _ = writeln!(out, "        }}");
458    }
459    let _ = writeln!(out);
460
461    for sref in &wf.attached_signals {
462        if let Some(sig) = svc.signals.iter().find(|s| s.rpc_method == sref.rpc_method) {
463            render_signal_method(out, sig);
464        }
465    }
466    for qref in &wf.attached_queries {
467        if let Some(q) = svc
468            .queries
469            .iter()
470            .find(|qq| qq.rpc_method == qref.rpc_method)
471        {
472            render_query_method(out, q);
473        }
474    }
475    for uref in &wf.attached_updates {
476        if let Some(u) = svc
477            .updates
478            .iter()
479            .find(|uu| uu.rpc_method == uref.rpc_method)
480        {
481            render_update_method(out, u);
482        }
483    }
484
485    let _ = writeln!(out, "    }}");
486    let _ = writeln!(out);
487}
488
489fn render_signal_method(out: &mut String, sig: &SignalModel) {
490    let method_snake = sig.rpc_method.to_snake_case();
491    let _ = writeln!(
492        out,
493        "        /// Send the `{}` signal.",
494        sig.registered_name
495    );
496    if sig.input_type.is_empty {
497        let _ = writeln!(
498            out,
499            "        pub async fn {method_snake}(&self) -> Result<()> {{"
500        );
501        let _ = writeln!(
502            out,
503            "            temporal_runtime::signal_unit(&self.inner, \"{}\").await",
504            sig.registered_name
505        );
506        let _ = writeln!(out, "        }}");
507    } else {
508        let input_ty = sig.input_type.rust_name();
509        let _ = writeln!(
510            out,
511            "        pub async fn {method_snake}(&self, input: {input_ty}) -> Result<()> {{"
512        );
513        let _ = writeln!(
514            out,
515            "            temporal_runtime::signal_proto(&self.inner, \"{}\", &input).await",
516            sig.registered_name
517        );
518        let _ = writeln!(out, "        }}");
519    }
520    let _ = writeln!(out);
521}
522
523fn render_query_method(out: &mut String, q: &QueryModel) {
524    let method_snake = q.rpc_method.to_snake_case();
525    let out_ty = q.output_type.rust_name();
526    let _ = writeln!(out, "        /// Run the `{}` query.", q.registered_name);
527    if q.input_type.is_empty {
528        let _ = writeln!(
529            out,
530            "        pub async fn {method_snake}(&self) -> Result<{out_ty}> {{"
531        );
532        let _ = writeln!(
533            out,
534            "            temporal_runtime::query_proto_empty::<{out_ty}>(&self.inner, \"{}\").await",
535            q.registered_name
536        );
537        let _ = writeln!(out, "        }}");
538    } else {
539        let in_ty = q.input_type.rust_name();
540        let _ = writeln!(
541            out,
542            "        pub async fn {method_snake}(&self, input: {in_ty}) -> Result<{out_ty}> {{"
543        );
544        let _ = writeln!(
545            out,
546            "            temporal_runtime::query_proto::<{in_ty}, {out_ty}>(&self.inner, \"{}\", &input).await",
547            q.registered_name
548        );
549        let _ = writeln!(out, "        }}");
550    }
551    let _ = writeln!(out);
552}
553
554fn render_update_method(out: &mut String, u: &UpdateModel) {
555    let method_snake = u.rpc_method.to_snake_case();
556    let out_ty = u.output_type.rust_name();
557    let _ = writeln!(out, "        /// Run the `{}` update.", u.registered_name);
558    if u.input_type.is_empty {
559        let _ = writeln!(
560            out,
561            "        pub async fn {method_snake}(&self, wait_policy: temporal_runtime::WaitPolicy) -> Result<{out_ty}> {{"
562        );
563        let _ = writeln!(
564            out,
565            "            temporal_runtime::update_proto_empty::<{out_ty}>(&self.inner, \"{}\", wait_policy).await",
566            u.registered_name
567        );
568        let _ = writeln!(out, "        }}");
569    } else {
570        let in_ty = u.input_type.rust_name();
571        let _ = writeln!(
572            out,
573            "        pub async fn {method_snake}(&self, input: {in_ty}, wait_policy: temporal_runtime::WaitPolicy) -> Result<{out_ty}> {{"
574        );
575        let _ = writeln!(
576            out,
577            "            temporal_runtime::update_proto::<{in_ty}, {out_ty}>(&self.inner, \"{}\", &input, wait_policy).await",
578            u.registered_name
579        );
580        let _ = writeln!(out, "        }}");
581    }
582    let _ = writeln!(out);
583}
584
585fn render_with_start_functions(out: &mut String, svc: &ServiceModel) {
586    for wf in &svc.workflows {
587        for sref in &wf.attached_signals {
588            if !sref.start {
589                continue;
590            }
591            let Some(sig) = svc.signals.iter().find(|s| s.rpc_method == sref.rpc_method) else {
592                continue;
593            };
594            render_signal_with_start_fn(out, svc, wf, sig);
595        }
596        for uref in &wf.attached_updates {
597            if !uref.start {
598                continue;
599            }
600            let Some(u) = svc
601                .updates
602                .iter()
603                .find(|uu| uu.rpc_method == uref.rpc_method)
604            else {
605                continue;
606            };
607            render_update_with_start_fn(out, svc, wf, u);
608        }
609    }
610}
611
612fn render_signal_with_start_fn(
613    out: &mut String,
614    svc: &ServiceModel,
615    wf: &WorkflowModel,
616    sig: &SignalModel,
617) {
618    // Free function lives alongside the client struct so callers can pass a
619    // raw `TemporalClient` without constructing the client wrapper.
620    let fn_name = format!("{}_with_start", sig.rpc_method.to_snake_case());
621    let handle_struct = format!("{}Handle", wf.rpc_method);
622    let opts_struct = format!("{}StartOptions", wf.rpc_method);
623    let const_name = format!("{}_WORKFLOW_NAME", wf.rpc_method.to_shouty_snake_case());
624
625    let _ = writeln!(
626        out,
627        "    /// Start `{}` and atomically deliver the `{}` signal.",
628        wf.registered_name, sig.registered_name
629    );
630    let _ = writeln!(out, "    pub async fn {fn_name}(");
631    let _ = writeln!(out, "        client: &temporal_runtime::TemporalClient,");
632    if !sig.input_type.is_empty {
633        let _ = writeln!(out, "        signal_input: {},", sig.input_type.rust_name());
634    }
635    if !wf.input_type.is_empty {
636        let _ = writeln!(
637            out,
638            "        workflow_input: {},",
639            wf.input_type.rust_name()
640        );
641    }
642    let _ = writeln!(out, "        opts: {opts_struct},");
643    let _ = writeln!(out, "    ) -> Result<{handle_struct}> {{");
644
645    let signal_input_expr = if sig.input_type.is_empty {
646        "&()".to_string()
647    } else {
648        "&signal_input".to_string()
649    };
650    let workflow_input_expr = if wf.input_type.is_empty {
651        "&()".to_string()
652    } else {
653        "&workflow_input".to_string()
654    };
655
656    let _ = writeln!(
657        out,
658        "        let workflow_id = opts.workflow_id.clone().unwrap_or_else(|| {{"
659    );
660    let _ = writeln!(
661        out,
662        "            {}",
663        id_fallback_call(wf, /* in_self_method: */ false)
664    );
665    let _ = writeln!(out, "        }});");
666    if let Some(tq) = effective_task_queue(svc, wf) {
667        let _ = writeln!(
668            out,
669            "        let task_queue = opts.task_queue.unwrap_or_else(|| \"{tq}\".to_string());"
670        );
671    } else {
672        let _ = writeln!(
673            out,
674            "        let task_queue = opts.task_queue.expect(\"workflow has no proto-level task_queue; opts.task_queue is required\");"
675        );
676    }
677    let _ = writeln!(
678        out,
679        "        let inner = temporal_runtime::signal_with_start_workflow_proto("
680    );
681    let _ = writeln!(out, "            client,");
682    let _ = writeln!(out, "            {const_name},");
683    let _ = writeln!(out, "            &workflow_id,");
684    let _ = writeln!(out, "            &task_queue,");
685    let _ = writeln!(out, "            {workflow_input_expr},");
686    let _ = writeln!(out, "            \"{}\",", sig.registered_name);
687    let _ = writeln!(out, "            {signal_input_expr},");
688    let _ = writeln!(out, "            opts.id_reuse_policy,");
689    let _ = writeln!(out, "            opts.execution_timeout,");
690    let _ = writeln!(out, "            opts.run_timeout,");
691    let _ = writeln!(out, "            opts.task_timeout,");
692    let _ = writeln!(out, "        ).await?;");
693    let _ = writeln!(out, "        Ok({handle_struct} {{ inner }})");
694    let _ = writeln!(out, "    }}");
695    let _ = writeln!(out);
696}
697
698fn render_update_with_start_fn(
699    out: &mut String,
700    svc: &ServiceModel,
701    wf: &WorkflowModel,
702    u: &UpdateModel,
703) {
704    let fn_name = format!("{}_with_start", u.rpc_method.to_snake_case());
705    let handle_struct = format!("{}Handle", wf.rpc_method);
706    let opts_struct = format!("{}StartOptions", wf.rpc_method);
707    let const_name = format!("{}_WORKFLOW_NAME", wf.rpc_method.to_shouty_snake_case());
708
709    let _ = writeln!(
710        out,
711        "    /// Start `{}` and atomically deliver the `{}` update.",
712        wf.registered_name, u.registered_name
713    );
714    let _ = writeln!(out, "    pub async fn {fn_name}(");
715    let _ = writeln!(out, "        client: &temporal_runtime::TemporalClient,");
716    if !u.input_type.is_empty {
717        let _ = writeln!(out, "        update_input: {},", u.input_type.rust_name());
718    }
719    if !wf.input_type.is_empty {
720        let _ = writeln!(
721            out,
722            "        workflow_input: {},",
723            wf.input_type.rust_name()
724        );
725    }
726    let _ = writeln!(out, "        opts: {opts_struct},");
727    let _ = writeln!(out, "        wait_policy: temporal_runtime::WaitPolicy,");
728    let _ = writeln!(
729        out,
730        "    ) -> Result<({handle_struct}, {})> {{",
731        u.output_type.rust_name()
732    );
733
734    let update_input_expr = if u.input_type.is_empty {
735        "&()".to_string()
736    } else {
737        "&update_input".to_string()
738    };
739    let workflow_input_expr = if wf.input_type.is_empty {
740        "&()".to_string()
741    } else {
742        "&workflow_input".to_string()
743    };
744
745    let _ = writeln!(
746        out,
747        "        let workflow_id = opts.workflow_id.clone().unwrap_or_else(|| {{"
748    );
749    let _ = writeln!(
750        out,
751        "            {}",
752        id_fallback_call(wf, /* in_self_method: */ false)
753    );
754    let _ = writeln!(out, "        }});");
755    if let Some(tq) = effective_task_queue(svc, wf) {
756        let _ = writeln!(
757            out,
758            "        let task_queue = opts.task_queue.unwrap_or_else(|| \"{tq}\".to_string());"
759        );
760    } else {
761        let _ = writeln!(
762            out,
763            "        let task_queue = opts.task_queue.expect(\"workflow has no proto-level task_queue; opts.task_queue is required\");"
764        );
765    }
766    // Three explicit generics: workflow input (W), update input (U),
767    // update output (O). All three appear in distinct argument positions
768    // so they could in principle be inferred, but spelling them out keeps
769    // generated code grep-able and immune to inference brittleness.
770    let _ = writeln!(
771        out,
772        "        let (inner, update_result) = temporal_runtime::update_with_start_workflow_proto::<{}, {}, {}>(",
773        wf.input_type.rust_name(),
774        u.input_type.rust_name(),
775        u.output_type.rust_name(),
776    );
777    let _ = writeln!(out, "            client,");
778    let _ = writeln!(out, "            {const_name},");
779    let _ = writeln!(out, "            &workflow_id,");
780    let _ = writeln!(out, "            &task_queue,");
781    let _ = writeln!(out, "            {workflow_input_expr},");
782    let _ = writeln!(out, "            \"{}\",", u.registered_name);
783    let _ = writeln!(out, "            {update_input_expr},");
784    let _ = writeln!(out, "            wait_policy,");
785    let _ = writeln!(out, "            opts.id_reuse_policy,");
786    let _ = writeln!(out, "            opts.execution_timeout,");
787    let _ = writeln!(out, "            opts.run_timeout,");
788    let _ = writeln!(out, "            opts.task_timeout,");
789    let _ = writeln!(out, "        ).await?;");
790    let _ = writeln!(
791        out,
792        "        Ok(({handle_struct} {{ inner }}, update_result))"
793    );
794    let _ = writeln!(out, "    }}");
795    let _ = writeln!(out);
796}
797
798fn effective_task_queue<'a>(svc: &'a ServiceModel, wf: &'a WorkflowModel) -> Option<&'a str> {
799    wf.task_queue
800        .as_deref()
801        .or(svc.default_task_queue.as_deref())
802}
803
804fn duration_literal(d: std::time::Duration) -> String {
805    let secs = d.as_secs();
806    let nanos = d.subsec_nanos();
807    if nanos == 0 {
808        format!("Duration::from_secs({secs})")
809    } else {
810        format!("Duration::new({secs}, {nanos})")
811    }
812}
813
814fn mod_name(svc: &ServiceModel) -> String {
815    // jobs.v1 + JobService -> jobs_v1_job_service_temporal
816    format!(
817        "{}_{}_temporal",
818        svc.package.replace('.', "_"),
819        svc.service.to_snake_case()
820    )
821}
822
823fn proto_module_path(package: &str) -> String {
824    let mut p = String::from("crate");
825    if package.is_empty() {
826        return p;
827    }
828    for seg in package.split('.') {
829        p.push_str("::");
830        p.push_str(seg);
831    }
832    p
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    #[test]
840    fn mod_name_lowers_dots_and_camel() {
841        let svc = make_service("jobs.v1", "JobService");
842        assert_eq!(mod_name(&svc), "jobs_v1_job_service_temporal");
843    }
844
845    #[test]
846    fn proto_module_path_walks_package() {
847        assert_eq!(proto_module_path("jobs.v1"), "crate::jobs::v1");
848        assert_eq!(proto_module_path(""), "crate");
849    }
850
851    fn make_service(package: &str, service: &str) -> ServiceModel {
852        ServiceModel {
853            package: package.to_string(),
854            service: service.to_string(),
855            source_file: "test.proto".to_string(),
856            default_task_queue: None,
857            workflows: vec![],
858            signals: vec![],
859            queries: vec![],
860            updates: vec![],
861            activities: vec![],
862        }
863    }
864}