1use std::path::Path;
6
7pub struct ProjectSkeleton {
9 pub name: String,
10 pub files: Vec<(String, String)>,
11}
12
13impl ProjectSkeleton {
14 pub fn write_to(&self, root: &Path) -> std::io::Result<()> {
16 for (rel_path, content) in &self.files {
17 let full = root.join(rel_path);
18 if let Some(parent) = full.parent() {
19 std::fs::create_dir_all(parent)?;
20 }
21 std::fs::write(&full, content)?;
22 }
23 Ok(())
24 }
25}
26
27pub fn rust_binary_skeleton(name: &str) -> ProjectSkeleton {
29 let cargo_toml = format!(
30 r#"[package]
31name = "{name}"
32version = "0.1.0"
33edition = "2021"
34rust-version = "1.75"
35"#
36 );
37 let main_rs = "fn main() {\n println!(\"Hello, world!\");\n}\n".to_string();
38
39 ProjectSkeleton {
40 name: name.to_string(),
41 files: vec![
42 ("Cargo.toml".into(), cargo_toml),
43 ("src/main.rs".into(), main_rs),
44 ],
45 }
46}
47
48pub fn rust_library_skeleton(name: &str) -> ProjectSkeleton {
50 let cargo_toml = format!(
51 r#"[package]
52name = "{name}"
53version = "0.1.0"
54edition = "2021"
55rust-version = "1.75"
56"#
57 );
58 let lib_rs = "/// Add two numbers.\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn it_works() {\n assert_eq!(add(2, 2), 4);\n }\n}\n".to_string();
59
60 ProjectSkeleton {
61 name: name.to_string(),
62 files: vec![
63 ("Cargo.toml".into(), cargo_toml),
64 ("src/lib.rs".into(), lib_rs),
65 ],
66 }
67}
68
69pub fn pawan_agent_skeleton(name: &str) -> ProjectSkeleton {
71 let cargo_toml = format!(
72 r#"[package]
73name = "{name}"
74version = "0.1.0"
75edition = "2021"
76rust-version = "1.75"
77
78[dependencies]
79pawan = {{ git = "https://github.com/dirmacs/pawan.git" }}
80"#
81 );
82 let main_rs = format!(
83 r#"fn main() {{
84 println!("Pawan agent: {name}");
85}}
86"#
87 );
88 let pawan_toml = format!(
89 r#"[agent]
90name = "{name}"
91model = "qwen/qwen3.5-122b-a10b"
92
93[provider]
94name = "nvidia"
95api_url = "https://integrate.api.nvidia.com/v1"
96"#
97 );
98
99 ProjectSkeleton {
100 name: name.to_string(),
101 files: vec![
102 ("Cargo.toml".into(), cargo_toml),
103 ("src/main.rs".into(), main_rs),
104 ("pawan.toml".into(), pawan_toml),
105 ],
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::fs;
113
114 #[test]
115 fn binary_skeleton_files() {
116 let sk = rust_binary_skeleton("myapp");
117 assert_eq!(sk.name, "myapp");
118 let paths: Vec<&str> = sk.files.iter().map(|(p, _)| p.as_str()).collect();
119 assert_eq!(paths, vec!["Cargo.toml", "src/main.rs"]);
120 }
121
122 #[test]
123 fn library_skeleton_files() {
124 let sk = rust_library_skeleton("mylib");
125 assert_eq!(sk.name, "mylib");
126 let paths: Vec<&str> = sk.files.iter().map(|(p, _)| p.as_str()).collect();
127 assert_eq!(paths, vec!["Cargo.toml", "src/lib.rs"]);
128 }
129
130 #[test]
131 fn agent_skeleton_files() {
132 let sk = pawan_agent_skeleton("myagent");
133 assert_eq!(sk.name, "myagent");
134 let paths: Vec<&str> = sk.files.iter().map(|(p, _)| p.as_str()).collect();
135 assert_eq!(paths, vec!["Cargo.toml", "src/main.rs", "pawan.toml"]);
136 }
137
138 #[test]
139 fn write_to_creates_files() {
140 let dir = tempfile::tempdir().unwrap();
141 let sk = rust_binary_skeleton("testproj");
142 sk.write_to(dir.path()).unwrap();
143
144 let cargo = fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
145 assert!(cargo.contains("name = \"testproj\""));
146 assert!(cargo.contains("edition = \"2021\""));
147 assert!(cargo.contains("rust-version = \"1.75\""));
148
149 let main = fs::read_to_string(dir.path().join("src/main.rs")).unwrap();
150 assert!(main.contains("fn main()"));
151 }
152
153 #[test]
154 fn cargo_toml_is_valid_toml() {
155 for sk in [
156 rust_binary_skeleton("a"),
157 rust_library_skeleton("b"),
158 pawan_agent_skeleton("c"),
159 ] {
160 let cargo_content = &sk.files.iter().find(|(p, _)| p == "Cargo.toml").unwrap().1;
161 let parsed: Result<toml::Value, _> = toml::from_str(cargo_content);
162 assert!(parsed.is_ok(), "Invalid TOML in {} skeleton", sk.name);
163 }
164 }
165
166 #[test]
167 fn pawan_agent_skeleton_includes_pawan_toml_in_file_list() {
168 let sk = pawan_agent_skeleton("demo");
169 let has_pawan_toml = sk.files.iter().any(|(p, _)| p == "pawan.toml");
170 assert!(
171 has_pawan_toml,
172 "pawan_agent_skeleton must include pawan.toml"
173 );
174 }
175
176 #[test]
177 fn write_to_creates_nested_directories() {
178 let dir = tempfile::tempdir().unwrap();
179 let sk = rust_binary_skeleton("nested");
180 sk.write_to(dir.path()).unwrap();
181 assert!(
183 dir.path().join("src").is_dir(),
184 "src/ directory not created"
185 );
186 assert!(dir.path().join("src/main.rs").is_file());
187 }
188
189 #[test]
190 fn skeleton_names_set_correctly() {
191 assert_eq!(rust_binary_skeleton("alpha").name, "alpha");
192 assert_eq!(rust_library_skeleton("beta").name, "beta");
193 assert_eq!(pawan_agent_skeleton("gamma").name, "gamma");
194 }
195
196 #[test]
197 fn generated_cargo_toml_contains_edition_and_rust_version() {
198 for (sk, expected_name) in [
199 (rust_binary_skeleton("x"), "x"),
200 (rust_library_skeleton("y"), "y"),
201 (pawan_agent_skeleton("z"), "z"),
202 ] {
203 let cargo = &sk.files.iter().find(|(p, _)| p == "Cargo.toml").unwrap().1;
204 assert!(
205 cargo.contains("edition = \"2021\""),
206 "{expected_name} missing edition"
207 );
208 assert!(
209 cargo.contains("rust-version = \"1.75\""),
210 "{expected_name} missing rust-version"
211 );
212 }
213 }
214
215 #[test]
216 fn agent_skeleton_has_pawan_toml() {
217 let dir = tempfile::tempdir().unwrap();
218 let sk = pawan_agent_skeleton("agent1");
219 sk.write_to(dir.path()).unwrap();
220
221 let pawan_cfg = fs::read_to_string(dir.path().join("pawan.toml")).unwrap();
222 assert!(pawan_cfg.contains("name = \"agent1\""));
223 assert!(pawan_cfg.contains("model ="));
224 }
225}