udb-client 0.5.22

Rust client for UDB — a proto-driven gRPC broker over multiple databases. Typed tonic clients plus the tenant/project metadata and token lifecycle the broker expects.
Documentation
//! Generate the tonic client stubs from the UDB protos.
//!
//! Client only — this crate consumes the broker's wire contract and never serves
//! it, so `build_server(false)` keeps the generated surface (and the compile
//! time) to what a client actually uses.
//!
//! Proto resolution has two modes on purpose:
//!
//! - **Repo checkout** (`../../proto`): the SDK builds against the live contract
//!   in the same tree, so it cannot silently lag the broker it ships beside.
//! - **Published crate** (`./proto`): `cargo publish` cannot reach outside the
//!   package directory, so the release step vendors the protos in first — see
//!   README, "Publishing". The repo path is preferred, so a working checkout is
//!   always authoritative over a stale vendored copy.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Vendored protoc, matching the broker: building this crate must not require
    // a system protoc install.
    // SAFETY: build scripts are single-threaded; nothing else reads PROTOC here.
    unsafe {
        std::env::set_var("PROTOC", protoc_bin_vendored::protoc_bin_path()?);
    }

    let (proto_root, googleapis) = resolve_roots()?;

    let protos = [
        // Data plane: Select / Upsert / Update / Delete / BulkCas / streaming.
        "udb/services/v1/data_broker.proto",
        // Native auth plane: login, refresh, and the authz decision surface.
        "udb/core/authn/services/v1/authn_service.proto",
        "udb/core/authz/services/v1/authz_service.proto",
        // The typed error payload the broker attaches to the
        // `udb-error-detail-bin` trailer. Listed EXPLICITLY because no service
        // proto imports it — it travels as trailer bytes, not as a field — so it
        // is absent from the transitive closure and would not otherwise be
        // generated.
        "udb/entity/v1/error.proto",
    ];
    let proto_paths: Vec<PathBuf> = protos.iter().map(|p| proto_root.join(p)).collect();

    for p in &proto_paths {
        if !p.exists() {
            return Err(format!(
                "missing proto {}\nExpected the UDB protos under {}. In a repo checkout they live \
                 at ../../proto; for a published crate they must be vendored into sdk/rust/proto \
                 (see README, \"Publishing\").",
                p.display(),
                proto_root.display()
            )
            .into());
        }
        println!("cargo:rerun-if-changed={}", p.display());
    }
    println!("cargo:rerun-if-changed={}", proto_root.display());

    let out_dir = PathBuf::from(std::env::var("OUT_DIR")?);
    tonic_prost_build::configure()
        .build_server(false)
        .build_client(true)
        // google/api's comments embed indented HTTP and proto examples. rustdoc
        // reads an indented block in a doc comment as a RUST doctest, so
        // `cargo test` tries to compile Google's prose and fails on it. Dropping
        // comments for that package only leaves UDB's own docs intact.
        .disable_comments(["google.api"])
        .compile_protos(&proto_paths, &[proto_root.clone(), googleapis])?;

    // Emit the module tree from what was ACTUALLY generated, rather than a
    // hand-written list of `include_proto!` calls. The transitive closure of
    // these three services spans dozens of proto packages; a hand-maintained list
    // would rot the first time the contract adds one, and the failure would read
    // as "cannot find module" rather than "the SDK is out of date".
    write_module_tree(&out_dir)?;

    Ok(())
}

#[derive(Default)]
struct Node {
    children: BTreeMap<String, Node>,
    file: Option<String>,
}

/// Build a nested `pub mod` tree over the per-package files tonic emitted.
///
/// tonic writes one `<proto.package>.rs` per package into `OUT_DIR`; this turns
/// `udb.core.authn.services.v1.rs` into `udb::core::authn::services::v1`.
fn write_module_tree(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let mut root = Node::default();
    for entry in std::fs::read_dir(out_dir)? {
        let path = entry?.path();
        let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
            continue;
        };
        let Some(pkg) = name.strip_suffix(".rs") else {
            continue;
        };
        // Our own output, and anything that is not a dotted proto package.
        if pkg == MODULE_TREE_STEM || !pkg.contains('.') {
            continue;
        }
        let mut node = &mut root;
        for seg in pkg.split('.') {
            node = node.children.entry(seg.to_string()).or_default();
        }
        node.file = Some(name.to_string());
    }

    let mut body = String::from("// @generated by build.rs from the emitted proto packages.\n");
    render(&root, &mut body)?;
    std::fs::write(out_dir.join(format!("{MODULE_TREE_STEM}.rs")), body)?;
    Ok(())
}

const MODULE_TREE_STEM: &str = "udb_modules";

fn render(node: &Node, out: &mut String) -> Result<(), std::fmt::Error> {
    if let Some(file) = &node.file {
        writeln!(out, r#"include!(concat!(env!("OUT_DIR"), "/{file}"));"#)?;
    }
    for (name, child) in &node.children {
        writeln!(out, "pub mod {name} {{")?;
        render(child, out)?;
        writeln!(out, "}}")?;
    }
    Ok(())
}

/// `(proto_root, googleapis_root)` — repo layout first, vendored copy second.
fn resolve_roots() -> Result<(PathBuf, PathBuf), Box<dyn std::error::Error>> {
    let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
    let candidates = [
        // Repo checkout: sdk/rust -> repo root.
        (
            manifest.join("../../proto"),
            manifest.join("../../third_party/googleapis"),
        ),
        // Published crate: vendored alongside the sources.
        (
            manifest.join("proto"),
            manifest.join("third_party/googleapis"),
        ),
    ];
    for (proto, google) in candidates {
        if proto.is_dir() && google.is_dir() {
            return Ok((normalize(&proto), normalize(&google)));
        }
    }
    Err(
        "could not locate the UDB protos: looked for ../../proto (repo checkout) and ./proto \
         (vendored). See README, \"Publishing\"."
            .into(),
    )
}

/// Collapse `..` so protoc include paths stay comparable across platforms.
///
/// On Windows `canonicalize` returns an extended-length path (the `\\?\` form).
/// protoc cannot resolve an import against an include root written that way and
/// reports a misleading "File not found" for a file that is plainly there, so the
/// prefix is stripped back off.
fn normalize(p: &Path) -> PathBuf {
    let Ok(canonical) = p.canonicalize() else {
        return p.to_path_buf();
    };
    let text = canonical.to_string_lossy().into_owned();
    if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
        return PathBuf::from(format!(r"\\{rest}"));
    }
    if let Some(rest) = text.strip_prefix(r"\\?\") {
        return PathBuf::from(rest);
    }
    canonical
}