Skip to main content

pinto/service/
lifecycle.rs

1//! Board initialization and creation of the default configuration.
2
3use crate::config::Config;
4use crate::error::{Error, Result};
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8/// Result of [`init_board`].
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum InitOutcome {
11    /// A new board was initialized; contains the generated `.pinto` path.
12    Created(PathBuf),
13    /// The board was already initialized; contains the existing `.pinto` path.
14    AlreadyInitialized(PathBuf),
15}
16
17/// Initialize `.pinto/` (`config.toml` and `tasks/`) in `project_dir`.
18///
19/// If `config.toml` already exists, leave the board unchanged and return
20/// [`InitOutcome::AlreadyInitialized`].
21pub async fn init_board(project_dir: &Path) -> Result<InitOutcome> {
22    let board_dir = project_dir.join(".pinto");
23    let config_path = board_dir.join("config.toml");
24    if fs::try_exists(&config_path)
25        .await
26        .map_err(|e| Error::io(&config_path, &e))?
27    {
28        return Ok(InitOutcome::AlreadyInitialized(board_dir));
29    }
30
31    let tasks_dir = board_dir.join("tasks");
32    fs::create_dir_all(&tasks_dir)
33        .await
34        .map_err(|e| Error::io(&tasks_dir, &e))?;
35
36    let mut config = Config::default();
37    if let Some(name) = project_dir.file_name().and_then(|s| s.to_str()) {
38        config.project.name = name.to_string();
39    }
40    config.save(&config_path).await?;
41
42    Ok(InitOutcome::Created(board_dir))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use tempfile::TempDir;
49
50    #[tokio::test]
51    async fn init_creates_config_and_tasks_dir() {
52        let dir = TempDir::new().expect("temp dir");
53        let board = dir.path().join(".pinto");
54
55        let outcome = init_board(dir.path()).await.expect("init succeeds");
56
57        assert_eq!(outcome, InitOutcome::Created(board.clone()));
58        assert!(board.join("config.toml").is_file());
59        assert!(board.join("tasks").is_dir());
60    }
61
62    #[tokio::test]
63    async fn init_is_idempotent_and_preserves_config() {
64        let dir = TempDir::new().expect("temp dir");
65        init_board(dir.path()).await.expect("first init");
66        let config_path = dir.path().join(".pinto").join("config.toml");
67        let before = std::fs::read_to_string(&config_path).expect("read config");
68
69        let outcome = init_board(dir.path()).await.expect("second init");
70
71        assert!(matches!(outcome, InitOutcome::AlreadyInitialized(_)));
72        let after = std::fs::read_to_string(&config_path).expect("read config");
73        assert_eq!(before, after, "existing config must not be modified");
74    }
75
76    #[tokio::test]
77    async fn init_sets_project_name_from_directory() {
78        let dir = TempDir::new().expect("temp dir");
79        let project = dir.path().join("myproject");
80        std::fs::create_dir(&project).expect("mkdir");
81
82        init_board(&project).await.expect("init succeeds");
83
84        let config = Config::load(&project.join(".pinto").join("config.toml"))
85            .await
86            .expect("load");
87        assert_eq!(config.project.name, "myproject");
88    }
89}