homeboy 0.76.0

CLI for multi-component deployment and development workflow automation
Documentation
use serde::Serialize;

use crate::error::Result;
use crate::output::{CreateOutput, EntityCrudOutput, MergeOutput, RemoveResult};

use super::{calculate_deploy_readiness, collect_status, list, load, Project};

#[derive(Debug, Clone, Serialize)]
pub struct ProjectListItem {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub domain: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProjectComponentVersion {
    pub component_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version_source: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProjectShowReport {
    pub project: Project,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hint: Option<String>,
    pub deploy_ready: bool,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub deploy_blockers: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProjectListReport {
    pub projects: Vec<ProjectListItem>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hint: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProjectStatusReport {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub health: Option<crate::health::ServerHealth>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub component_versions: Option<Vec<ProjectComponentVersion>>,
}

#[derive(Debug, Default, Clone, Serialize)]
pub struct ProjectReportExtra {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub projects: Option<Vec<ProjectListItem>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub components: Option<crate::project::ProjectComponentsOutput>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pin: Option<crate::project::ProjectPinOutput>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub removed: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deploy_ready: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deploy_blockers: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub health: Option<crate::health::ServerHealth>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub component_versions: Option<Vec<ProjectComponentVersion>>,
}

pub type ProjectReportOutput = EntityCrudOutput<Project, ProjectReportExtra>;

pub fn list_report() -> Result<ProjectListReport> {
    let projects = list()?;

    let items: Vec<ProjectListItem> = projects
        .into_iter()
        .map(|p| ProjectListItem {
            id: p.id,
            domain: p.domain,
        })
        .collect();

    let hint = if items.is_empty() {
        Some(
            "No projects configured. Run 'homeboy status --full' to see project context"
                .to_string(),
        )
    } else {
        None
    };

    Ok(ProjectListReport {
        projects: items,
        hint,
    })
}

pub fn show_report(project_id: &str) -> Result<ProjectShowReport> {
    let project = load(project_id)?;

    let hint = if project.server_id.is_none() {
        Some(
            "Local project: Commands execute on this machine. Only deploy requires a server."
                .to_string(),
        )
    } else if project.components.is_empty() {
        Some(format!(
            "No components linked. Use: homeboy project components add {} <component-id> or homeboy project components attach-path {} <component-id> <path>",
            project.id,
            project.id
        ))
    } else {
        None
    };

    let (deploy_ready, deploy_blockers) = calculate_deploy_readiness(&project);

    Ok(ProjectShowReport {
        project,
        hint,
        deploy_ready,
        deploy_blockers,
    })
}

pub fn status_report(project_id: &str, health_only: bool) -> Result<ProjectStatusReport> {
    load(project_id)?;

    let snapshot = collect_status(project_id, health_only);
    let component_versions = snapshot.component_versions.map(|versions| {
        versions
            .into_iter()
            .map(|version| ProjectComponentVersion {
                component_id: version.component_id,
                version: version.version,
                version_source: version.version_source,
            })
            .collect()
    });

    Ok(ProjectStatusReport {
        health: snapshot.health,
        component_versions,
    })
}

pub fn build_list_output(report: ProjectListReport) -> ProjectReportOutput {
    ProjectReportOutput {
        command: "project.list".to_string(),
        hint: report.hint,
        extra: ProjectReportExtra {
            projects: Some(report.projects),
            ..Default::default()
        },
        ..Default::default()
    }
}

pub fn build_show_output(report: ProjectShowReport) -> ProjectReportOutput {
    ProjectReportOutput {
        command: "project.show".to_string(),
        id: Some(report.project.id.clone()),
        entity: Some(report.project),
        hint: report.hint,
        extra: ProjectReportExtra {
            deploy_ready: Some(report.deploy_ready),
            deploy_blockers: if report.deploy_blockers.is_empty() {
                None
            } else {
                Some(report.deploy_blockers)
            },
            ..Default::default()
        },
        ..Default::default()
    }
}

pub fn build_create_output(result: CreateOutput<Project>) -> (ProjectReportOutput, i32) {
    match result {
        CreateOutput::Single(result) => (
            ProjectReportOutput {
                command: "project.create".to_string(),
                id: Some(result.id),
                entity: Some(result.entity),
                ..Default::default()
            },
            0,
        ),
        CreateOutput::Bulk(summary) => {
            let exit_code = summary.exit_code();
            (
                ProjectReportOutput {
                    command: "project.create".to_string(),
                    import: Some(summary),
                    ..Default::default()
                },
                exit_code,
            )
        }
    }
}

pub fn build_set_output(result: MergeOutput) -> Result<(ProjectReportOutput, i32)> {
    match result {
        MergeOutput::Single(result) => Ok((
            ProjectReportOutput {
                command: "project.set".to_string(),
                id: Some(result.id.clone()),
                entity: Some(load(&result.id)?),
                updated_fields: result.updated_fields,
                ..Default::default()
            },
            0,
        )),
        MergeOutput::Bulk(summary) => {
            let exit_code = summary.exit_code();
            Ok((
                ProjectReportOutput {
                    command: "project.set".to_string(),
                    batch: Some(summary),
                    ..Default::default()
                },
                exit_code,
            ))
        }
    }
}

pub fn build_remove_output(result: RemoveResult) -> Result<ProjectReportOutput> {
    Ok(ProjectReportOutput {
        command: "project.remove".to_string(),
        id: Some(result.id.clone()),
        entity: Some(load(&result.id)?),
        extra: ProjectReportExtra {
            removed: Some(result.removed_from),
            ..Default::default()
        },
        ..Default::default()
    })
}

pub fn build_rename_output(project: Project) -> ProjectReportOutput {
    ProjectReportOutput {
        command: "project.rename".to_string(),
        id: Some(project.id.clone()),
        entity: Some(project),
        updated_fields: vec!["id".to_string()],
        ..Default::default()
    }
}

pub fn build_delete_output(project_id: &str) -> ProjectReportOutput {
    ProjectReportOutput {
        command: "project.delete".to_string(),
        id: Some(project_id.to_string()),
        deleted: vec![project_id.to_string()],
        ..Default::default()
    }
}

pub fn build_components_output(
    project_id: &str,
    action: &str,
    components: crate::project::ProjectComponentsOutput,
) -> ProjectReportOutput {
    ProjectReportOutput {
        command: format!("project.components.{action}"),
        id: Some(project_id.to_string()),
        updated_fields: vec!["componentIds".to_string()],
        extra: ProjectReportExtra {
            components: Some(components),
            ..Default::default()
        },
        ..Default::default()
    }
}

pub fn build_pin_output(
    command: &str,
    project_id: &str,
    pin: crate::project::ProjectPinOutput,
) -> ProjectReportOutput {
    ProjectReportOutput {
        command: command.to_string(),
        id: Some(project_id.to_string()),
        extra: ProjectReportExtra {
            pin: Some(pin),
            ..Default::default()
        },
        ..Default::default()
    }
}

pub fn build_status_output(project_id: &str, report: ProjectStatusReport) -> ProjectReportOutput {
    ProjectReportOutput {
        command: "project.status".to_string(),
        id: Some(project_id.to_string()),
        extra: ProjectReportExtra {
            health: report.health,
            component_versions: report.component_versions,
            ..Default::default()
        },
        ..Default::default()
    }
}