epcis-cli 0.2.0

Command line interface for EPCIS event hashing and identifier translation
use clap::Parser;
use epcis_hash::{canonicalize_json, canonicalize_xml, compute_hash_from_prehash};
use epcis_translate::{
    Cpi, Gdti, Giai, Ginc, Grai, Gsin, Gsrn, Gsrnp, Itip, Lgtin, Pgln, Sgcn, Sgln, Sgtin, Sscc,
    Upui,
};
use std::fs;
use std::io::{self, Read};

#[derive(Parser, Debug)]
#[command(
    author = "Duma Mtungwa <duma@matangaresearch.com>",
    version,
    about = "EPCIS SDK Command Line Interface",
    long_about = "A command line tool to generate canonical pre-hashes, final SHA-256 hashes, and translate GS1 identifiers between URN and Digital Link formats."
)]
struct Args {
    /// File path to parse (use "-" or omit to read from standard input)
    file: Option<String>,

    /// Print the canonical pre-hash string instead of the hash URN
    #[arg(short, long)]
    prehash: bool,

    /// Force input format: "json" or "xml"
    #[arg(short, long)]
    enforce_format: Option<String>,

    /// Translate a GS1 key between EPC URN and Digital Link formats
    #[arg(short, long)]
    translate: Option<String>,

    /// The company prefix length to use for Digital Link translation
    #[arg(short = 'l', long, default_value_t = 7)]
    prefix_len: usize,

    /// Use legacy CBV 1.2 rules (omits user extensions from hashing)
    #[arg(long)]
    cbv_1_2: bool,

    /// The base URL to use when translating URN to Digital Link
    #[arg(long, default_value = "https://id.gs1.org")]
    base_url: String,
}

