Skip to main content

zlink_codegen/
lib.rs

1#![doc(
2    html_logo_url = "https://raw.githubusercontent.com/z-galaxy/zlink/3660d731d7de8f60c8d82e122b3ece15617185e4/data/logo.png"
3)]
4//! Code generation for Varlink interfaces.
5
6use std::{fs, path::PathBuf};
7use zlink::idl::Interface;
8
9#[cfg(doctest)]
10mod doctests {
11    // Book markdown checks. The other chapters are checked from the `zlink` crate; this one is
12    // checked from here because its code-generation examples depend on this crate, which `zlink`
13    // can't (dev-)depend on without creating a dependency cycle.
14    doc_comment::doctest!("../../book/src/introspection.md");
15}
16
17mod codegen;
18pub use codegen::CodeGenerator;
19mod error;
20pub use self::error::Error;
21
22/// Generate Rust code from a Varlink interface.
23///
24/// # Errors
25///
26/// - [`Error::Fmt`] - Writing to the internal output buffer failed.
27pub fn generate_interface(interface: &Interface<'_>) -> Result<String, Error> {
28    let mut generator = CodeGenerator::new();
29    generator.generate_interface(interface, false)?;
30    Ok(generator.output())
31}
32
33/// Generate Rust code from multiple Varlink interfaces.
34///
35/// # Errors
36///
37/// - [`Error::Fmt`] - Writing to the internal output buffer failed.
38pub fn generate_interfaces(interfaces: &[Interface<'_>]) -> Result<String, Error> {
39    let mut generator = CodeGenerator::new();
40
41    // Add module-level header for multiple interfaces.
42    if interfaces.len() > 1 {
43        generator.write_module_header()?;
44    }
45
46    for interface in interfaces {
47        // Skip module header for all interfaces when generating multiple.
48        let skip_header = interfaces.len() > 1;
49        generator.generate_interface(interface, skip_header)?;
50    }
51    Ok(generator.output())
52}
53
54/// Format generated Rust code using rustfmt.
55///
56/// # Errors
57///
58/// - [`Error::Io`] - Spawning or communicating with the `rustfmt` process failed.
59/// - [`Error::InvalidUtf8`] - `rustfmt` produced output that was not valid UTF-8.
60pub fn format_code(code: &str) -> Result<String, Error> {
61    use std::{
62        io::Write,
63        process::{Command, Stdio},
64    };
65
66    let mut child = Command::new("rustfmt")
67        .arg("--edition=2021")
68        .stdin(Stdio::piped())
69        .stdout(Stdio::piped())
70        .stderr(Stdio::piped())
71        .spawn()?;
72
73    if let Some(mut stdin) = child.stdin.take() {
74        stdin.write_all(code.as_bytes())?;
75    }
76
77    let output = child.wait_with_output()?;
78
79    if !output.status.success() {
80        // If rustfmt fails, return the original code.
81        eprintln!(
82            "Warning: rustfmt failed: {}",
83            String::from_utf8_lossy(&output.stderr),
84        );
85        return Ok(code.to_string());
86    }
87
88    String::from_utf8(output.stdout).map_err(Error::from)
89}
90
91/// Configuration options for Varlink code generation.
92///
93/// See [`generate_files`] for an end-to-end usage example.
94#[derive(Default)]
95pub struct CodegenOptions {
96    /// Input Varlink IDL file(s).
97    pub files: Vec<PathBuf>,
98    /// Output file path (defaults to stdout if not specified).
99    pub output: Option<PathBuf>,
100    /// Generate separate files for each interface (ignored if output is specified).
101    pub multiple_files: bool,
102    /// Whether to format the generated Rust code using `rustfmt`.
103    ///
104    /// Default value: false
105    pub rustfmt: bool,
106}
107
108/// Generate Rust source files from Varlink interface files.
109///
110/// This function reads Varlink interface definition files from `config.files`, parses them,
111/// generates corresponding Rust code, and handles output based on the configuration.
112///
113/// # Behavior
114///
115/// - If `config.output` is specified: Generates a single file with all interfaces combined.
116/// - If `config.multiple_files` is true: Generates separate `.rs` files for each interface in the
117///   current working directory. The output filename is derived from the interface name (dots
118///   replaced with underscores and converted to lowercase).
119/// - Otherwise: Writes the generated code to stdout.
120///
121/// # Arguments
122///
123/// * `config` - Code generation options containing input files, output configuration, and
124///   formatting options.
125///
126/// # Returns
127///
128/// Returns `Ok(())` on successful code generation and output.
129///
130/// # Errors
131///
132/// This function will return an error if:
133/// - [`Error::InvalidArgument`] - No input files were specified.
134/// - [`Error::Io`] - File I/O failed (reading input, writing output or invoking `rustfmt`).
135/// - [`Error::Zlink`] - A Varlink interface definition is malformed or invalid.
136/// - [`Error::Fmt`] - Writing to the internal output buffer failed.
137/// - [`Error::InvalidUtf8`] - `rustfmt` produced output that was not valid UTF-8.
138///
139/// # Examples
140///
141/// ```no_run
142/// use std::path::PathBuf;
143/// use zlink_codegen::CodegenOptions;
144///
145/// let config = CodegenOptions {
146///     files: vec![PathBuf::from("interface.varlink")],
147///     output: Some(PathBuf::from("generated.rs")),
148///     rustfmt: true,
149///     ..Default::default()
150/// };
151/// zlink_codegen::generate_files(&config).expect("Failed to generate code");
152/// ```
153pub fn generate_files(config: &CodegenOptions) -> Result<(), Error> {
154    use std::io::Write;
155
156    if config.files.is_empty() {
157        return Err(Error::InvalidArgument);
158    }
159
160    // Read and parse all interface files
161    let mut file_contents = Vec::new();
162    for interface_file in &config.files {
163        let content = fs::read_to_string(interface_file)?;
164        file_contents.push(content);
165    }
166
167    let mut interfaces = Vec::new();
168    for content in &file_contents {
169        let interface = Interface::try_from(content.as_str()).map_err(zlink::Error::from)?;
170        interfaces.push(interface);
171    }
172
173    // Determine output mode
174    if let Some(output_path) = &config.output {
175        // Single output file mode
176        let code = if interfaces.len() == 1 {
177            generate_interface(&interfaces[0])?
178        } else {
179            generate_interfaces(&interfaces)?
180        };
181
182        let output = if config.rustfmt {
183            format_code(&code)?
184        } else {
185            code
186        };
187
188        fs::write(output_path, output)?;
189    } else if config.multiple_files {
190        // Multiple output files mode - generate separate file for each interface
191        for interface in &interfaces {
192            let code = generate_interface(interface)?;
193
194            let output = if config.rustfmt {
195                format_code(&code)?
196            } else {
197                code
198            };
199
200            // Generate output filename from interface name
201            let filename = interface_to_filename(interface.name());
202            let output_path = PathBuf::from(filename);
203
204            fs::write(&output_path, output)?;
205        }
206    } else {
207        // Stdout mode
208        let code = if interfaces.len() == 1 {
209            generate_interface(&interfaces[0])?
210        } else {
211            generate_interfaces(&interfaces)?
212        };
213
214        let output = if config.rustfmt {
215            format_code(&code)?
216        } else {
217            code
218        };
219
220        std::io::stdout().write_all(output.as_bytes())?;
221    }
222
223    Ok(())
224}
225
226/// Convert an interface name to a filename.
227///
228/// The filename is converted to lowercase to comply with Rust's naming conventions.
229///
230/// For example: `"org.example.Interface"` → `"org_example_interface.rs"`
231fn interface_to_filename(interface_name: &str) -> String {
232    format!("{}.rs", interface_name.replace('.', "_").to_lowercase())
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_interface_to_filename() {
241        assert_eq!(
242            interface_to_filename("org.example.Interface"),
243            "org_example_interface.rs"
244        );
245        assert_eq!(
246            interface_to_filename("com.example.MyService"),
247            "com_example_myservice.rs"
248        );
249        assert_eq!(
250            interface_to_filename("SimpleInterface"),
251            "simpleinterface.rs"
252        );
253        assert_eq!(
254            interface_to_filename("org.varlink.service"),
255            "org_varlink_service.rs"
256        );
257    }
258}