autumn-cli 0.9.0

CLI tool for the Autumn web framework
//! `autumn generate service` -- emit a one-off operational service skeleton.

use std::path::Path;

use super::emit::Plan;
use super::model::validate_resource_name;
use super::naming::{pascal, snake};
use super::{Flags, GenerateError, ensure_project_root};

/// Compute the file actions for `autumn generate service`.
///
/// # Errors
/// Project layout and name validation errors surface here.
pub fn plan_service(project_root: &Path, name: &str) -> Result<Plan, GenerateError> {
    ensure_project_root(project_root)?;
    validate_resource_name(name)?;

    let snake_name = snake(name);
    let pascal_name = pascal(name);
    let mut plan = Plan::new(project_root);

    plan.create(
        project_root.join("src").join("services").join(format!("{snake_name}.rs")),
        render_service_file(&snake_name, &pascal_name),
    );

    let mod_path = project_root.join("src").join("services").join("mod.rs");
    let existing_mod = std::fs::read_to_string(&mod_path).unwrap_or_default();
    plan.modify(
        mod_path,
        super::schema_edit::add_mod_declaration(&existing_mod, &snake_name),
    );

    let main_path = project_root.join("src").join("main.rs");
    let content = std::fs::read_to_string(&main_path).unwrap_or_default();
    if !content.is_empty() {
         plan.modify(
             main_path,
             super::schema_edit::update_main_rs(&content, &["services"], &[]),
         );
    }

    Ok(plan)
}

fn render_service_file(snake_name: &str, pascal_name: &str) -> String {
    format!(
        r"//! Service generated by `autumn generate service`.

use autumn_web::prelude::*;
use autumn_web::service;

/// Service for `{snake_name}` business logic.
#[service]
pub trait {pascal_name}Service {{
    fn deps();
}}

impl {pascal_name}ServiceImpl {{
    // Add business logic methods here
}}
"
    )
}

/// CLI entry point.
pub fn run(name: &str, flags: Flags) {
    let cwd = match std::env::current_dir() {
        Ok(d) => d,
        Err(e) => {
            eprintln!("Error: cannot determine current directory: {e}");
            std::process::exit(1);
        }
    };
    match plan_service(&cwd, name).and_then(|p| p.execute(flags)) {
        Ok(()) => {}
        Err(e) => {
            eprintln!("Error: {e}");
            std::process::exit(1);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    fn project() -> TempDir {
        let tmp = TempDir::new().unwrap();
        fs::write(tmp.path().join("Cargo.toml"), "[package]\nname=\"x\"\n").unwrap();
        fs::create_dir_all(tmp.path().join("src")).unwrap();
        fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
        tmp
    }

    #[test]
    fn plan_creates_service_file() {
        let tmp = project();
        let plan = plan_service(tmp.path(), "Order").unwrap();
        assert!(
            plan.actions
                .iter()
                .any(|action| action.path().ends_with("src/services/order.rs")),
            "plan should create src/services/order.rs"
        );
    }

    #[test]
    fn execute_writes_service_skeleton() {
        let tmp = project();
        let plan = plan_service(tmp.path(), "Order").unwrap();
        plan.execute(Flags::default()).unwrap();

        let svc = fs::read_to_string(tmp.path().join("src/services/order.rs")).unwrap();
        assert!(svc.contains("#[service]"));
        assert!(svc.contains("pub trait OrderService"));
        assert!(svc.contains("impl OrderServiceImpl"));
    }
}