breathe-api-server 0.1.29

breathe's REST surface (axum) over the BreatheStore facade — the same typed core the MCP drives. Routes for bands + nodepools + catalog mirror spec/breathe.openapi.yaml (the source of truth); handlers are kube-rs facade calls (the design's legit hand-serve).
Documentation
//! Compile the gRPC proto with serde-capable messages (pbjson). Prefer a system
//! `protoc` (nix nativeBuildInput = protobuf); fall back to the vendored binary
//! for local dev builds.
//!
//! `proto/breathe.proto` is GENERATED by grpc-forge from `spec/breathe.openapi.yaml`
//! (the source of truth) — `grpc-forge proto --spec ... --package breathe.v1`.
//! Do not hand-edit it; regenerate when the spec changes.
//!
//! Path resolution must work under BOTH build models:
//!   - cargo dev: CWD / CARGO_MANIFEST_DIR == the crate dir, so `proto/...` works.
//!   - gen / substrate lockfile-builder: the crate builds with `src = workspaceSrc`
//!     (the whole workspace), so the proto lives at
//!     `breathe-api-server/proto/breathe.proto`. Probe candidate roots.
use std::path::PathBuf;

fn main() {
    if std::env::var_os("PROTOC").is_none() && which::which("protoc").is_err() {
        if let Ok(p) = protoc_bin_vendored::protoc_bin_path() {
            // SAFETY: build scripts are single-threaded; setting PROTOC for tonic-build.
            unsafe {
                std::env::set_var("PROTOC", p);
            }
        }
    }

    let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
    let candidates = [
        PathBuf::from(&manifest).join("proto"), // cargo: crate dir
        PathBuf::from(&manifest).join("breathe-api-server").join("proto"), // gen: workspace root
        PathBuf::from("proto"),                 // fallback: CWD == crate dir
        PathBuf::from("breathe-api-server").join("proto"), // fallback: CWD == workspace root
    ];
    let proto_dir = candidates
        .iter()
        .find(|p| p.join("breathe.proto").is_file())
        .expect("breathe-api-server build.rs: could not locate proto/breathe.proto");
    let proto_file = proto_dir.join("breathe.proto");

    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
    let descriptor_path = out_dir.join("proto_descriptor.bin");

    // `compile_well_known_types()` removes prost's default `.google.protobuf →
    // ::prost_types` extern so the pbjson-types extern is the only one
    // (otherwise: "duplicate extern Protobuf path .google.protobuf").
    let mut config = prost_build::Config::new();
    config.compile_well_known_types();
    config.extern_path(".google.protobuf", "::pbjson_types");
    config.file_descriptor_set_path(&descriptor_path);

    tonic_build::configure()
        .compile_protos_with_config(config, &[proto_file.clone()], &[proto_dir.clone()])
        .expect("compile proto/breathe.proto");

    let descriptor_set = std::fs::read(&descriptor_path).expect("read descriptor set");
    pbjson_build::Builder::new()
        .register_descriptors(&descriptor_set)
        .expect("register descriptors")
        .build(&[".breathe.v1"])
        .expect("build pbjson serde impls");

    println!("cargo:rerun-if-changed={}", proto_file.display());
}