#![doc(
html_logo_url = "https://raw.githubusercontent.com/z-galaxy/zlink/3660d731d7de8f60c8d82e122b3ece15617185e4/data/logo.png"
)]
use std::{fs, path::PathBuf};
use zlink::idl::Interface;
#[cfg(doctest)]
mod doctests {
doc_comment::doctest!("../../book/src/introspection.md");
}
mod codegen;
pub use codegen::CodeGenerator;
mod error;
pub use self::error::Error;
pub fn generate_interface(interface: &Interface<'_>) -> Result<String, Error> {
let mut generator = CodeGenerator::new();
generator.generate_interface(interface, false)?;
Ok(generator.output())
}
pub fn generate_interfaces(interfaces: &[Interface<'_>]) -> Result<String, Error> {
let mut generator = CodeGenerator::new();
if interfaces.len() > 1 {
generator.write_module_header()?;
}
for interface in interfaces {
let skip_header = interfaces.len() > 1;
generator.generate_interface(interface, skip_header)?;
}
Ok(generator.output())
}
pub fn format_code(code: &str) -> Result<String, Error> {
use std::{
io::Write,
process::{Command, Stdio},
};
let mut child = Command::new("rustfmt")
.arg("--edition=2021")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(code.as_bytes())?;
}
let output = child.wait_with_output()?;
if !output.status.success() {
eprintln!(
"Warning: rustfmt failed: {}",
String::from_utf8_lossy(&output.stderr),
);
return Ok(code.to_string());
}
String::from_utf8(output.stdout).map_err(Error::from)
}
#[derive(Default)]
pub struct CodegenOptions {
pub files: Vec<PathBuf>,
pub output: Option<PathBuf>,
pub multiple_files: bool,
pub rustfmt: bool,
}
pub fn generate_files(config: &CodegenOptions) -> Result<(), Error> {
use std::io::Write;
if config.files.is_empty() {
return Err(Error::InvalidArgument);
}
let mut file_contents = Vec::new();
for interface_file in &config.files {
let content = fs::read_to_string(interface_file)?;
file_contents.push(content);
}
let mut interfaces = Vec::new();
for content in &file_contents {
let interface = Interface::try_from(content.as_str()).map_err(zlink::Error::from)?;
interfaces.push(interface);
}
if let Some(output_path) = &config.output {
let code = if interfaces.len() == 1 {
generate_interface(&interfaces[0])?
} else {
generate_interfaces(&interfaces)?
};
let output = if config.rustfmt {
format_code(&code)?
} else {
code
};
fs::write(output_path, output)?;
} else if config.multiple_files {
for interface in &interfaces {
let code = generate_interface(interface)?;
let output = if config.rustfmt {
format_code(&code)?
} else {
code
};
let filename = interface_to_filename(interface.name());
let output_path = PathBuf::from(filename);
fs::write(&output_path, output)?;
}
} else {
let code = if interfaces.len() == 1 {
generate_interface(&interfaces[0])?
} else {
generate_interfaces(&interfaces)?
};
let output = if config.rustfmt {
format_code(&code)?
} else {
code
};
std::io::stdout().write_all(output.as_bytes())?;
}
Ok(())
}
fn interface_to_filename(interface_name: &str) -> String {
format!("{}.rs", interface_name.replace('.', "_").to_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interface_to_filename() {
assert_eq!(
interface_to_filename("org.example.Interface"),
"org_example_interface.rs"
);
assert_eq!(
interface_to_filename("com.example.MyService"),
"com_example_myservice.rs"
);
assert_eq!(
interface_to_filename("SimpleInterface"),
"simpleinterface.rs"
);
assert_eq!(
interface_to_filename("org.varlink.service"),
"org_varlink_service.rs"
);
}
}