smbcloud-cli 0.4.11

smbCloud command line interface.
use crate::{
    cli::CommandResult,
    client,
    cloud_auth::{
        auth_app::{
            create_auth_app, delete_auth_app, get_auth_app, get_auth_apps, update_auth_app,
        },
        cli::Commands,
        render::{print_auth_app_detail, print_auth_apps},
    },
    mail::current_project::{resolve_optional_project_id, resolve_required_project_id},
    token::get_smb_token::get_smb_token,
    ui::{fail_message, fail_symbol, prompt::confirm_delete, succeed_message, succeed_symbol},
};
use anyhow::{anyhow, Result};
use smbcloud_model::app_auth::{AuthAppCreate, AuthAppUpdate};
use smbcloud_network::environment::Environment;
use spinners::Spinner;

pub async fn process_cloud_auth(env: Environment, commands: Commands) -> Result<CommandResult> {
    match commands {
        Commands::List { project_id } => process_auth_app_list(env, project_id).await,
        Commands::Show { id } => process_auth_app_show(env, id).await,
        Commands::New {
            name,
            project_id,
            support_email,
        } => process_auth_app_new(env, name, project_id, support_email).await,
        Commands::Update {
            id,
            name,
            support_email,
        } => process_auth_app_update(env, id, name, support_email).await,
        Commands::Delete { id } => process_auth_app_delete(env, id).await,
    }
}

async fn process_auth_app_list(
    env: Environment,
    project_id: Option<String>,
) -> Result<CommandResult> {
    let access_token = get_smb_token(env)?;
    let resolved_project_id = resolve_optional_project_id(env, normalize_optional(project_id))?;
    let mut spinner = loading_spinner("Loading Auth apps");

    let auth_apps = get_auth_apps(env, client(), access_token, resolved_project_id)
        .await
        .map_err(api_error)?;

    spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
    print_auth_apps(&auth_apps);

    Ok(done_result(if auth_apps.is_empty() {
        "No Auth apps found."
    } else {
        "Done."
    }))
}

async fn process_auth_app_show(env: Environment, id: String) -> Result<CommandResult> {
    let access_token = get_smb_token(env)?;
    let auth_app_id = normalize_required("Auth app id", id)?;
    let mut spinner = loading_spinner("Loading Auth app");

    let auth_app = get_auth_app(env, client(), access_token, auth_app_id)
        .await
        .map_err(api_error)?;

    spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
    print_auth_app_detail(&auth_app);

    Ok(done_result("Done."))
}

async fn process_auth_app_new(
    env: Environment,
    name: String,
    project_id: Option<String>,
    support_email: Option<String>,
) -> Result<CommandResult> {
    let access_token = get_smb_token(env)?;
    let auth_app = AuthAppCreate {
        name: normalize_required("name", name)?,
        project_id: resolve_required_project_id(env, normalize_optional(project_id))?,
        support_email: normalize_optional(support_email),
    };
    let mut spinner = loading_spinner("Creating Auth app");

    let auth_app = create_auth_app(env, client(), access_token, auth_app)
        .await
        .map_err(api_error)?;

    spinner.stop_and_persist(&succeed_symbol(), succeed_message("Created."));
    print_auth_app_detail(&auth_app);

    Ok(done_result("Auth app created."))
}

async fn process_auth_app_update(
    env: Environment,
    id: String,
    name: Option<String>,
    support_email: Option<String>,
) -> Result<CommandResult> {
    let access_token = get_smb_token(env)?;
    let auth_app_id = normalize_required("Auth app id", id)?;
    let update = AuthAppUpdate {
        name: normalize_optional(name),
        support_email: normalize_optional(support_email),
    };

    if update.is_empty() {
        return Err(anyhow!(
            "Specify at least one of `--name` or `--support-email`."
        ));
    }

    let mut spinner = loading_spinner("Updating Auth app");
    let auth_app = update_auth_app(env, client(), access_token, auth_app_id, update)
        .await
        .map_err(api_error)?;

    spinner.stop_and_persist(&succeed_symbol(), succeed_message("Updated."));
    print_auth_app_detail(&auth_app);

    Ok(done_result("Auth app updated."))
}

async fn process_auth_app_delete(env: Environment, id: String) -> Result<CommandResult> {
    let auth_app_id = normalize_required("Auth app id", id)?;
    let confirmed = confirm_delete(
        "Auth app deletion confirmation",
        &format!("Delete Auth app #{auth_app_id}"),
    )?;

    if !confirmed {
        return Ok(done_result("Cancelled."));
    }

    let access_token = get_smb_token(env)?;
    let spinner = loading_spinner("Deleting Auth app");

    match delete_auth_app(env, client(), access_token, auth_app_id).await {
        Ok(()) => Ok(CommandResult {
            spinner,
            symbol: succeed_symbol(),
            msg: succeed_message("Done. Auth app has been deleted."),
        }),
        Err(error) => Ok(CommandResult {
            spinner,
            symbol: fail_symbol(),
            msg: fail_message(&error.to_string()),
        }),
    }
}

fn loading_spinner(message: &str) -> Spinner {
    Spinner::new(
        spinners::Spinners::SimpleDotsScrolling,
        succeed_message(message),
    )
}

fn done_result(message: &str) -> CommandResult {
    CommandResult {
        spinner: loading_spinner("Done"),
        symbol: succeed_symbol(),
        msg: succeed_message(message),
    }
}

fn normalize_required(label: &str, value: String) -> Result<String> {
    let normalized_value = value.trim().to_string();
    if normalized_value.is_empty() {
        return Err(anyhow!("{} cannot be empty.", label));
    }
    Ok(normalized_value)
}

fn normalize_optional(value: Option<String>) -> Option<String> {
    value.and_then(|value| {
        let normalized_value = value.trim().to_string();
        if normalized_value.is_empty() {
            None
        } else {
            Some(normalized_value)
        }
    })
}

fn api_error(error: impl std::fmt::Display) -> anyhow::Error {
    anyhow!(error.to_string())
}