eure-cli 0.1.9

Command-line tool for Eure format conversion and validation
use std::fs;
use std::io::{self, Read};
use std::sync::Arc;

use anyhow::anyhow;
use clap::ValueEnum;
use eure::query::{
    CacheOptions, Glob, GlobResult, TextFile, TextFileContent, fetch_url, fetch_url_cached,
};
use eure::query_flow::{DurabilityLevel, Query, QueryError, QueryRuntime};

/// Read input from file path or stdin.
/// - `None` or `Some("-")` reads from stdin
/// - `Some(path)` reads from file
pub fn read_input(file: Option<&str>) -> Result<String, String> {
    match file {
        None | Some("-") => {
            let mut buffer = String::new();
            io::stdin()
                .read_to_string(&mut buffer)
                .map_err(|e| format!("Error reading from stdin: {e}"))?;
            Ok(buffer)
        }
        Some(path) => fs::read_to_string(path).map_err(|e| format!("Error reading file: {e}")),
    }
}

/// Helper to get display path for error messages
pub fn display_path(file: Option<&str>) -> &str {
    file.unwrap_or("<stdin>")
}

/// Variant representation format for JSON conversion
#[derive(ValueEnum, Clone, Debug)]
pub enum VariantFormat {
    /// Default: {"variant-name": {...}}
    External,
    /// {"type": "variant-name", ...fields...}
    Internal,
    /// {"type": "variant-name", "content": {...}}
    Adjacent,
    /// Just the content without variant information
    Untagged,
}

/// Handle results from queries wrapped with `WithFormattedError`.
///
/// - `Ok(Ok(value))` - returns the value
/// - `Ok(Err(formatted))` - prints the formatted error and exits
/// - `Err(e)` - prints the system error and exits
pub fn handle_formatted_error<T>(
    result: Result<Arc<Result<Arc<T>, String>>, QueryError>,
) -> Arc<T> {
    match result {
        Ok(inner) => match inner.as_ref() {
            Ok(value) => value.clone(),
            Err(formatted_error) => {
                eprintln!("{formatted_error}");
                std::process::exit(1);
            }
        },
        Err(e) => {
            eprintln!("Error: {e}");
            std::process::exit(1);
        }
    }
}

/// Handle query errors by printing the error message and exiting.
///
/// Use this for queries that don't produce formatted error reports
/// (e.g., tolerant parsing or non-Eure formats).
pub fn handle_query_error(e: QueryError) -> ! {
    eprintln!("Error: {e}");
    std::process::exit(1);
}

/// Run a query with automatic file loading on suspend.
///
/// This helper enables single-query patterns for CLI commands by automatically
/// loading files when the query suspends waiting for assets.
///
/// Uses query-flow's suspend/resume mechanism:
/// 1. Execute query
/// 2. If query suspends (waiting for assets), load pending files from disk or network
/// 3. Retry query until it completes or errors
///
/// When `cache_opts` is provided, remote files are fetched using the cache.
/// Pass `None` for cache_opts to fetch remote files without caching.
pub fn run_query_with_file_loading_cached<Q, R>(
    runtime: &QueryRuntime,
    query: Q,
    cache_opts: Option<&CacheOptions>,
) -> Result<Arc<R>, QueryError>
where
    Q: Query<Output = R> + Clone,
{
    loop {
        match runtime.query(query.clone()) {
            Ok(result) => return Ok(result),
            Err(QueryError::Suspend { .. }) => {
                // Load pending file assets from disk or network
                for pending in runtime.pending_assets() {
                    if let Some(file) = pending.key::<TextFile>() {
                        match file {
                            TextFile::Local(path) => {
                                let path_ref = path.as_ref();
                                let content = fs::read_to_string(path_ref).map_err(|e| {
                                    std::io::Error::new(
                                        e.kind(),
                                        format!("{}: {}", path_ref.display(), e),
                                    )
                                })?;
                                runtime.resolve_asset(
                                    file.clone(),
                                    TextFileContent(content),
                                    DurabilityLevel::Static,
                                );
                            }
                            TextFile::Remote(url) => {
                                // Use cached fetch when cache options are provided
                                let result = if let Some(opts) = cache_opts {
                                    fetch_url_cached(url, opts)
                                } else {
                                    fetch_url(url)
                                };
                                match result {
                                    Ok(content) => {
                                        runtime.resolve_asset(
                                            file.clone(),
                                            content,
                                            DurabilityLevel::Static,
                                        );
                                    }
                                    Err(e) => {
                                        runtime.resolve_asset_error::<TextFile>(
                                            file.clone(),
                                            anyhow!("Failed to fetch {}: {}", url, e),
                                            DurabilityLevel::Static,
                                        );
                                    }
                                }
                            }
                        }
                    } else if let Some(glob_key) = pending.key::<Glob>() {
                        // Expand glob pattern on filesystem
                        let pattern = glob_key.full_pattern();
                        let pattern_str = pattern.to_string_lossy();
                        let files: Vec<TextFile> = glob::glob(&pattern_str)
                            .into_iter()
                            .flat_map(|paths| paths.flatten().map(TextFile::from_path))
                            .collect();
                        runtime.resolve_asset(
                            glob_key.clone(),
                            GlobResult(files),
                            DurabilityLevel::Static,
                        );
                    }
                }
            }
            Err(e) => return Err(e),
        }
    }
}