fn handle_translation(key: &str, prefix_len: usize, base_url: &str) -> Result<String, String> {
    if key.starts_with("urn:epc:class:lgtin:") {
        let lgtin = Lgtin::from_urn(key).map_err(|e| format!("Failed to parse LGTIN: {:?}", e))?;
        return Ok(lgtin.to_digital_link(base_url));
    }
    if key.starts_with("urn:epc:id:") {
        // Translate from URN to Digital Link
        let parts: Vec<&str> = key.split(':').collect();
        if parts.len() < 5 {
            return Err("Invalid URN format. Expected e.g. urn:epc:id:sgtin:...".to_string());
        }
        let scheme = parts[3];
        match scheme {
            "sgtin" => {
                let sgtin =
                    Sgtin::from_urn(key).map_err(|e| format!("Failed to parse SGTIN: {:?}", e))?;
                Ok(sgtin.to_digital_link(base_url))
            }
            "sscc" => {
                let sscc =
                    Sscc::from_urn(key).map_err(|e| format!("Failed to parse SSCC: {:?}", e))?;
                Ok(sscc.to_digital_link(base_url))
            }
            "sgln" => {
                let sgln =
                    Sgln::from_urn(key).map_err(|e| format!("Failed to parse SGLN: {:?}", e))?;
                Ok(sgln.to_digital_link(base_url))
            }
            "grai" => {
                let grai =
                    Grai::from_urn(key).map_err(|e| format!("Failed to parse GRAI: {:?}", e))?;
                Ok(grai.to_digital_link(base_url))
            }
            "giai" => {
                let giai =
                    Giai::from_urn(key).map_err(|e| format!("Failed to parse GIAI: {:?}", e))?;
                Ok(giai.to_digital_link(base_url))
            }
            "pgln" => {
                let pgln =
                    Pgln::from_urn(key).map_err(|e| format!("Failed to parse PGLN: {:?}", e))?;
                Ok(pgln.to_digital_link(base_url))
            }
            "gdti" => {
                let gdti =
                    Gdti::from_urn(key).map_err(|e| format!("Failed to parse GDTI: {:?}", e))?;
                Ok(gdti.to_digital_link(base_url))
            }
            "gsrn" => {
                let gsrn =
                    Gsrn::from_urn(key).map_err(|e| format!("Failed to parse GSRN: {:?}", e))?;
                Ok(gsrn.to_digital_link(base_url))
            }
            "gsrnp" => {
                let gsrnp =
                    Gsrnp::from_urn(key).map_err(|e| format!("Failed to parse GSRNP: {:?}", e))?;
                Ok(gsrnp.to_digital_link(base_url))
            }
            "sgcn" => {
                let sgcn =
                    Sgcn::from_urn(key).map_err(|e| format!("Failed to parse SGCN: {:?}", e))?;
                Ok(sgcn.to_digital_link(base_url))
            }
            "ginc" => {
                let ginc =
                    Ginc::from_urn(key).map_err(|e| format!("Failed to parse GINC: {:?}", e))?;
                Ok(ginc.to_digital_link(base_url))
            }
            "gsin" => {
                let gsin =
                    Gsin::from_urn(key).map_err(|e| format!("Failed to parse GSIN: {:?}", e))?;
                Ok(gsin.to_digital_link(base_url))
            }
            "itip" => {
                let itip =
                    Itip::from_urn(key).map_err(|e| format!("Failed to parse ITIP: {:?}", e))?;
                Ok(itip.to_digital_link(base_url))
            }
            "upui" => {
                let upui =
                    Upui::from_urn(key).map_err(|e| format!("Failed to parse UPUI: {:?}", e))?;
                Ok(upui.to_digital_link(base_url))
            }
            "cpi" => {
                let cpi =
                    Cpi::from_urn(key).map_err(|e| format!("Failed to parse CPI: {:?}", e))?;
                Ok(cpi.to_digital_link(base_url))
            }
            other => Err(format!("Unsupported URN scheme: {}", other)),
        }
    } else if key.starts_with("http") || key.contains('/') {
        // Translate from Digital Link to URN
        if key.contains("/01/") && key.contains("/235/") {
            let upui = Upui::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse UPUI DL: {:?}", e))?;
            Ok(upui.to_urn())
        } else if key.contains("/01/") && key.contains("/10/") {
            let lgtin = Lgtin::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse LGTIN DL: {:?}", e))?;
            Ok(lgtin.to_urn())
        } else if key.contains("/01/") {
            let sgtin = Sgtin::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse SGTIN DL: {:?}", e))?;
            Ok(sgtin.to_urn())
        } else if key.contains("/8006/") {
            let itip = Itip::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse ITIP DL: {:?}", e))?;
            Ok(itip.to_urn())
        } else if key.contains("/8010/") {
            let cpi = Cpi::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse CPI DL: {:?}", e))?;
            Ok(cpi.to_urn())
        } else if key.contains("/8017/") {
            let gsrnp = Gsrnp::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GSRNP DL: {:?}", e))?;
            Ok(gsrnp.to_urn())
        } else if key.contains("/8018/") {
            let gsrn = Gsrn::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GSRN DL: {:?}", e))?;
            Ok(gsrn.to_urn())
        } else if key.contains("/253/") {
            let gdti = Gdti::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GDTI DL: {:?}", e))?;
            Ok(gdti.to_urn())
        } else if key.contains("/255/") {
            let sgcn = Sgcn::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse SGCN DL: {:?}", e))?;
            Ok(sgcn.to_urn())
        } else if key.contains("/401/") {
            let ginc = Ginc::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GINC DL: {:?}", e))?;
            Ok(ginc.to_urn())
        } else if key.contains("/402/") {
            let gsin = Gsin::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GSIN DL: {:?}", e))?;
            Ok(gsin.to_urn())
        } else if key.contains("/417/") {
            let pgln = Pgln::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse PGLN DL: {:?}", e))?;
            Ok(pgln.to_urn())
        } else if key.contains("/00/") {
            let sscc = Sscc::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse SSCC DL: {:?}", e))?;
            Ok(sscc.to_urn())
        } else if key.contains("/414/") {
            let sgln = Sgln::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse SGLN DL: {:?}", e))?;
            Ok(sgln.to_urn())
        } else if key.contains("/8003/") {
            let grai = Grai::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GRAI DL: {:?}", e))?;
            Ok(grai.to_urn())
        } else if key.contains("/8004/") {
            let giai = Giai::from_digital_link(key, prefix_len)
                .map_err(|e| format!("Failed to parse GIAI DL: {:?}", e))?;
            Ok(giai.to_urn())
        } else {
            Err("Could not detect GS1 Application Identifier (AI) in Digital Link path (e.g. /01/, /00/, /414/, /417/, /253/, /255/, /401/, /402/, /8003/, /8004/, /8006/, /8010/, /8017/, /8018/)".to_string())
        }
    } else {
        Err("Invalid key format. Key must be an EPC URN (starts with urn:epc:id:) or GS1 Digital Link URL/path".to_string())
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();

    // 1. Handle Key Translation
    if let Some(ref key) = args.translate {
        match handle_translation(key, args.prefix_len, &args.base_url) {
            Ok(translated) => {
                println!("{}", translated);
                return Ok(());
            }
            Err(e) => {
                eprintln!("Translation Error: {}", e);
                std::process::exit(1);
            }
        }
    }

    // 2. Read Input Content
    let mut content = String::new();
    if let Some(ref filepath) = args.file {
        if filepath == "-" {
            io::stdin().read_to_string(&mut content)?;
        } else {
            content = fs::read_to_string(filepath)?;
        }
    } else {
        io::stdin().read_to_string(&mut content)?;
    }

    let trimmed = content.trim();
    if trimmed.is_empty() {
        eprintln!("Error: Input is empty.");
        std::process::exit(1);
    }

    // 3. Determine Format
    let is_xml = if let Some(ref format) = args.enforce_format {
        format.to_lowercase() == "xml"
    } else {
        trimmed.starts_with('<')
    };

    let is_cbv_2_0 = !args.cbv_1_2;

    // 4. Generate Pre-hashes
    let prehashes_str = if is_xml {
        canonicalize_xml(&content, is_cbv_2_0)?
    } else {
        let json_val = serde_json::from_str(&content)?;
        canonicalize_json(&json_val, is_cbv_2_0)?
    };

    // 5. Output
    if args.prehash {
        print!("{}", prehashes_str);
    } else {
        // Compute final hashes line by line
        for line in prehashes_str.lines() {
            if !line.trim().is_empty() {
                let hash = compute_hash_from_prehash(line);
                println!("{}", hash);
            }
        }
    }

    Ok(())
}