autumn-cli 0.10.0

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

use std::path::Path;

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

const TASK_DEPS: &[(&str, &str)] = &[("serde", "{ version = \"1\", features = [\"derive\"] }")];

/// Compute the file actions for `autumn generate task`.
///
/// # Errors
/// Project layout and name validation errors surface here.
pub fn plan_task(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("tasks").join(format!("{snake_name}.rs")),
        render_task_file(&snake_name, &pascal_name),
    );
    plan_cargo_deps(
        &mut plan,
        project_root,
        TASK_DEPS,
        &project_root.join("tasks"),
    );
    Ok(plan)
}

fn render_task_file(snake_name: &str, pascal_name: &str) -> String {
    format!(
        r#"//! One-off operational task generated by `autumn generate task`.

use autumn_web::prelude::*;
use serde::Deserialize;

#[derive(Debug, Default, Deserialize)]
pub struct {pascal_name}Args {{
    #[serde(default)]
    pub dry_run: bool,
}}

/// Describe what `{snake_name}` does.
#[autumn_web::task]
pub async fn {snake_name}(TaskArgs(args): TaskArgs<{pascal_name}Args>) -> AutumnResult<()> {{
    if args.dry_run {{
        autumn_web::reexports::tracing::info!(task = "{snake_name}", "dry run requested");
        return Ok(());
    }}

    Ok(())
}}
"#
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::generate::Flags;
    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();
        tmp
    }

    #[test]
    fn plan_creates_task_file() {
        let tmp = project();
        let plan = plan_task(tmp.path(), "cleanup_users").unwrap();
        assert!(
            plan.actions
                .iter()
                .any(|action| action.path().ends_with("tasks/cleanup_users.rs")),
            "plan should create tasks/cleanup_users.rs"
        );
    }

    #[test]
    fn execute_writes_task_skeleton() {
        let tmp = project();
        let plan = plan_task(tmp.path(), "cleanup_users").unwrap();
        plan.execute(Flags::default()).unwrap();

        let task = fs::read_to_string(tmp.path().join("tasks/cleanup_users.rs")).unwrap();
        assert!(task.contains("#[autumn_web::task]"));
        assert!(task.contains("pub async fn cleanup_users"));
        assert!(task.contains("TaskArgs<CleanupUsersArgs>"));
    }
}