ez-token 0.1.0

CLI tool for generating OAuth2 access tokens via PKCE and Client Credentials for Microsoft Entra ID and Auth0
Documentation
use console::{Emoji, style};
use indicatif::{ProgressBar, ProgressStyle};
use miette::{IntoDiagnostic, Result};
use std::time::Duration;

/// A type alias for emojis with static lifetimes, used throughout the CLI output.
pub type TermEmoji = Emoji<'static, 'static>;

/// Named emoji variants used in CLI output.
///
/// Provides a type-safe alternative to scattering `Emoji("...", "...")` literals
/// across the codebase. Each variant has a text fallback for terminals that
/// do not support Unicode.
pub enum AppEmoji {
    /// ✨ Used for section headers.
    Sparkle,
    /// πŸ“‹ Used for success messages, typically when a token is copied to the clipboard.
    Clipboard,
    /// πŸš€ Used for action steps such as opening the browser.
    Rocket,
    /// πŸ’Ύ Used when configuration is saved to disk.
    Floppy,
}

impl AppEmoji {
    /// Returns the [`TermEmoji`] value for this variant.
    pub fn as_emoji(&self) -> TermEmoji {
        match self {
            AppEmoji::Sparkle => Emoji("✨ ", ":-)"),
            AppEmoji::Clipboard => Emoji("πŸ“‹ ", ""),
            AppEmoji::Rocket => Emoji("πŸš€ ", ""),
            AppEmoji::Floppy => Emoji("πŸ’Ύ ", ""),
        }
    }
}

/// Prints a bold green section header prefixed with a sparkle emoji.
///
/// # Example Output
/// ```text
/// ✨ OAuth2 Token Generator
/// ```
pub fn print_header(title: &str) {
    println!(
        "\n{} {}",
        AppEmoji::Sparkle.as_emoji(),
        style(title).bold().green()
    );
}

/// Prints a bold step message prefixed with the given emoji.
///
/// Used to indicate progress through a multi-step flow.
///
/// # Example Output
/// ```text
/// πŸš€ Opening browser...
/// ```
pub fn print_step(emoji: AppEmoji, message: &str) {
    println!("\n{} {}", emoji.as_emoji(), style(message).bold());
}

/// Prints a bold green success message prefixed with a clipboard emoji.
///
/// # Example Output
/// ```text
/// πŸ“‹ Token copied to clipboard!
/// ```
pub fn print_success(message: &str) {
    println!(
        "\n{} {}",
        AppEmoji::Clipboard.as_emoji(),
        style(message).bold().green()
    );
}

/// Prints a yellow warning message prefixed with `Warning:`.
///
/// Used for non-fatal issues such as clipboard access failures.
///
/// # Example Output
/// ```text
/// Warning: Could not access clipboard.
/// ```
pub fn print_warning(message: &str) {
    println!("{} {}", style("Warning:").yellow(), message);
}

/// Creates and starts an animated spinner with the given message.
///
/// The spinner ticks every 80ms and is displayed in blue. Call
/// [`finish_spinner_success`] or [`finish_spinner_error`] to stop it.
///
/// # Errors
///
/// Returns an error if the underlying spinner style template is invalid.
pub fn start_spinner(message: &str) -> Result<ProgressBar> {
    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
            .template("{spinner:.blue} {msg}")
            .into_diagnostic()?,
    );
    spinner.set_message(message.to_string());
    spinner.enable_steady_tick(Duration::from_millis(80));
    Ok(spinner)
}

/// Stops the spinner and displays a green checkmark with a success message.
///
/// # Example Output
/// ```text
/// βœ” Authentication successful!
/// ```
pub fn finish_spinner_success(spinner: &ProgressBar, message: &str) {
    spinner.finish_with_message(format!("{} {}", style("βœ”").green(), message));
}

/// Stops the spinner and displays a red cross with an error message.
///
/// # Example Output
/// ```text
/// ✘ Authentication failed!
/// ```
pub fn finish_spinner_error(spinner: &ProgressBar, message: &str) {
    spinner.finish_with_message(format!("{} {}", style("✘").red(), message));
}

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

    #[test]
    fn test_app_emoji_variants() {
        let sparkle = AppEmoji::Sparkle.as_emoji();
        let clipboard = AppEmoji::Clipboard.as_emoji();
        let rocket = AppEmoji::Rocket.as_emoji();
        let floppy = AppEmoji::Floppy.as_emoji();

        assert!(!sparkle.to_string().is_empty());
        assert!(!clipboard.to_string().is_empty());
        assert!(!rocket.to_string().is_empty());
        assert!(!floppy.to_string().is_empty());
    }

    #[test]
    fn test_start_spinner_template_valid() {
        let spinner_result = start_spinner("Fetching token...");
        assert!(spinner_result.is_ok(), "Spinner template should be valid");
    }

    #[test]
    fn test_spinner_success_lifecycle() {
        let spinner = start_spinner("Working...").unwrap();
        assert!(!spinner.is_finished());

        finish_spinner_success(&spinner, "Done!");
        assert!(spinner.is_finished());
    }

    #[test]
    fn test_spinner_error_lifecycle() {
        let spinner = start_spinner("Working...").unwrap();
        assert!(!spinner.is_finished());

        finish_spinner_error(&spinner, "Failed!");
        assert!(spinner.is_finished());
    }
}