From b7159c3e0c22ac706eaa8e0ad950701fba8d4348 Mon Sep 17 00:00:00 2001 From: Folyd Date: Sun, 9 Mar 2025 18:50:51 +0800 Subject: [PATCH 1/3] Add `aiscript new` command to create new project --- Cargo.lock | 3 + aiscript/Cargo.toml | 4 + aiscript/src/main.rs | 16 +++ aiscript/src/project.rs | 218 ++++++++++++++++++++++++++++++++++++++++ examples/routes/ai.ai | 8 ++ 5 files changed, 249 insertions(+) create mode 100644 aiscript/src/project.rs diff --git a/Cargo.lock b/Cargo.lock index 1cf627f..c3f1edb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,9 @@ dependencies = [ "dotenv", "rustyline", "serde", + "tempfile", "tokio", + "whoami", ] [[package]] @@ -3596,6 +3598,7 @@ checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ "redox_syscall", "wasite", + "web-sys", ] [[package]] diff --git a/aiscript/Cargo.toml b/aiscript/Cargo.toml index 19ba22c..dd22b8f 100644 --- a/aiscript/Cargo.toml +++ b/aiscript/Cargo.toml @@ -24,6 +24,10 @@ dotenv = "0.15.0" rustyline = "15.0" dirs = "6.0" serde.workspace = true +whoami = "1.4.1" + +[dev-dependencies] +tempfile = "3.8.1" [features] ai_test = ["aiscript-vm/ai_test"] diff --git a/aiscript/src/main.rs b/aiscript/src/main.rs index ff1b4e5..c50dcf6 100644 --- a/aiscript/src/main.rs +++ b/aiscript/src/main.rs @@ -8,6 +8,9 @@ use repr::Repl; use tokio::task; mod repr; +mod project; + +use project::ProjectGenerator; #[derive(Parser)] #[command(version, about, long_about = None)] @@ -34,6 +37,12 @@ enum Commands { #[arg(short, long, default_value_t = false)] reload: bool, }, + /// Create a new AIScript project with a standard directory structure. + New { + /// The name of the new project + #[arg(value_name = "PROJECT_NAME")] + name: String, + }, } #[tokio::main] @@ -47,6 +56,13 @@ async fn main() { println!("Server listening on port http://localhost:{}", port); aiscript_runtime::run(file, port, reload).await; } + Some(Commands::New { name }) => { + let generator = ProjectGenerator::new(&name); + if let Err(e) = generator.generate() { + eprintln!("{}", e); + process::exit(1); + } + } None => { if let Some(path) = cli.file { let pg_connection = aiscript_runtime::get_pg_connection().await; diff --git a/aiscript/src/project.rs b/aiscript/src/project.rs new file mode 100644 index 0000000..83adbcc --- /dev/null +++ b/aiscript/src/project.rs @@ -0,0 +1,218 @@ +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +pub struct ProjectGenerator { + project_name: String, + project_path: PathBuf, +} + +impl ProjectGenerator { + pub fn new(project_name: &str) -> Self { + let project_path = PathBuf::from(project_name); + Self { + project_name: project_name.to_string(), + project_path, + } + } + + pub fn generate(&self) -> Result<(), String> { + // Check if directory already exists + if self.project_path.exists() { + return Err(format!( + "Error: Directory '{}' already exists", + self.project_name + )); + } + + // Create project directory + fs::create_dir_all(&self.project_path).map_err(|e| { + format!( + "Failed to create project directory '{}': {}", + self.project_name, e + ) + })?; + + // Create standard directories + self.create_directories()?; + + // Create project.toml + self.create_project_toml()?; + + // Create basic example file + self.create_example_file()?; + + println!( + "Successfully created new AIScript project: {}", + self.project_name + ); + println!("Project structure:"); + println!("{}", self.display_project_structure()); + println!(""); + println!("Run `aiscript serve` to start the server."); + + Ok(()) + } + + fn create_directories(&self) -> Result<(), String> { + let dirs = vec!["lib", "routes"]; + + for dir in dirs { + let dir_path = self.project_path.join(dir); + fs::create_dir_all(&dir_path).map_err(|e| { + format!("Failed to create directory '{}': {}", dir_path.display(), e) + })?; + } + + Ok(()) + } + + fn create_project_toml(&self) -> Result<(), String> { + let toml_path = self.project_path.join("project.toml"); + let username = whoami::username(); + + let toml_content = format!( + r#"[project] +name = "{}" +description = "An AIScript project" +version = "0.1.0" +authors = ["{}"] + +[network] +host = "0.0.0.0" +port = 8000 + +[apidoc] +enabled = true +type = "redoc" +path = "/docs" +"#, + self.project_name, username + ); + + let mut file = fs::File::create(&toml_path) + .map_err(|e| format!("Failed to create project.toml: {}", e))?; + + file.write_all(toml_content.as_bytes()) + .map_err(|e| format!("Failed to write to project.toml: {}", e))?; + + Ok(()) + } + + fn create_example_file(&self) -> Result<(), String> { + let routes_dir = self.project_path.join("routes"); + let example_path = routes_dir.join("index.ai"); + + let example_content = r#"// Example AIScript route handler +get /hello { + query { + name: str + } + + return { message: f"Hello, {query.name}!" }; +} +"#; + + let mut file = fs::File::create(&example_path) + .map_err(|e| format!("Failed to create example file: {}", e))?; + + file.write_all(example_content.as_bytes()) + .map_err(|e| format!("Failed to write to example file: {}", e))?; + + Ok(()) + } + + fn display_project_structure(&self) -> String { + let mut result = format!("{}\n", self.project_name); + result.push_str("├── lib/\n"); + result.push_str("├── routes/\n"); + result.push_str("│ └── index.ai\n"); + result.push_str("└── project.toml\n"); + + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + #[test] + fn test_project_generator() { + // Use tempdir to ensure test files are cleaned up + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + + // Create a test project in the temp directory + let project_name = "test_project"; + let project_path = temp_path.join(project_name); + + // Run in the temp directory + std::env::set_current_dir(temp_path).unwrap(); + + let generator = ProjectGenerator::new(project_name); + let result = generator.generate(); + + assert!(result.is_ok(), "Project generation failed: {:?}", result); + + // Verify project structure + assert!(project_path.exists(), "Project directory not created"); + assert!( + project_path.join("lib").exists(), + "lib directory not created" + ); + assert!( + project_path.join("routes").exists(), + "routes directory not created" + ); + assert!( + project_path.join("project.toml").exists(), + "project.toml not created" + ); + assert!( + project_path.join("routes/index.ai").exists(), + "Example file not created" + ); + + // Verify project.toml content + let toml_content = fs::read_to_string(project_path.join("project.toml")).unwrap(); + assert!(toml_content.contains(&format!("name = \"{}\"", project_name))); + assert!(toml_content.contains("version = \"0.1.0\"")); + + // Verify example file content + let example_content = fs::read_to_string(project_path.join("routes/index.ai")).unwrap(); + assert!(example_content.contains("fn index(req, res)")); + } + + #[test] + fn test_project_already_exists() { + // Use tempdir to ensure test files are cleaned up + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + + // Create a directory that will conflict + let project_name = "existing_project"; + let project_path = temp_path.join(project_name); + fs::create_dir_all(&project_path).unwrap(); + + // Run in the temp directory + std::env::set_current_dir(temp_path).unwrap(); + + let generator = ProjectGenerator::new(project_name); + let result = generator.generate(); + + assert!( + result.is_err(), + "Project generation should fail for existing directory" + ); + if let Err(err) = result { + assert!( + err.contains("already exists"), + "Wrong error message: {}", + err + ); + } + } +} diff --git a/examples/routes/ai.ai b/examples/routes/ai.ai index 9332de3..7142d8d 100644 --- a/examples/routes/ai.ai +++ b/examples/routes/ai.ai @@ -8,4 +8,12 @@ get /guess { let message = "You got it!" if query.value == 42 else "Try again"; return { message }; +} + +get /hello { + query { + name: str + } + + return { message: f"Hello, {query.name}!" }; } \ No newline at end of file From 9eb8140a0c0e27e0da2afd4e926d5f6bb85c662f Mon Sep 17 00:00:00 2001 From: Folyd Date: Sun, 9 Mar 2025 18:58:15 +0800 Subject: [PATCH 2/3] Fix unit tests --- aiscript/src/project.rs | 129 +++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/aiscript/src/project.rs b/aiscript/src/project.rs index 83adbcc..7b8efa1 100644 --- a/aiscript/src/project.rs +++ b/aiscript/src/project.rs @@ -1,6 +1,6 @@ use std::fs; use std::io::Write; -use std::path::PathBuf; +use std::path::{PathBuf}; pub struct ProjectGenerator { project_name: String, @@ -35,17 +35,14 @@ impl ProjectGenerator { // Create standard directories self.create_directories()?; - + // Create project.toml self.create_project_toml()?; - + // Create basic example file self.create_example_file()?; - println!( - "Successfully created new AIScript project: {}", - self.project_name - ); + println!("Successfully created new AIScript project: {}", self.project_name); println!("Project structure:"); println!("{}", self.display_project_structure()); println!(""); @@ -56,21 +53,21 @@ impl ProjectGenerator { fn create_directories(&self) -> Result<(), String> { let dirs = vec!["lib", "routes"]; - + for dir in dirs { let dir_path = self.project_path.join(dir); fs::create_dir_all(&dir_path).map_err(|e| { format!("Failed to create directory '{}': {}", dir_path.display(), e) })?; } - + Ok(()) } fn create_project_toml(&self) -> Result<(), String> { let toml_path = self.project_path.join("project.toml"); let username = whoami::username(); - + let toml_content = format!( r#"[project] name = "{}" @@ -89,20 +86,22 @@ path = "/docs" "#, self.project_name, username ); - - let mut file = fs::File::create(&toml_path) - .map_err(|e| format!("Failed to create project.toml: {}", e))?; - - file.write_all(toml_content.as_bytes()) - .map_err(|e| format!("Failed to write to project.toml: {}", e))?; - + + let mut file = fs::File::create(&toml_path).map_err(|e| { + format!("Failed to create project.toml: {}", e) + })?; + + file.write_all(toml_content.as_bytes()).map_err(|e| { + format!("Failed to write to project.toml: {}", e) + })?; + Ok(()) } fn create_example_file(&self) -> Result<(), String> { let routes_dir = self.project_path.join("routes"); let example_path = routes_dir.join("index.ai"); - + let example_content = r#"// Example AIScript route handler get /hello { query { @@ -112,13 +111,15 @@ get /hello { return { message: f"Hello, {query.name}!" }; } "#; - - let mut file = fs::File::create(&example_path) - .map_err(|e| format!("Failed to create example file: {}", e))?; - - file.write_all(example_content.as_bytes()) - .map_err(|e| format!("Failed to write to example file: {}", e))?; - + + let mut file = fs::File::create(&example_path).map_err(|e| { + format!("Failed to create example file: {}", e) + })?; + + file.write_all(example_content.as_bytes()).map_err(|e| { + format!("Failed to write to example file: {}", e) + })?; + Ok(()) } @@ -128,7 +129,7 @@ get /hello { result.push_str("├── routes/\n"); result.push_str("│ └── index.ai\n"); result.push_str("└── project.toml\n"); - + result } } @@ -144,75 +145,65 @@ mod tests { // Use tempdir to ensure test files are cleaned up let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path(); - + // Create a test project in the temp directory let project_name = "test_project"; + + // Create an absolute path for the project let project_path = temp_path.join(project_name); - - // Run in the temp directory - std::env::set_current_dir(temp_path).unwrap(); - + + // Create a generator with the project name let generator = ProjectGenerator::new(project_name); + + // Override the project path for testing + let generator = ProjectGenerator { + project_name: project_name.to_string(), + project_path: project_path.clone(), + }; + let result = generator.generate(); - + assert!(result.is_ok(), "Project generation failed: {:?}", result); - + // Verify project structure assert!(project_path.exists(), "Project directory not created"); - assert!( - project_path.join("lib").exists(), - "lib directory not created" - ); - assert!( - project_path.join("routes").exists(), - "routes directory not created" - ); - assert!( - project_path.join("project.toml").exists(), - "project.toml not created" - ); - assert!( - project_path.join("routes/index.ai").exists(), - "Example file not created" - ); - + assert!(project_path.join("lib").exists(), "lib directory not created"); + assert!(project_path.join("routes").exists(), "routes directory not created"); + assert!(project_path.join("project.toml").exists(), "project.toml not created"); + assert!(project_path.join("routes/index.ai").exists(), "Example file not created"); + // Verify project.toml content let toml_content = fs::read_to_string(project_path.join("project.toml")).unwrap(); assert!(toml_content.contains(&format!("name = \"{}\"", project_name))); assert!(toml_content.contains("version = \"0.1.0\"")); - + // Verify example file content let example_content = fs::read_to_string(project_path.join("routes/index.ai")).unwrap(); - assert!(example_content.contains("fn index(req, res)")); + assert!(example_content.contains("get /hello")); } - + #[test] fn test_project_already_exists() { // Use tempdir to ensure test files are cleaned up let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path(); - + // Create a directory that will conflict let project_name = "existing_project"; let project_path = temp_path.join(project_name); fs::create_dir_all(&project_path).unwrap(); - - // Run in the temp directory - std::env::set_current_dir(temp_path).unwrap(); - - let generator = ProjectGenerator::new(project_name); + + // Create a generator with absolute path + let generator = ProjectGenerator { + project_name: project_name.to_string(), + project_path, + }; + let result = generator.generate(); - - assert!( - result.is_err(), - "Project generation should fail for existing directory" - ); + + assert!(result.is_err(), "Project generation should fail for existing directory"); if let Err(err) = result { - assert!( - err.contains("already exists"), - "Wrong error message: {}", - err - ); + assert!(err.contains("already exists"), "Wrong error message: {}", err); } } } From 64509e36dc14a38032a57e06f43ef626fdc11d46 Mon Sep 17 00:00:00 2001 From: Folyd Date: Sun, 9 Mar 2025 19:00:14 +0800 Subject: [PATCH 3/3] Cargo fmt and clippy --- aiscript/src/main.rs | 2 +- aiscript/src/project.rs | 114 +++++++++++++++++++++++----------------- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/aiscript/src/main.rs b/aiscript/src/main.rs index c50dcf6..bbc8f3a 100644 --- a/aiscript/src/main.rs +++ b/aiscript/src/main.rs @@ -7,8 +7,8 @@ use clap::{Parser, Subcommand}; use repr::Repl; use tokio::task; -mod repr; mod project; +mod repr; use project::ProjectGenerator; diff --git a/aiscript/src/project.rs b/aiscript/src/project.rs index 7b8efa1..ff64e56 100644 --- a/aiscript/src/project.rs +++ b/aiscript/src/project.rs @@ -1,6 +1,6 @@ use std::fs; use std::io::Write; -use std::path::{PathBuf}; +use std::path::PathBuf; pub struct ProjectGenerator { project_name: String, @@ -35,17 +35,20 @@ impl ProjectGenerator { // Create standard directories self.create_directories()?; - + // Create project.toml self.create_project_toml()?; - + // Create basic example file self.create_example_file()?; - println!("Successfully created new AIScript project: {}", self.project_name); + println!( + "Successfully created new AIScript project: {}", + self.project_name + ); println!("Project structure:"); println!("{}", self.display_project_structure()); - println!(""); + println!(); println!("Run `aiscript serve` to start the server."); Ok(()) @@ -53,21 +56,21 @@ impl ProjectGenerator { fn create_directories(&self) -> Result<(), String> { let dirs = vec!["lib", "routes"]; - + for dir in dirs { let dir_path = self.project_path.join(dir); fs::create_dir_all(&dir_path).map_err(|e| { format!("Failed to create directory '{}': {}", dir_path.display(), e) })?; } - + Ok(()) } fn create_project_toml(&self) -> Result<(), String> { let toml_path = self.project_path.join("project.toml"); let username = whoami::username(); - + let toml_content = format!( r#"[project] name = "{}" @@ -86,22 +89,20 @@ path = "/docs" "#, self.project_name, username ); - - let mut file = fs::File::create(&toml_path).map_err(|e| { - format!("Failed to create project.toml: {}", e) - })?; - - file.write_all(toml_content.as_bytes()).map_err(|e| { - format!("Failed to write to project.toml: {}", e) - })?; - + + let mut file = fs::File::create(&toml_path) + .map_err(|e| format!("Failed to create project.toml: {}", e))?; + + file.write_all(toml_content.as_bytes()) + .map_err(|e| format!("Failed to write to project.toml: {}", e))?; + Ok(()) } fn create_example_file(&self) -> Result<(), String> { let routes_dir = self.project_path.join("routes"); let example_path = routes_dir.join("index.ai"); - + let example_content = r#"// Example AIScript route handler get /hello { query { @@ -111,15 +112,13 @@ get /hello { return { message: f"Hello, {query.name}!" }; } "#; - - let mut file = fs::File::create(&example_path).map_err(|e| { - format!("Failed to create example file: {}", e) - })?; - - file.write_all(example_content.as_bytes()).map_err(|e| { - format!("Failed to write to example file: {}", e) - })?; - + + let mut file = fs::File::create(&example_path) + .map_err(|e| format!("Failed to create example file: {}", e))?; + + file.write_all(example_content.as_bytes()) + .map_err(|e| format!("Failed to write to example file: {}", e))?; + Ok(()) } @@ -129,7 +128,7 @@ get /hello { result.push_str("├── routes/\n"); result.push_str("│ └── index.ai\n"); result.push_str("└── project.toml\n"); - + result } } @@ -145,65 +144,84 @@ mod tests { // Use tempdir to ensure test files are cleaned up let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path(); - + // Create a test project in the temp directory let project_name = "test_project"; - + // Create an absolute path for the project let project_path = temp_path.join(project_name); - + // Create a generator with the project name let generator = ProjectGenerator::new(project_name); - + // Override the project path for testing let generator = ProjectGenerator { project_name: project_name.to_string(), project_path: project_path.clone(), }; - + let result = generator.generate(); - + assert!(result.is_ok(), "Project generation failed: {:?}", result); - + // Verify project structure assert!(project_path.exists(), "Project directory not created"); - assert!(project_path.join("lib").exists(), "lib directory not created"); - assert!(project_path.join("routes").exists(), "routes directory not created"); - assert!(project_path.join("project.toml").exists(), "project.toml not created"); - assert!(project_path.join("routes/index.ai").exists(), "Example file not created"); - + assert!( + project_path.join("lib").exists(), + "lib directory not created" + ); + assert!( + project_path.join("routes").exists(), + "routes directory not created" + ); + assert!( + project_path.join("project.toml").exists(), + "project.toml not created" + ); + assert!( + project_path.join("routes/index.ai").exists(), + "Example file not created" + ); + // Verify project.toml content let toml_content = fs::read_to_string(project_path.join("project.toml")).unwrap(); assert!(toml_content.contains(&format!("name = \"{}\"", project_name))); assert!(toml_content.contains("version = \"0.1.0\"")); - + // Verify example file content let example_content = fs::read_to_string(project_path.join("routes/index.ai")).unwrap(); assert!(example_content.contains("get /hello")); } - + #[test] fn test_project_already_exists() { // Use tempdir to ensure test files are cleaned up let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path(); - + // Create a directory that will conflict let project_name = "existing_project"; let project_path = temp_path.join(project_name); fs::create_dir_all(&project_path).unwrap(); - + // Create a generator with absolute path let generator = ProjectGenerator { project_name: project_name.to_string(), project_path, }; - + let result = generator.generate(); - - assert!(result.is_err(), "Project generation should fail for existing directory"); + + assert!( + result.is_err(), + "Project generation should fail for existing directory" + ); if let Err(err) = result { - assert!(err.contains("already exists"), "Wrong error message: {}", err); + assert!( + err.contains("already exists"), + "Wrong error message: {}", + err + ); } } }