zipity 0.0.1-alpha.2

A markdown-based micro web framework built in Rust
use clap::{ App, SubCommand, Arg };
use std::fs::{ self, File };
use std::io::Write;
use std::path::Path;
use chrono::offset::Local;
use chrono::Datelike;
use slug::slugify;
use std::process::Command;
use std::path::PathBuf;

fn main() {
    let app = App::new("Zipity")
        .version("0.0.1-alpha.2")
        .author("Eric David Smith")
        .about("A command-line tool for Zipity")
        .subcommand(
            SubCommand::with_name("init")
                .about("Creates a new Zipity project")
                .arg(Arg::with_name("project_name").required(true))
        )
        .subcommand(SubCommand::with_name("build"))
        .subcommand(SubCommand::with_name("serve"))
        .subcommand(
            SubCommand::with_name("add")
                .about("Adds a new component to the project")
                .subcommand(
                    SubCommand::with_name("route")
                        .about("Adds a new route with a markdown file")
                        .arg(Arg::with_name("route_name").required(true))
                )
                .subcommand(
                    SubCommand::with_name("api")
                        .about("Adds a new API endpoint")
                        .arg(Arg::with_name("endpoint_name").required(true))
                )
                .subcommand(
                    SubCommand::with_name("test")
                        .about("Adds a new test")
                        .arg(Arg::with_name("test_name").required(true))
                )
                .subcommand(
                    SubCommand::with_name("md")
                        .about("Adds a new markdown file")
                        .arg(Arg::with_name("file_name").required(true))
                )
                .subcommand(
                    SubCommand::with_name("static")
                        .about("Adds a new static file")
                        .arg(Arg::with_name("file_name").required(true))
                )
        )
        .get_matches();

    match app.subcommand() {
        ("init", Some(init_matches)) => {
            let project_name = init_matches.value_of("project_name").unwrap();
            println!("Creating new Zipity project: {}", project_name);

            // Create the project directory
            fs::create_dir(project_name).expect("Failed to create project directory");

            // Create the README.md file inside the project directory
            let readme_path = Path::new(project_name).join("README.md");
            let mut readme_file = File::create(&readme_path).expect(
                "Failed to create README.md file"
            );
            readme_file
                .write_all(
                    b"# My Zipity Project\n\nWelcome to my Zipity project!\n\n## Usage\n\n```bash\n./cli/zipity <SUBCOMMAND> [OPTIONS]\n```\n\n## Help\n\n```text\nZipity 0.0.1-alpha.2\nEric David Smith\nA command-line tool for Zipity\n\nUSAGE:\n    cli [SUBCOMMAND]\n\nFLAGS:\n    -h, --help       Prints help information\n    -V, --version    Prints version information\n\nSUBCOMMANDS:\n    add      Adds a new component to the project\n    build\n    help     Prints this message or the help of the given subcommand(s)\n    init\n    serve\n```"
                )
                .expect("Failed to write to README.md");

            // Copy static assets to the project directory
            let static_dir = Path::new("static");
            let destination_dir = Path::new(project_name).join("static");
            copy_directory(static_dir, destination_dir).expect("Failed to copy static assets");

            // Create the .gitignore file
            let gitignore_path = Path::new(project_name).join(".gitignore");
            let mut gitignore_file = File::create(&gitignore_path).expect(
                "Failed to create .gitignore file"
            );
            gitignore_file.write_all(b"/target\n/out\n").expect("Failed to write to .gitignore");

            // Copy the template.html file to the project directory
            let template_path = Path::new("template.html");
            let destination_path = Path::new(project_name).join("template.html");
            fs::copy(template_path, destination_path).expect("Failed to copy template.html");

            // Run 'git init' in the project directory
            Command::new("git")
                .arg("init")
                .arg(project_name)
                .output()
                .expect("Failed to run 'git init'");

            println!("New Zipity project created: {}", project_name);

            // Create the routes directory
            let routes_dir_path = Path::new(project_name).join("routes");
            fs::create_dir(&routes_dir_path).expect("Failed to create routes directory");

            // Create the index.md file in the routes directory
            let current_date = Local::now();
            let formatted_date = format!(
                "{}-{:02}-{:02}",
                current_date.year(),
                current_date.month(),
                current_date.day()
            );
            let route_slug = slugify("Index");
            let index_md_path = routes_dir_path.join(format!("{}.md", route_slug));
            let mut index_md_file = File::create(&index_md_path).expect(
                "Failed to create index.md file"
            );
            let index_md_content = format!(
                r#"---
title: "Index"
slug: "{}"
date: "{}"
description: "This is a description of my post"
keywords: "keyword1, keyword2, keyword3"
author: "Author Name"
---

# Index

This is a placeholder for the Index route.
"#,
                route_slug,
                formatted_date
            );
            index_md_file
                .write_all(index_md_content.as_bytes())
                .expect("Failed to write to index.md file");

            println!("Created index.md file at {}", index_md_path.to_string_lossy());
        }
        ("build", Some(_)) => {
            println!("You ran 'build' command");
        }
        ("serve", Some(_)) => {
            println!("You ran 'serve' command");
            let host = "127.0.0.1";
            let port = 8080;

            println!("Server is running on http://{}:{}", host, port);

            let output = Command::new("cargo")
                .args(&["run", "--bin", "zipity"])
                .output()
                .expect("Failed to start server");

            if !output.status.success() {
                eprintln!("Error: {:?}", output);
            }
        }
        ("add", Some(add_matches)) => {
            match add_matches.subcommand() {
                ("route", Some(matches)) => {
                    let route_name = matches.value_of("route_name").unwrap();
                    println!("You ran 'add route' command with route name '{}'", route_name);

                    let current_date = Local::now();
                    let formatted_date = format!(
                        "{}-{:02}-{:02}",
                        current_date.year(),
                        current_date.month(),
                        current_date.day()
                    );
                    let route_slug = slugify(route_name);

                    let route_file_path = Path::new("routes").join(format!("{}.md", route_slug));
                    let mut route_file = File::create(&route_file_path).expect(
                        "Failed to create route file"
                    );

                    let template = format!(
                        r#"---
title: "{}"
slug: "{}"
date: "{}"
description: "This is a description of my post"
keywords: "keyword1, keyword2, keyword3"
author: "Author Name"
---

# {}

This is a placeholder for the {} route.
"#,
                        route_name,
                        route_slug,
                        formatted_date,
                        route_name,
                        route_name
                    );
                    route_file
                        .write_all(template.as_bytes())
                        .expect("Failed to write to route file");

                    println!("Created route file at {}", route_file_path.to_string_lossy());
                }
                ("api", Some(matches)) => {
                    let api_name = matches.value_of("endpoint_name").unwrap();
                    println!("You ran 'add api' command with API name '{}'", api_name);

                    let api_slug = slugify(api_name);

                    // Create the 'api' directory if it doesn't exist
                    fs::create_dir_all("api").expect("Failed to create 'api' directory");

                    let api_file_path = Path::new("api").join(format!("{}.rs", api_slug));
                    let mut api_file = File::create(&api_file_path).expect(
                        "Failed to create API file"
                    );

                    let template = format!(
                        r#"use actix_web::{{web, HttpResponse, Responder}};

pub async fn {}() -> impl Responder {{
    HttpResponse::Ok().body("This is the {} API endpoint")
}}
"#,
                        api_slug,
                        api_name
                    );
                    api_file.write_all(template.as_bytes()).expect("Failed to write to API file");

                    println!("Created API file at {}", api_file_path.to_string_lossy());
                }
                ("test", Some(matches)) => {
                    let test_name = matches.value_of("test_name").unwrap();
                    println!("You ran 'add test' command with test name '{}'", test_name);
                    // Here is where you would add the logic to actually create the new test
                }
                ("md", Some(matches)) => {
                    let file_name = matches.value_of("file_name").unwrap();
                    println!("You ran 'add md' command with file name '{}'", file_name);
                    // Here is where you would add the logic to actually create the new markdown file
                }
                ("static", Some(matches)) => {
                    let file_name = matches.value_of("file_name").unwrap();
                    println!("You ran 'add static' command with file name '{}'", file_name);
                    // Here is where you would add the logic to actually create the new static file
                }
                _ => {
                    println!("Unknown 'add' subcommand");
                }
            }
        }
        _ => {
            println!("Unknown command");
        }
    }
}

// Helper function to copy a directory recursively
fn copy_directory(source: &Path, destination: PathBuf) -> std::io::Result<()> {
    if source.is_dir() {
        fs::create_dir_all(&destination)?;

        for entry in fs::read_dir(source)? {
            let entry = entry?;
            let entry_path = entry.path();
            let dest_path = destination.join(entry.file_name());

            if entry_path.is_dir() {
                copy_directory(&entry_path, dest_path)?;
            } else {
                fs::copy(&entry_path, &dest_path)?;
            }
        }
    }
    Ok(())
}