cufflink-cli 0.8.31

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

const WORKSPACE_FILE: &str = "cufflink-workspace.toml";

#[derive(Debug, Deserialize)]
pub struct WorkspaceConfig {
    pub workspace: WorkspaceInfo,
    #[serde(default)]
    pub services: Vec<WorkspaceService>,
    #[serde(default)]
    pub environments: HashMap<String, WorkspaceEnv>,
}

#[derive(Debug, Deserialize)]
pub struct WorkspaceInfo {
    pub name: String,
}

#[derive(Debug, Deserialize)]
pub struct WorkspaceService {
    pub name: String,
    pub path: String,
    #[serde(default)]
    pub seed: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct WorkspaceEnv {
    #[serde(default)]
    pub deploy: Option<Vec<String>>,
    #[serde(default)]
    pub seed: Option<Vec<String>>,
}

impl WorkspaceConfig {
    pub fn find_and_load() -> eyre::Result<(Self, PathBuf)> {
        let mut dir = std::env::current_dir()?;
        loop {
            let candidate = dir.join(WORKSPACE_FILE);
            if candidate.is_file() {
                let content = std::fs::read_to_string(&candidate)?;
                let config: WorkspaceConfig = toml::from_str(&content)
                    .map_err(|e| eyre::eyre!("Failed to parse {}: {}", WORKSPACE_FILE, e))?;
                return Ok((config, dir));
            }
            if !dir.pop() {
                break;
            }
        }
        eyre::bail!(
            "No {} found in current directory or any parent",
            WORKSPACE_FILE
        )
    }

    pub fn deploy_list(&self, env: Option<&str>) -> Vec<&WorkspaceService> {
        let env_config = env.and_then(|e| self.environments.get(e));
        let deploy_names = env_config.and_then(|e| e.deploy.as_ref());

        match deploy_names {
            Some(names) => self
                .services
                .iter()
                .filter(|s| names.iter().any(|n| n == &s.name))
                .collect(),
            None => self.services.iter().collect(),
        }
    }

    pub fn seed_list(&self, env: Option<&str>) -> Vec<&WorkspaceService> {
        let env_config = env.and_then(|e| self.environments.get(e));
        let seed_names = env_config.and_then(|e| e.seed.as_ref());

        match seed_names {
            Some(names) => self
                .services
                .iter()
                .filter(|s| names.iter().any(|n| n == &s.name) && s.seed.is_some())
                .collect(),
            None => Vec::new(),
        }
    }

    pub fn validate_paths(&self, root: &Path) -> eyre::Result<()> {
        for svc in &self.services {
            let path = root.join(&svc.path);
            if !path.is_dir() {
                eyre::bail!(
                    "Service '{}' path '{}' does not exist",
                    svc.name,
                    path.display()
                );
            }
            if let Some(ref seed) = svc.seed {
                let seed_path = root.join(seed);
                if !seed_path.is_file() {
                    eyre::bail!(
                        "Service '{}' seed file '{}' does not exist",
                        svc.name,
                        seed_path.display()
                    );
                }
            }
        }
        Ok(())
    }
}