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