apcore_toolkit/output/
rust_writer.rs1use std::fs;
9use std::path::PathBuf;
10
11use tracing::debug;
12
13use crate::output::errors::WriteError;
14use crate::output::types::WriteResult;
15use crate::types::ScannedModule;
16
17pub struct RustWriter {
27 pub output_dir: PathBuf,
29}
30
31impl RustWriter {
32 pub fn new(output_dir: impl Into<PathBuf>) -> Self {
34 Self {
35 output_dir: output_dir.into(),
36 }
37 }
38
39 pub fn write(&self, modules: &[ScannedModule]) -> Result<Vec<WriteResult>, WriteError> {
44 let mut results: Vec<WriteResult> = Vec::new();
45
46 for module in modules {
47 let file_path = self.module_path(&module.module_id);
48
49 if let Some(parent) = file_path.parent() {
51 fs::create_dir_all(parent)
52 .map_err(|e| WriteError::io(parent.display().to_string(), e))?;
53 }
54
55 let content = generate_stub(module);
56 fs::write(&file_path, content.as_bytes())
57 .map_err(|e| WriteError::io(file_path.display().to_string(), e))?;
58
59 debug!(file_path = %file_path.display(), "RustWriter: written");
60 results.push(WriteResult::with_path(
61 module.module_id.clone(),
62 file_path.display().to_string(),
63 ));
64 }
65
66 Ok(results)
67 }
68
69 fn module_path(&self, module_id: &str) -> PathBuf {
73 let mut path = self.output_dir.clone();
74 let parts: Vec<&str> = module_id.split('.').collect();
75 if let Some((last, dirs)) = parts.split_last() {
77 for dir in dirs {
78 path.push(dir);
79 }
80 path.push(format!("{last}.rs"));
81 } else {
82 path.push(format!("{module_id}.rs"));
83 }
84 path
85 }
86}
87
88fn generate_stub(module: &ScannedModule) -> String {
90 let module_id = &module.module_id;
91 let description = module.description.replace('"', "\\\"");
92
93 format!(
94 "// Auto-generated by apcore-toolkit RustWriter\n\
95 // Module: {module_id}\n\
96 // Description: {description}\n\
97 // Do not edit manually unless you intend to customize the handler.\n\
98 \n\
99 use apcore::module;\n\
100 \n\
101 #[module(id = \"{module_id}\", description = \"{description}\")]\n\
102 pub async fn handler(\n\
103 inputs: serde_json::Value,\n\
104 ) -> serde_json::Value {{\n\
105 todo!(\"Implement handler for {module_id}\")\n\
106 }}\n"
107 )
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use serde_json::json;
114 use tempfile::TempDir;
115
116 fn sample_module() -> ScannedModule {
117 ScannedModule::new(
118 "users.get_user".into(),
119 "Get a user by id".into(),
120 json!({"type": "object", "properties": {"id": {"type": "integer"}}}),
121 json!({"type": "object"}),
122 vec!["users".into()],
123 "myapp.views:get_user".into(),
124 )
125 }
126
127 #[test]
128 fn test_rust_writer_generates_file_with_module_id() {
129 let dir = TempDir::new().unwrap();
130 let writer = RustWriter::new(dir.path());
131 let modules = vec![sample_module()];
132 let results = writer.write(&modules).unwrap();
133
134 assert_eq!(results.len(), 1);
135 let path = results[0].path.as_deref().expect("path should be set");
136 assert!(
137 std::path::Path::new(path).exists(),
138 "file must exist: {path}"
139 );
140
141 let content = fs::read_to_string(path).unwrap();
142 assert!(
143 content.contains("users.get_user"),
144 "stub must contain module_id: {content}"
145 );
146 assert!(
147 content.contains("Auto-generated"),
148 "stub must contain header: {content}"
149 );
150 }
151
152 #[test]
153 fn test_rust_writer_creates_nested_directory() {
154 let dir = TempDir::new().unwrap();
156 let writer = RustWriter::new(dir.path());
157 let modules = vec![sample_module()];
158 let results = writer.write(&modules).unwrap();
159
160 let path = results[0].path.as_deref().unwrap();
161 assert!(
163 path.ends_with("users/get_user.rs"),
164 "unexpected path: {path}"
165 );
166 assert!(std::path::Path::new(path).exists());
167 }
168
169 #[test]
170 fn test_rust_writer_module_path_flat() {
171 let dir = TempDir::new().unwrap();
173 let writer = RustWriter::new(dir.path());
174 let module = ScannedModule::new(
175 "hello".into(),
176 "Simple module".into(),
177 json!({"type": "object"}),
178 json!({"type": "object"}),
179 vec![],
180 "app:hello".into(),
181 );
182 let results = writer.write(&[module]).unwrap();
183 let path = results[0].path.as_deref().unwrap();
184 assert!(path.ends_with("hello.rs"), "unexpected path: {path}");
185 }
186
187 #[test]
188 fn test_rust_writer_empty_modules() {
189 let dir = TempDir::new().unwrap();
190 let writer = RustWriter::new(dir.path());
191 let results = writer.write(&[]).unwrap();
192 assert!(results.is_empty());
193 }
194
195 #[test]
196 fn test_rust_writer_multiple_modules() {
197 let dir = TempDir::new().unwrap();
198 let writer = RustWriter::new(dir.path());
199 let modules = vec![
200 sample_module(),
201 ScannedModule::new(
202 "tasks.list".into(),
203 "List tasks".into(),
204 json!({"type": "object"}),
205 json!({"type": "object"}),
206 vec![],
207 "app:list_tasks".into(),
208 ),
209 ];
210 let results = writer.write(&modules).unwrap();
211 assert_eq!(results.len(), 2);
212 for r in &results {
213 let path = r.path.as_deref().unwrap();
214 assert!(
215 std::path::Path::new(path).exists(),
216 "file must exist: {path}"
217 );
218 }
219 }
220}