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 super::{HistoryKey, HistoryManager};
use crate::config::cli_config::CliConfig;
use dialoguer::BasicHistory;
use dialoguer::History;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;

/// A [`HistoryManager`] implementation that persists history to disk.
///
/// History files are stored alongside the application's configuration file,
/// named `.{key}_history` (e.g., `.tenant_history`).
///
/// Errors during read or write are silently ignored — history is a
/// best-effort convenience feature and should never interrupt the CLI flow.
pub struct FileHistoryManager;

impl HistoryManager for FileHistoryManager {
    /// Loads history from disk for the given [`HistoryKey`].
    ///
    /// Returns an empty [`BasicHistory`] if the file does not exist,
    /// cannot be opened, or contains no valid entries.
    fn load(&self, key: HistoryKey) -> BasicHistory {
        let mut history = BasicHistory::new().max_entries(10).no_duplicates(true);

        let Ok(file) = fs::File::open(get_history_path(key)) else {
            return history;
        };

        BufReader::new(file)
            .lines()
            .map_while(Result::ok)
            .filter(|line| !line.trim().is_empty())
            .for_each(|line| {
                history.write(&line);
            });

        history
    }

    /// Saves the current history entries to disk for the given [`HistoryKey`].
    ///
    /// Creates parent directories if they do not exist. The file is
    /// truncated and rewritten on each save. Entries are written in
    /// chronological order (oldest first).
    ///
    /// Silently returns on any I/O error.
    fn save(&self, key: HistoryKey, history: &BasicHistory) {
        let path = get_history_path(key);

        if let Some(parent) = path.parent() {
            let _ = fs::create_dir_all(parent);
        }

        let Ok(mut file) = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(path)
        else {
            return;
        };

        for entry in collect_entries(history).iter().rev() {
            let _ = writeln!(file, "{}", entry);
        }
    }
}

/// Returns the filesystem path for the history file associated with `key`.
///
/// The file is placed in the same directory as the application config file,
/// falling back to the current directory if the config path cannot be determined.
fn get_history_path(key: HistoryKey) -> PathBuf {
    CliConfig::get_path()
        .map(|p| p.parent().unwrap().to_path_buf())
        .unwrap_or_else(|_| PathBuf::from("."))
        .join(format!(".{}_history", key.as_str()))
}

/// Collects all entries from a [`BasicHistory`] into a [`Vec<String>`].
///
/// Iterates by index until [`History::read`] returns `None`,
/// indicating no more entries are available.
fn collect_entries(history: &BasicHistory) -> Vec<String> {
    (0..)
        .map(|i| History::<String>::read(history, i))
        .take_while(Option::is_some)
        .flatten()
        .collect()
}