protoc-gen-ts-temporal 0.0.1

protoc plugin that emits a typed TypeScript Temporal client from temporal.v1.* annotated protos
Documentation
//! Cross-method invariants on a parsed `ServiceModel`.
//!
//! Anything that requires looking at more than one method, or comparing
//! attached refs against declared rpcs, lives here. Single-method shape
//! checks belong in `parse.rs`.

use std::collections::HashSet;

use anyhow::{Result, bail};

use crate::model::*;

pub fn validate(svc: &ServiceModel) -> Result<()> {
    no_name_collisions(svc)?;
    workflows_have_task_queue(svc)?;
    signals_return_empty(svc)?;
    refs_resolve(svc)?;
    Ok(())
}

/// A workflow needs a non-empty resolved task_queue — either set on the
/// workflow itself or inherited from the enclosing service's
/// `ServiceOptions.task_queue` default.
fn workflows_have_task_queue(svc: &ServiceModel) -> Result<()> {
    for w in &svc.workflows {
        if svc.resolved_task_queue(w).is_empty() {
            bail!(
                "{}.{}: workflow {:?} has no task_queue. Set option (temporal.v1.workflow) = {{ task_queue: \"...\" }} on the rpc, or option (temporal.v1.service) = {{ task_queue: \"...\" }} on the service.",
                svc.package,
                svc.service,
                w.rpc_method
            );
        }
    }
    Ok(())
}

/// Workflow, signal, query, update, and activity registered names share the
/// same namespace from a client's perspective — collisions would yield
/// unreachable methods on the generated handle.
fn no_name_collisions(svc: &ServiceModel) -> Result<()> {
    let entries = svc
        .workflows
        .iter()
        .map(|w| {
            (
                w.registered_name.as_deref().unwrap_or(&w.rpc_method),
                "workflow",
            )
        })
        .chain(
            svc.signals
                .iter()
                .map(|s| (s.registered_name.as_str(), "signal")),
        )
        .chain(
            svc.queries
                .iter()
                .map(|q| (q.registered_name.as_str(), "query")),
        )
        .chain(
            svc.updates
                .iter()
                .map(|u| (u.registered_name.as_str(), "update")),
        )
        .chain(
            svc.activities
                .iter()
                .map(|a| (a.registered_name.as_str(), "activity")),
        );

    let mut seen: HashSet<&str> = HashSet::new();
    for (name, kind) in entries {
        if !seen.insert(name) {
            bail!(
                "{}.{}: name {:?} ({}) collides with another method registered name",
                svc.package,
                svc.service,
                name,
                kind
            );
        }
    }
    Ok(())
}

/// Temporal signals are fire-and-forget — they can't return values. The
/// generator special-cases `google.protobuf.Empty` outputs.
fn signals_return_empty(svc: &ServiceModel) -> Result<()> {
    for s in &svc.signals {
        if !s.output_type.is_empty {
            bail!(
                "{}.{}: signal rpc {:?} must return google.protobuf.Empty, got {}",
                svc.package,
                svc.service,
                s.rpc_method,
                s.output_type.full_name
            );
        }
    }
    Ok(())
}

/// Every `WorkflowOptions.{signal,query,update}.ref` must point at a method
/// in the same service with the matching annotation.
fn refs_resolve(svc: &ServiceModel) -> Result<()> {
    let signal_methods: HashSet<&str> = svc.signals.iter().map(|s| s.rpc_method.as_str()).collect();
    let query_methods: HashSet<&str> = svc.queries.iter().map(|q| q.rpc_method.as_str()).collect();
    let update_methods: HashSet<&str> = svc.updates.iter().map(|u| u.rpc_method.as_str()).collect();

    for w in &svc.workflows {
        for r in &w.attached_signals {
            if !signal_methods.contains(r.method.as_str()) {
                bail!(
                    "{}.{}: workflow {:?} references unknown signal ref {:?}",
                    svc.package,
                    svc.service,
                    w.rpc_method,
                    r.method
                );
            }
        }
        for r in &w.attached_queries {
            if !query_methods.contains(r.method.as_str()) {
                bail!(
                    "{}.{}: workflow {:?} references unknown query ref {:?}",
                    svc.package,
                    svc.service,
                    w.rpc_method,
                    r.method
                );
            }
        }
        for r in &w.attached_updates {
            if !update_methods.contains(r.method.as_str()) {
                bail!(
                    "{}.{}: workflow {:?} references unknown update ref {:?}",
                    svc.package,
                    svc.service,
                    w.rpc_method,
                    r.method
                );
            }
        }
    }
    Ok(())
}