rext_core/
lib.rs

1//! # rext_core
2//!
3//! The `rext_core` crate is the library that powers Rext, the fullstack, batteries included Rust framework for developing web applications.
4//!
5//! It handles the absolute most basic requirements nearly all web apps will share, such as routing, API documentation, and the front-end.
6//!
7//! Status: 0%
8//!
9//! [Visit Rext](https://rextstack.org)
10//!
11
12mod error;
13
14use crate::error::RextCoreError;
15use std::fs::{self, File};
16use std::io::{BufRead, BufReader, Write};
17use std::process::Command;
18
19/// Constant list of data types to target (easily expandable)
20pub const TYPES_TO_WRAP: [&str; 2] = ["Uuid", "DateTimeWithTimeZone"];
21
22/// Directory containing generated sea-orm entity files
23pub const ENTITIES_DIR: &str = "backend/entity/models";
24
25/// Configuration for the server
26pub 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
40/// Check if a Rext app has been initialized in the current directory by looking for the rext_app directory
41///
42/// Returns true if the rext_app directory exists, false otherwise.
43///
44/// # Example
45///
46/// ```rust
47/// use rext_core::check_for_rext_app;
48///
49/// let is_rext_app = check_for_rext_app();
50/// // This should be false as there is no Rext app here
51/// assert!(!is_rext_app);
52/// ```
53pub 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
59/// Scaffold a new Rext application in the current directory
60///
61/// Creates the basic directory structure and config files needed for a Rext app.
62/// This includes:
63/// - rext.toml configuration file
64/// - src/ directory for application code
65/// - public/ directory for static assets
66/// - templates/ directory for HTML templates
67///
68/// Returns an error if the app already exists or if there's an I/O error during creation.
69///
70/// # Example
71///
72/// ```rust
73/// use rext_core::scaffold_rext_app;
74///
75/// match scaffold_rext_app() {
76///     Ok(_) => println!("Rext app created successfully!"),
77///     Err(e) => eprintln!("Failed to create app: {}", e),
78/// }
79/// ```
80pub fn scaffold_rext_app() -> Result<(), RextCoreError> {
81    let current_dir = std::env::current_dir().map_err(RextCoreError::CurrentDir)?;
82
83    // Confirm a rust project does not already exist in this directory
84    // (helps prevent accidental overwriting of existing projects, including rext-core, oopsies)
85    if current_dir.join("Cargo.toml").exists() {
86        return Err(RextCoreError::AppAlreadyExists);
87    }
88
89    // Check if rext.toml already exists
90    if current_dir.join("rext.toml").exists() {
91        return Err(RextCoreError::AppAlreadyExists);
92    }
93
94    // Create basic directory structure
95    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    // Create directories
100    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    // Create rext.toml configuration file
105    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    // Create a basic Cargo.toml file
129    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    // Create a basic main.rs file
147    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    // Create a basic index.html template
159    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    // Create a basic CSS file
178    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
204/// Completely destroys a Rext application in the current directory
205///
206/// Removes all files and directories created by the scaffold_rext_app function.
207///
208/// Returns an error if there's an I/O error during destruction.
209pub fn destroy_rext_app() -> Result<(), RextCoreError> {
210    let current_dir = std::env::current_dir().map_err(RextCoreError::CurrentDir)?;
211
212    // Files and directories that scaffold_rext_app creates
213    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    // Safety check: Verify that directories only contain expected files
223
224    // Check src/ directory
225    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    // Check public/ directory
244    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    // Check templates/ directory
263    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 we've reached here, all directories contain only expected files
282    // Now remove all files and directories in reverse order of creation
283
284    // Remove files first
285    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    // Remove directories (they should now be empty)
311    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
329/// Generates the SeaORM entities with OpenAPI support
330///
331/// Adds the derive ToSchema and #[schema(value_type = String)] to unsupported data types
332///
333/// Returns a RextCoreError if an error occurs during the generation process
334pub fn generate_sea_orm_entities_with_open_api_schema() -> Result<(), RextCoreError> {
335    // run the see-orm-cli command with serde and utoipa derives
336    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", // These tables are ignored, they are for the job queue
349        ])
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    // Process each .rs file in the entities directory
363    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            // Check if this is a SeaORM entity file
369            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            // Re-open file to process it
382            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                // Check if the line is a public field with a target type
391                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                // Insert the schema attribute if matched
400                if add_schema {
401                    output_lines.push("    #[schema(value_type = String)]".to_string());
402                }
403
404                output_lines.push(line);
405            }
406
407            // Write the modified content back to the file
408            let mut file = File::create(&path)?;
409            for line in &output_lines {
410                writeln!(file, "{}", line)?;
411            }
412        }
413    }
414
415    Ok(())
416}