use std::fs;
use std::path::PathBuf;
use tracing::debug;
use crate::output::errors::WriteError;
use crate::output::types::WriteResult;
use crate::types::ScannedModule;
pub struct RustWriter {
pub output_dir: PathBuf,
}
impl RustWriter {
pub fn new(output_dir: impl Into<PathBuf>) -> Self {
Self {
output_dir: output_dir.into(),
}
}
pub fn write(&self, modules: &[ScannedModule]) -> Result<Vec<WriteResult>, WriteError> {
let mut results: Vec<WriteResult> = Vec::new();
for module in modules {
let file_path = self.module_path(&module.module_id);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| WriteError::io(parent.display().to_string(), e))?;
}
let content = generate_stub(module);
fs::write(&file_path, content.as_bytes())
.map_err(|e| WriteError::io(file_path.display().to_string(), e))?;
debug!(file_path = %file_path.display(), "RustWriter: written");
results.push(WriteResult::with_path(
module.module_id.clone(),
file_path.display().to_string(),
));
}
Ok(results)
}
fn module_path(&self, module_id: &str) -> PathBuf {
let mut path = self.output_dir.clone();
let parts: Vec<&str> = module_id.split('.').collect();
if let Some((last, dirs)) = parts.split_last() {
for dir in dirs {
path.push(dir);
}
path.push(format!("{last}.rs"));
} else {
path.push(format!("{module_id}.rs"));
}
path
}
}
fn generate_stub(module: &ScannedModule) -> String {
let module_id = &module.module_id;
let description = module.description.replace('"', "\\\"");
format!(
"// Auto-generated by apcore-toolkit RustWriter\n\
// Module: {module_id}\n\
// Description: {description}\n\
// Do not edit manually unless you intend to customize the handler.\n\
\n\
use apcore::module;\n\
\n\
#[module(id = \"{module_id}\", description = \"{description}\")]\n\
pub async fn handler(\n\
inputs: serde_json::Value,\n\
) -> serde_json::Value {{\n\
todo!(\"Implement handler for {module_id}\")\n\
}}\n"
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
fn sample_module() -> ScannedModule {
ScannedModule::new(
"users.get_user".into(),
"Get a user by id".into(),
json!({"type": "object", "properties": {"id": {"type": "integer"}}}),
json!({"type": "object"}),
vec!["users".into()],
"myapp.views:get_user".into(),
)
}
#[test]
fn test_rust_writer_generates_file_with_module_id() {
let dir = TempDir::new().unwrap();
let writer = RustWriter::new(dir.path());
let modules = vec![sample_module()];
let results = writer.write(&modules).unwrap();
assert_eq!(results.len(), 1);
let path = results[0].path.as_deref().expect("path should be set");
assert!(
std::path::Path::new(path).exists(),
"file must exist: {path}"
);
let content = fs::read_to_string(path).unwrap();
assert!(
content.contains("users.get_user"),
"stub must contain module_id: {content}"
);
assert!(
content.contains("Auto-generated"),
"stub must contain header: {content}"
);
}
#[test]
fn test_rust_writer_creates_nested_directory() {
let dir = TempDir::new().unwrap();
let writer = RustWriter::new(dir.path());
let modules = vec![sample_module()];
let results = writer.write(&modules).unwrap();
let path = results[0].path.as_deref().unwrap();
assert!(
path.ends_with("users/get_user.rs"),
"unexpected path: {path}"
);
assert!(std::path::Path::new(path).exists());
}
#[test]
fn test_rust_writer_module_path_flat() {
let dir = TempDir::new().unwrap();
let writer = RustWriter::new(dir.path());
let module = ScannedModule::new(
"hello".into(),
"Simple module".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec![],
"app:hello".into(),
);
let results = writer.write(&[module]).unwrap();
let path = results[0].path.as_deref().unwrap();
assert!(path.ends_with("hello.rs"), "unexpected path: {path}");
}
#[test]
fn test_rust_writer_empty_modules() {
let dir = TempDir::new().unwrap();
let writer = RustWriter::new(dir.path());
let results = writer.write(&[]).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_rust_writer_multiple_modules() {
let dir = TempDir::new().unwrap();
let writer = RustWriter::new(dir.path());
let modules = vec![
sample_module(),
ScannedModule::new(
"tasks.list".into(),
"List tasks".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec![],
"app:list_tasks".into(),
),
];
let results = writer.write(&modules).unwrap();
assert_eq!(results.len(), 2);
for r in &results {
let path = r.path.as_deref().unwrap();
assert!(
std::path::Path::new(path).exists(),
"file must exist: {path}"
);
}
}
}