fundaia 0.52.2

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use anyhow::Result;
use owo_colors::OwoColorize;

use crate::api::client::{ApiClient, NewWebhook, WebhookChange};
use crate::api::models::{Webhook, WebhookDelivery};
use crate::exit;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;

pub async fn list(client: &ApiClient, project_reference: &str, format: Format) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    let answer = client.webhooks(&project.id).await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    let mut table = Table::new(&["aviso", "destino", "formato", "eventos", "último envío"]);
    for webhook in &answer.body.webhooks {
        table.push(vec![
            Cell::styled(
                sanitise::plain(&webhook.name),
                if webhook.enabled {
                    theme::strong()
                } else {
                    theme::muted()
                },
            ),
            Cell::styled(sanitise::plain(&webhook.url_hint), theme::muted()),
            Cell::plain(format_word(&webhook.format)),
            Cell::plain(events_word(webhook, answer.body.events.len())),
            delivery_cell(webhook),
        ]);
    }

    table.print_or(format_args!(
        "`{}` no tiene avisos. fundaia webhook add {} <url>",
        project.name, project.slug
    ));
    Ok(())
}

pub struct AddRequest<'a> {
    pub project: &'a str,
    pub url: &'a str,
    pub name: Option<&'a str>,
    pub format: Option<&'a str>,
    pub events: &'a [String],
}

pub async fn add(client: &ApiClient, request: AddRequest<'_>, format: Format) -> Result<()> {
    let project = client.find_project(request.project).await?;
    let answer = client
        .create_webhook(
            &project.id,
            &NewWebhook {
                url: request.url,
                name: request.name,
                format: request.format,
                events: request.events,
            },
        )
        .await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    let created = &answer.body;
    println!();
    println!(
        "  {} {} avisará a {} ({})",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&project.name).style(theme::strong()),
        sanitise::plain(&created.webhook.name).style(theme::strong()),
        sanitise::plain(&created.webhook.url_hint).style(theme::muted()),
    );
    if created.webhook.format == "json" {
        println!(
            "  {} {}",
            "Secreto para comprobar la firma (X-Fundaia-Signature), no se volverá a mostrar:"
                .style(theme::muted()),
            created.signing_secret.style(theme::accent()),
        );
    }
    println!(
        "  {}",
        format!(
            "fundaia webhook test {} {}",
            project.slug,
            shell_word(&created.webhook.name)
        )
        .style(theme::muted())
    );
    println!();
    Ok(())
}

pub struct SetRequest<'a> {
    pub project: &'a str,
    pub webhook: &'a str,
    pub name: Option<&'a str>,
    pub events: Option<&'a [String]>,
    pub enabled: Option<bool>,
}

pub async fn set(client: &ApiClient, request: SetRequest<'_>, format: Format) -> Result<()> {
    if request.name.is_none() && request.events.is_none() && request.enabled.is_none() {
        return Err(exit::Refused::invalid(
            "nada que cambiar: usa --name, --events, --pause o --resume",
        )
        .into());
    }

    let project = client.find_project(request.project).await?;
    let answer = client
        .update_webhook(
            &project.id,
            request.webhook,
            &WebhookChange {
                name: request.name,
                events: request.events,
                enabled: request.enabled,
            },
        )
        .await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    let webhook = &answer.body.webhook;
    println!(
        "  {} {} {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&webhook.name).style(theme::strong()),
        if webhook.enabled {
            format!("avisa de: {}", webhook.events.join(", "))
        } else {
            "en pausa".to_owned()
        }
        .style(theme::muted()),
    );
    Ok(())
}

pub async fn remove(
    client: &ApiClient,
    project_reference: &str,
    webhook: &str,
    format: Format,
) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    client.delete_webhook(&project.id, webhook).await?;

    if format.is_json() {
        crate::output::emit(&serde_json::json!({ "removed": webhook }));
        return Ok(());
    }

    println!(
        "  {} {} ya no avisa a {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&project.name).style(theme::strong()),
        sanitise::plain(webhook).style(theme::strong()),
    );
    Ok(())
}

pub async fn test(
    client: &ApiClient,
    project_reference: &str,
    webhook: &str,
    format: Format,
) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    let answer = client.test_webhook(&project.id, webhook).await?;
    let delivery = &answer.body.delivery;

    if !format.answered_in_json(&answer.raw) {
        if delivery.delivered {
            println!(
                "  {} {} {}",
                theme::CHECK.style(theme::success()),
                "Prueba entregada a".style(theme::muted()),
                sanitise::plain(webhook).style(theme::strong()),
            );
        } else {
            println!(
                "  {} {}",
                theme::CROSS.style(theme::danger()),
                failure_sentence(delivery),
            );
        }
    }

    if delivery.delivered {
        Ok(())
    } else {
        Err(exit::Refused::upstream(failure_sentence(delivery)).into())
    }
}

fn failure_sentence(delivery: &WebhookDelivery) -> String {
    sanitise::plain(delivery.error.as_deref().unwrap_or("no llegó"))
}

fn format_word(format: &str) -> &str {
    match format {
        "slack" => "Slack",
        "discord" => "Discord",
        _ => "JSON firmado",
    }
}

fn events_word(webhook: &Webhook, known: usize) -> String {
    if !webhook.enabled {
        return "en pausa".to_owned();
    }
    if known > 0 && webhook.events.len() == known {
        return "todos".to_owned();
    }
    webhook.events.join(", ")
}

fn delivery_cell(webhook: &Webhook) -> Cell {
    match &webhook.last_delivery {
        None => Cell::styled("—", theme::muted()),
        Some(delivery) => {
            let when = time::relative_time(&delivery.at).unwrap_or_else(|| delivery.at.clone());
            if delivery.delivered {
                Cell::styled(format!("{} {when}", theme::CHECK), theme::success())
            } else {
                Cell::styled(format!("{} {when}", theme::CROSS), theme::danger())
            }
        }
    }
}

fn shell_word(name: &str) -> String {
    if name
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
    {
        name.to_owned()
    } else {
        format!("'{}'", name.replace('\'', "'\\''"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_should_quote_a_name_with_a_space() {
        assert_eq!(shell_word("Slack 2"), "'Slack 2'");
    }

    #[test]
    fn it_should_leave_a_plain_name_bare() {
        assert_eq!(shell_word("Discord"), "Discord");
    }
}