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}