Skip to main content

panproto_protocols/
emit.rs

1//! Shared emit helpers for protocol serialization.
2//!
3//! These utilities help protocol modules convert a [`Schema`](panproto_schema::Schema) back into
4//! native format text. They provide common operations like finding root
5//! vertices, walking edges, and generating indented output.
6
7use panproto_schema::{Constraint, Edge, Schema, Vertex};
8
9/// Find root vertices (vertices with no incoming edges of the given structural kinds).
10///
11/// Returns vertices sorted by ID for deterministic output.
12#[must_use]
13pub fn find_roots<'a>(schema: &'a Schema, structural_edge_kinds: &[&str]) -> Vec<&'a Vertex> {
14    let mut roots: Vec<&Vertex> = schema
15        .vertices
16        .values()
17        .filter(|v| {
18            let incoming = schema.incoming_edges(&v.id);
19            !incoming
20                .iter()
21                .any(|e| structural_edge_kinds.contains(&&*e.kind))
22        })
23        .collect();
24    roots.sort_by(|a, b| a.id.cmp(&b.id));
25    roots
26}
27
28/// Get children of a vertex via a specific edge kind, sorted by edge name.
29///
30/// Returns pairs of (edge, target vertex) for each outgoing edge of the
31/// specified kind.
32#[must_use]
33pub fn children_by_edge<'a>(
34    schema: &'a Schema,
35    parent: &str,
36    edge_kind: &str,
37) -> Vec<(&'a Edge, &'a Vertex)> {
38    let mut children: Vec<(&Edge, &Vertex)> = schema
39        .outgoing_edges(parent)
40        .iter()
41        .filter(|e| e.kind == edge_kind)
42        .filter_map(|e| schema.vertices.get(&e.tgt).map(|v| (e, v)))
43        .collect();
44    children.sort_by(|a, b| {
45        let a_name = a.0.name.as_deref().unwrap_or("");
46        let b_name = b.0.name.as_deref().unwrap_or("");
47        a_name.cmp(b_name)
48    });
49    children
50}
51
52/// Get a constraint value by sort for a vertex.
53#[must_use]
54pub fn constraint_value<'a>(schema: &'a Schema, vertex_id: &str, sort: &str) -> Option<&'a str> {
55    schema
56        .constraints
57        .get(vertex_id)?
58        .iter()
59        .find(|c| c.sort == sort)
60        .map(|c| c.value.as_str())
61}
62
63/// Get all constraints for a vertex.
64#[must_use]
65pub fn vertex_constraints<'a>(schema: &'a Schema, vertex_id: &str) -> Vec<&'a Constraint> {
66    schema
67        .constraints
68        .get(vertex_id)
69        .map(|cs| cs.iter().collect())
70        .unwrap_or_default()
71}
72
73/// An indented text writer for emitting nested format text.
74///
75/// Provides a simple API for building indented, line-oriented output
76/// such as `.proto` files, GraphQL SDL, or TypeScript declarations.
77pub struct IndentWriter {
78    buf: String,
79    level: usize,
80    indent_str: &'static str,
81}
82
83impl IndentWriter {
84    /// Create a new `IndentWriter` with the given indentation string.
85    ///
86    /// Common values: `"  "` (2 spaces), `"    "` (4 spaces), `"\t"`.
87    #[must_use]
88    pub const fn new(indent_str: &'static str) -> Self {
89        Self {
90            buf: String::new(),
91            level: 0,
92            indent_str,
93        }
94    }
95
96    /// Increase the indentation level by one.
97    pub const fn indent(&mut self) {
98        self.level += 1;
99    }
100
101    /// Decrease the indentation level by one.
102    pub const fn dedent(&mut self) {
103        self.level = self.level.saturating_sub(1);
104    }
105
106    /// Write a line at the current indentation level.
107    pub fn line(&mut self, s: &str) {
108        for _ in 0..self.level {
109            self.buf.push_str(self.indent_str);
110        }
111        self.buf.push_str(s);
112        self.buf.push('\n');
113    }
114
115    /// Write a blank line.
116    pub fn blank(&mut self) {
117        self.buf.push('\n');
118    }
119
120    /// Write raw text without indentation.
121    pub fn raw(&mut self, s: &str) {
122        self.buf.push_str(s);
123    }
124
125    /// Consume the writer and return the built string.
126    #[must_use]
127    pub fn finish(self) -> String {
128        self.buf
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn indent_writer_basic() {
138        let mut w = IndentWriter::new("  ");
139        w.line("message Foo {");
140        w.indent();
141        w.line("string bar = 1;");
142        w.dedent();
143        w.line("}");
144        let result = w.finish();
145        assert_eq!(result, "message Foo {\n  string bar = 1;\n}\n");
146    }
147}