Skip to main content

robonix_codegen/codegen/
contract_proto_modules_gen.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2// Emit `contract_proto_modules.rs` for Rust crates that `tonic::include_proto!` the whole
3// `robonix_contracts.proto` closure. Module order follows proto `import` dependencies (deps first).
4
5use anyhow::{Context, Result};
6use std::collections::BTreeSet;
7use std::fmt::Write as _;
8use std::fs;
9use std::path::Path;
10
11/// Parse `import "pkg/foo.proto";` lines; skip `google/` well-known types.
12fn parse_proto_imports(path: &Path) -> Result<Vec<String>> {
13    let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
14    let mut v = Vec::new();
15    for line in text.lines() {
16        let line = line.trim();
17        let Some(rest) = line.strip_prefix("import ") else {
18            continue;
19        };
20        let rest = rest.trim();
21        let Some(rest) = rest.strip_prefix('"') else {
22            continue;
23        };
24        let path_part = rest.split('"').next().unwrap_or("").trim();
25        if path_part.starts_with("google/") {
26            continue;
27        }
28        if path_part.ends_with(".proto") {
29            v.push(path_part.to_string());
30        }
31    }
32    Ok(v)
33}
34
35/// `package robonix.foo;` → `robonix.foo`
36fn parse_proto_package(path: &Path) -> Result<Option<String>> {
37    let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
38    for line in text.lines() {
39        let line = line.trim();
40        if line.starts_with("//") || line.is_empty() {
41            continue;
42        }
43        let Some(rest) = line.strip_prefix("package ") else {
44            continue;
45        };
46        let pkg = rest.trim_end_matches(';').trim();
47        if !pkg.is_empty() {
48            return Ok(Some(pkg.to_string()));
49        }
50    }
51    Ok(None)
52}
53
54/// `robonix.prm_base` → Rust sibling module name `prm_base` (last segment).
55fn package_to_rust_module(dot_pkg: &str) -> String {
56    dot_pkg
57        .rsplit_once('.')
58        .map(|(_, last)| last.to_string())
59        .unwrap_or_else(|| dot_pkg.to_string())
60}
61
62fn dfs_postorder(
63    out_dir: &Path,
64    file: &str,
65    visiting: &mut BTreeSet<String>,
66    visited: &mut BTreeSet<String>,
67    order: &mut Vec<String>,
68) -> Result<()> {
69    if visited.contains(file) {
70        return Ok(());
71    }
72    if visiting.contains(file) {
73        return Ok(()); // import cycle — skip re-entry
74    }
75    let path = out_dir.join(file);
76    if !path.is_file() {
77        return Ok(());
78    }
79
80    visiting.insert(file.to_string());
81    for imp in parse_proto_imports(&path)? {
82        dfs_postorder(out_dir, &imp, visiting, visited, order)?;
83    }
84    visiting.remove(file);
85    visited.insert(file.to_string());
86    order.push(file.to_string());
87    Ok(())
88}
89
90/// After `robonix_contracts.proto` is written, emit sibling `contract_proto_modules.rs`.
91pub fn write(out_dir: &Path, verbose: bool) -> Result<()> {
92    let root = "robonix_contracts.proto";
93    let contracts_path = out_dir.join(root);
94    if !contracts_path.is_file() {
95        return Ok(());
96    }
97
98    let mut visiting = BTreeSet::new();
99    let mut visited = BTreeSet::new();
100    let mut order = Vec::new();
101    dfs_postorder(out_dir, root, &mut visiting, &mut visited, &mut order)?;
102
103    let mut out = String::new();
104    writeln!(
105        &mut out,
106        "// @generated by robonix-codegen (--contracts). Source: proto import closure for `{}`.",
107        root
108    )?;
109    writeln!(&mut out, "// Do not edit by hand.")?;
110    writeln!(
111        &mut out,
112        "// `tonic::include_proto!` order matches prost `super::` sibling resolution."
113    )?;
114    writeln!(
115        &mut out,
116        "// Most generated types are unused in any single binary."
117    )?;
118    writeln!(
119        &mut out,
120        "// (Suppress dead_code on the `robonix-interfaces` crate — not `#![allow]` here: included via `include!`.)"
121    )?;
122    writeln!(&mut out)?;
123
124    for proto_file in &order {
125        let path = out_dir.join(proto_file);
126        let Some(dot_pkg) = parse_proto_package(&path)? else {
127            continue;
128        };
129        let rust_mod = package_to_rust_module(&dot_pkg);
130        writeln!(&mut out, "pub mod {rust_mod} {{")?;
131        writeln!(&mut out, "    tonic::include_proto!(\"{dot_pkg}\");")?;
132        writeln!(&mut out, "}}")?;
133        writeln!(&mut out)?;
134    }
135
136    let outfile = out_dir.join("contract_proto_modules.rs");
137    fs::write(&outfile, &out).with_context(|| format!("write {}", outfile.display()))?;
138    if verbose {
139        eprintln!(
140            "[robonix-codegen] contracts: wrote {} ({} include_proto modules)",
141            outfile.display(),
142            order.len()
143        );
144    }
145    Ok(())
146}