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}