1mod error;
13
14use crate::error::RextCoreError;
15use std::fs::{self, File};
16use std::io::{BufRead, BufReader, Write};
17use std::process::Command;
18
19pub const TYPES_TO_WRAP: [&str; 2] = ["Uuid", "DateTimeWithTimeZone"];
21
22pub const ENTITIES_DIR: &str = "backend/entity/models";
24
25pub struct ServerConfig {
27 pub host: [u8; 4],
28 pub port: u16,
29}
30
31impl Default for ServerConfig {
32 fn default() -> Self {
33 Self {
34 host: [0, 0, 0, 0],
35 port: 3000,
36 }
37 }
38}
39
40pub fn check_for_rext_app() -> bool {
54 let current_dir = std::env::current_dir().unwrap();
55 let rext_app_dir = current_dir.join("rext.toml");
56 rext_app_dir.exists()
57}
58
59pub fn scaffold_rext_app() -> Result<(), RextCoreError> {
81 let current_dir = std::env::current_dir().map_err(RextCoreError::CurrentDir)?;
82
83 if current_dir.join("Cargo.toml").exists() {
86 return Err(RextCoreError::AppAlreadyExists);
87 }
88
89 if current_dir.join("rext.toml").exists() {
91 return Err(RextCoreError::AppAlreadyExists);
92 }
93
94 let src_dir = current_dir.join("src");
96 let public_dir = current_dir.join("public");
97 let templates_dir = current_dir.join("templates");
98
99 std::fs::create_dir_all(&src_dir).map_err(RextCoreError::DirectoryCreation)?;
101 std::fs::create_dir_all(&public_dir).map_err(RextCoreError::DirectoryCreation)?;
102 std::fs::create_dir_all(&templates_dir).map_err(RextCoreError::DirectoryCreation)?;
103
104 let rext_toml_content = r#"[app]
106name = "my-rext-app"
107version = "0.1.0"
108description = "A new Rext application"
109
110[server]
111host = "0.0.0.0"
112port = 3000
113
114[database]
115url = "sqlite://rext.db"
116
117[static]
118directory = "public"
119
120[templates]
121directory = "templates"
122"#;
123
124 let rext_toml_path = current_dir.join("rext.toml");
125 std::fs::write(&rext_toml_path, rext_toml_content)
126 .map_err(|e| RextCoreError::FileWrite(format!("rext.toml: {}", e)))?;
127
128 let cargo_toml_content = format!(
130 r#"
131[package]
132name = "{}"
133version = "0.1.0"
134description = "A new Rext application"
135
136[dependencies]
137rext-core = "0.1.0"
138"#,
139 current_dir.to_str().unwrap()
140 );
141
142 let cargo_toml_path = current_dir.join("Cargo.toml");
143 std::fs::write(&cargo_toml_path, cargo_toml_content)
144 .map_err(|e| RextCoreError::FileWrite(format!("Cargo.toml: {}", e)))?;
145
146 let main_rs_content = r#"
148
149fn main() {
150 println!("Welcome to your new Rext app!");
151}
152"#;
153
154 let main_rs_path = src_dir.join("main.rs");
155 std::fs::write(&main_rs_path, main_rs_content)
156 .map_err(|e| RextCoreError::FileWrite(format!("src/main.rs: {}", e)))?;
157
158 let index_html_content = r#"<!DOCTYPE html>
160<html lang="en">
161<head>
162 <meta charset="UTF-8">
163 <meta name="viewport" content="width=device-width, initial-scale=1.0">
164 <title>My Rext App</title>
165</head>
166<body>
167 <h1>Welcome to Rext!</h1>
168 <p>Your fullstack Rust web application is ready.</p>
169</body>
170</html>
171"#;
172
173 let index_html_path = templates_dir.join("index.html");
174 std::fs::write(&index_html_path, index_html_content)
175 .map_err(|e| RextCoreError::FileWrite(format!("templates/index.html: {}", e)))?;
176
177 let style_css_content = r#"body {
179 font-family: Arial, sans-serif;
180 max-width: 800px;
181 margin: 0 auto;
182 padding: 2rem;
183 line-height: 1.6;
184}
185
186h1 {
187 color: #333;
188 text-align: center;
189}
190
191p {
192 color: #666;
193 text-align: center;
194}
195"#;
196
197 let style_css_path = public_dir.join("style.css");
198 std::fs::write(&style_css_path, style_css_content)
199 .map_err(|e| RextCoreError::FileWrite(format!("public/style.css: {}", e)))?;
200
201 Ok(())
202}
203
204pub fn destroy_rext_app() -> Result<(), RextCoreError> {
210 let current_dir = std::env::current_dir().map_err(RextCoreError::CurrentDir)?;
211
212 let rext_toml_path = current_dir.join("rext.toml");
214 let cargo_toml_path = current_dir.join("Cargo.toml");
215 let src_dir = current_dir.join("src");
216 let public_dir = current_dir.join("public");
217 let templates_dir = current_dir.join("templates");
218 let main_rs_path = src_dir.join("main.rs");
219 let index_html_path = templates_dir.join("index.html");
220 let style_css_path = public_dir.join("style.css");
221
222 if src_dir.exists() {
226 let src_entries: Result<Vec<_>, _> = std::fs::read_dir(&src_dir)
227 .map_err(RextCoreError::DirectoryRead)?
228 .collect();
229 let src_entries = src_entries.map_err(RextCoreError::DirectoryRead)?;
230
231 if src_entries.len() != 1
232 || !src_entries.iter().any(|entry| {
233 entry.file_name() == "main.rs"
234 && entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
235 })
236 {
237 return Err(RextCoreError::SafetyCheck(
238 "src directory contains unexpected files".to_string(),
239 ));
240 }
241 }
242
243 if public_dir.exists() {
245 let public_entries: Result<Vec<_>, _> = std::fs::read_dir(&public_dir)
246 .map_err(RextCoreError::DirectoryRead)?
247 .collect();
248 let public_entries = public_entries.map_err(RextCoreError::DirectoryRead)?;
249
250 if public_entries.len() != 1
251 || !public_entries.iter().any(|entry| {
252 entry.file_name() == "style.css"
253 && entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
254 })
255 {
256 return Err(RextCoreError::SafetyCheck(
257 "public directory contains unexpected files".to_string(),
258 ));
259 }
260 }
261
262 if templates_dir.exists() {
264 let templates_entries: Result<Vec<_>, _> = std::fs::read_dir(&templates_dir)
265 .map_err(RextCoreError::DirectoryRead)?
266 .collect();
267 let templates_entries = templates_entries.map_err(RextCoreError::DirectoryRead)?;
268
269 if templates_entries.len() != 1
270 || !templates_entries.iter().any(|entry| {
271 entry.file_name() == "index.html"
272 && entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
273 })
274 {
275 return Err(RextCoreError::SafetyCheck(
276 "templates directory contains unexpected files".to_string(),
277 ));
278 }
279 }
280
281 if style_css_path.exists() {
286 std::fs::remove_file(&style_css_path)
287 .map_err(|e| RextCoreError::FileRemoval(format!("public/style.css: {}", e)))?;
288 }
289
290 if index_html_path.exists() {
291 std::fs::remove_file(&index_html_path)
292 .map_err(|e| RextCoreError::FileRemoval(format!("templates/index.html: {}", e)))?;
293 }
294
295 if main_rs_path.exists() {
296 std::fs::remove_file(&main_rs_path)
297 .map_err(|e| RextCoreError::FileRemoval(format!("src/main.rs: {}", e)))?;
298 }
299
300 if cargo_toml_path.exists() {
301 std::fs::remove_file(&cargo_toml_path)
302 .map_err(|e| RextCoreError::FileRemoval(format!("Cargo.toml: {}", e)))?;
303 }
304
305 if rext_toml_path.exists() {
306 std::fs::remove_file(&rext_toml_path)
307 .map_err(|e| RextCoreError::FileRemoval(format!("rext.toml: {}", e)))?;
308 }
309
310 if templates_dir.exists() {
312 std::fs::remove_dir(&templates_dir)
313 .map_err(|e| RextCoreError::DirectoryRemoval(format!("templates: {}", e)))?;
314 }
315
316 if public_dir.exists() {
317 std::fs::remove_dir(&public_dir)
318 .map_err(|e| RextCoreError::DirectoryRemoval(format!("public: {}", e)))?;
319 }
320
321 if src_dir.exists() {
322 std::fs::remove_dir(&src_dir)
323 .map_err(|e| RextCoreError::DirectoryRemoval(format!("src: {}", e)))?;
324 }
325
326 Ok(())
327}
328
329pub fn generate_sea_orm_entities_with_open_api_schema() -> Result<(), RextCoreError> {
335 let output = Command::new("sea-orm-cli")
337 .args(&[
338 "generate",
339 "entity",
340 "-u",
341 "sqlite:./sqlite.db?mode=rwc",
342 "-o",
343 format!("{}", ENTITIES_DIR).as_str(),
344 "--model-extra-derives",
345 "utoipa::ToSchema",
346 "--with-serde",
347 "both",
348 "--ignore-tables jobs,workers", ])
350 .output()
351 .map_err(RextCoreError::SeaOrmCliGenerateEntities)?;
352
353 if !output.status.success() {
354 return Err(RextCoreError::SeaOrmCliGenerateEntities(
355 std::io::Error::new(
356 std::io::ErrorKind::Other,
357 format!("sea-orm-cli command failed with status: {}", output.status),
358 ),
359 ));
360 }
361
362 for entry in fs::read_dir(ENTITIES_DIR)? {
364 let entry = entry?;
365 let path = entry.path();
366
367 if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") {
368 let file = File::open(&path)?;
370 let reader = BufReader::new(file);
371 let first_line = reader.lines().next().transpose()?;
372
373 if let Some(line) = first_line {
374 if !line.trim().starts_with("//! `SeaORM` Entity") {
375 continue;
376 }
377 } else {
378 continue;
379 }
380
381 let file = File::open(&path)?;
383 let reader = BufReader::new(file);
384 let mut output_lines: Vec<String> = Vec::new();
385
386 for line_result in reader.lines() {
387 let line = line_result?;
388 let trimmed_line = line.trim_start();
389
390 let mut add_schema = false;
392 for dtype in &TYPES_TO_WRAP {
393 if trimmed_line.starts_with("pub ") && trimmed_line.contains(dtype) {
394 add_schema = true;
395 break;
396 }
397 }
398
399 if add_schema {
401 output_lines.push(" #[schema(value_type = String)]".to_string());
402 }
403
404 output_lines.push(line);
405 }
406
407 let mut file = File::create(&path)?;
409 for line in &output_lines {
410 writeln!(file, "{}", line)?;
411 }
412 }
413 }
414
415 Ok(())
416}