bizi-server 0.5.1

Task runner server for the bizi project
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::{io::ErrorKind, path::Path};

pub use bizi_api::Task;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub tasks: IndexMap<String, Task>,
}

#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("JSON parse error: {0}")]
    Json(#[from] serde_json::Error),
}

impl ConfigError {
    pub fn is_not_found(&self) -> bool {
        matches!(self, ConfigError::Io(e) if e.kind() == ErrorKind::NotFound)
    }
}

impl Config {
    pub async fn load(path: &str) -> Result<Self, ConfigError> {
        let path = Path::new(&path);
        let config = tokio::fs::read_to_string(path.join("task.config.json")).await?;

        Ok(serde_json::from_str(&config)?)
    }

    /// TODO: handle circular task references (this will just be an error as it's a user mistake and we can't fix it)
    pub fn get_task(&self, task_key: String) -> Option<Task> {
        let mut task = if let Some(task) = get_task(&self.tasks, task_key) {
            task.clone()
        } else {
            return None;
        };

        if let Some(depends_on) = &task.depends_on {
            let mut depends_on_tasks = IndexMap::new();
            for key in depends_on.iter() {
                let depends_on_task = get_task(&self.tasks, key.clone());
                if let Some(depends_on_task) = depends_on_task {
                    depends_on_tasks.insert(key.clone(), depends_on_task.clone());
                }
            }
            task.depends_on_tasks = Some(depends_on_tasks);
        }

        Some(task)
    }

    pub fn get_all_tasks(&self) -> IndexMap<String, Task> {
        get_all_tasks(&self.tasks, None)
    }
}

/// Handles getting nested tasks like `dev:packages` or `dev:server`.
fn get_task(tasks: &IndexMap<String, Task>, task_key: String) -> Option<&Task> {
    let task_key_segments = task_key.split(":").collect::<Vec<&str>>();
    if task_key_segments.len() == 0 {
        return None;
    }

    let task = tasks.get(task_key_segments[0])?;
    if task_key_segments.len() == 1 {
        return Some(task); // only looking for one segment so just return the task
    }

    if let Some(tasks) = &task.tasks {
        return get_task(tasks, task_key_segments[1..].join(":"));
    }

    Some(task)
}

fn get_all_tasks(
    tasks: &IndexMap<String, Task>,
    base_key: Option<String>,
) -> IndexMap<String, Task> {
    let base = base_key.map(|k| k + ":").unwrap_or("".to_string());
    let mut task_keys: IndexMap<String, Task> = IndexMap::new();
    for (key, task) in tasks.iter() {
        task_keys.insert(format!("{}{}", &base, key), task.clone());
        if let Some(tasks) = &task.tasks {
            task_keys.extend(get_all_tasks(&tasks, Some(format!("{}{}", &base, key))));
        }
    }
    task_keys
}