xberg-cli 1.0.0-rc.4

Command-line interface for Xberg document intelligence
//! CLI input resolution and validation helpers.

use anyhow::{Context, Result};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use std::path::PathBuf;

use crate::commands::{
    BatchInputFormat, ExtractInputSource, load_batch_input_manifest, uri_to_local_path, validate_batch_paths,
    validate_file_exists,
};

/// Apply inline JSON or base64 JSON overrides to an extraction config.
pub(crate) fn apply_json_overrides(
    config: &mut xberg::ExtractionConfig,
    config_json: Option<String>,
    config_json_base64: Option<String>,
) -> Result<()> {
    if let Some(json_str) = config_json {
        let json_value: serde_json::Value =
            serde_json::from_str(&json_str).context("Failed to parse --config-json as JSON")?;
        *config =
            merge_json_into_config(config, json_value).context("Failed to merge --config-json with file config")?;
    } else if let Some(base64_str) = config_json_base64 {
        let json_bytes = STANDARD
            .decode(&base64_str)
            .context("Failed to decode base64 in --config-json-base64")?;
        let json_str = String::from_utf8(json_bytes).context("Base64-decoded content is not valid UTF-8")?;
        let json_value: serde_json::Value =
            serde_json::from_str(&json_str).context("Failed to parse decoded --config-json-base64 as JSON")?;
        *config = merge_json_into_config(config, json_value)
            .context("Failed to merge --config-json-base64 with file config")?;
    }
    Ok(())
}

pub(crate) fn resolve_extract_input(
    uri: Option<String>,
    url: Option<String>,
    stdin: bool,
) -> Result<ExtractInputSource> {
    match (uri, url, stdin) {
        (Some(uri), None, false) => Ok(ExtractInputSource::Uri(uri)),
        (None, Some(url), false) => Ok(ExtractInputSource::Uri(url)),
        (None, None, true) => Ok(ExtractInputSource::Stdin),
        _ => anyhow::bail!("Provide exactly one extraction input: URI, --url, or --stdin."),
    }
}

pub(crate) fn validate_extract_input(input: &ExtractInputSource) -> Result<()> {
    match input {
        ExtractInputSource::Stdin => Ok(()),
        ExtractInputSource::Uri(uri) => {
            if is_remote_uri(uri) {
                return Ok(());
            }
            let path = uri_to_local_path(uri)?;
            validate_file_exists(&path)
        }
    }
}

pub(crate) fn resolve_batch_inputs(
    paths: Vec<PathBuf>,
    input: Option<PathBuf>,
    input_format: Option<BatchInputFormat>,
) -> Result<Vec<String>> {
    let mut uris: Vec<String> = paths
        .into_iter()
        .map(|path| path.to_string_lossy().to_string())
        .collect();

    if let Some(input_path) = input {
        let format = input_format.unwrap_or_else(|| infer_batch_input_format(&input_path));
        uris.extend(load_batch_input_manifest(&input_path, format)?);
    }

    if uris.is_empty() {
        anyhow::bail!("No files provided for batch extraction. Provide paths or --input.");
    }

    Ok(uris)
}

pub(crate) fn validate_batch_input_uris(uris: &[String]) -> Result<()> {
    let local_paths: Vec<PathBuf> = uris
        .iter()
        .filter(|uri| !is_remote_uri(uri))
        .map(|uri| uri_to_local_path(uri))
        .collect::<Result<Vec<_>>>()?;
    if local_paths.is_empty() {
        return Ok(());
    }
    validate_batch_paths(&local_paths)
}

/// Merge a JSON value into an existing extraction config via field-by-field override.
fn merge_json_into_config(
    base_config: &xberg::ExtractionConfig,
    json_value: serde_json::Value,
) -> Result<xberg::ExtractionConfig> {
    let json_str = serde_json::to_string(&json_value).map_err(|e| anyhow::anyhow!("{}", e))?;
    xberg::core::config::merge::merge_config_json(base_config, &json_str).map_err(|e| anyhow::anyhow!("{}", e))
}

fn infer_batch_input_format(path: &std::path::Path) -> BatchInputFormat {
    match path.extension().and_then(|ext| ext.to_str()) {
        Some(ext) if ext.eq_ignore_ascii_case("jsonl") || ext.eq_ignore_ascii_case("ndjson") => BatchInputFormat::Jsonl,
        _ => BatchInputFormat::Json,
    }
}

fn is_remote_uri(uri: &str) -> bool {
    uri.starts_with("http://") || uri.starts_with("https://")
}