mod indexer;
use clap::Parser;
use indexer::Indexer;
use serde_json::Value;
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
about = env!("CARGO_PKG_DESCRIPTION"),
version = env!("CARGO_PKG_VERSION"),
propagate_version = true
)]
struct Cli {
#[arg(short = 'i', long = "input")]
input: Option<PathBuf>,
#[arg(short = 'o', long = "output")]
output: Option<PathBuf>,
#[arg(short='f', long = "with-field", value_parser = parse_field_spec)]
fields: Vec<(String, String)>,
#[arg(short = 'p', long = "pretty", default_value = "false")]
pretty: bool,
#[arg(short = 'q', long = "quiet", default_value = "false")]
quiet: bool,
#[arg(short = 'w', long = "workdir", default_value_os = "./work")]
workdir: PathBuf,
}
trait DefaultToStdin {
fn open(&self) -> Box<dyn BufRead>;
}
impl DefaultToStdin for Option<PathBuf> {
fn open(&self) -> Box<dyn BufRead> {
match self {
None => Box::new(BufReader::new(io::stdin())),
Some(filename) if filename == "-" => Box::new(BufReader::new(io::stdin())),
Some(filename) => {
let file = File::open(filename).expect("Unable to open input file");
Box::new(BufReader::new(file))
}
}
}
}
trait DefaultToStdout {
fn create(&self) -> Box<dyn Write>;
}
impl DefaultToStdout for Option<PathBuf> {
fn create(&self) -> Box<dyn Write> {
match self {
None => Box::new(BufWriter::new(io::stdout())),
Some(filename) if filename == "-" => Box::new(BufWriter::new(io::stdout())),
Some(filename) => {
let file = File::create(filename).expect("Unable to create output file");
Box::new(BufWriter::new(file))
}
}
}
}
fn parse_field_spec(s: &str) -> Result<(String, String), String> {
match s.find("=") {
Some(pos) => Ok((s[..pos].to_string(), s[pos + 1..].to_string())),
None => Ok((s.to_string(), s.to_string())),
}
}
fn main() -> io::Result<()> {
let args = Cli::parse();
let mut indexer = Indexer::new(args.workdir, args.fields);
let mut indexed_type_counts: HashMap<String, usize> = HashMap::new();
let mut ignored_type_counts: HashMap<String, usize> = HashMap::new();
for (line_num, line_result) in args.input.open().lines().enumerate() {
let line = line_result?;
let anno: Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(_) => {
eprintln!("Error parsing JSON on line {}", line_num + 1);
continue;
}
};
if let (Some(id), Some(body)) = (anno.get("id"), anno.get("body")) {
let anno_id = id.as_str().expect("Missing 'id' in annotation");
let opt_body = body.get("type").and_then(|v| v.as_str());
let body_type = opt_body.expect("Missing 'body.type' in annotation");
let mut indexed: bool = true;
match body_type {
"Document" => {
indexer.add_str("type", "intro");
indexer.index_anno(body);
}
"Division" => {
if let Some(tei_type) = body.get("tei:type").and_then(|v| v.as_str()) {
match tei_type {
"original" => indexer.store_text(&anno, "letterOriginalText"),
"translation" => indexer.store_text(&anno, "letterTranslatedText"),
"about" => indexer.store_text(&anno, "introText"),
_ => {}
}
}
}
"Entity" => {
if let Some(tei_type) = body.get("tei:type").and_then(|v| v.as_str()) {
match tei_type {
"artwork" => indexer.index_artwork(body),
"person" => indexer.index_person(body, anno_id),
_ => {}
}
}
}
"Letter" => {
indexer.add_str("type", "letter");
indexer.index_anno(body);
}
"Note" => {
if let Some(subtype) = body.get("subtype").and_then(|v| v.as_str()) {
if matches!(subtype, "notes" | "typednotes" | "langnotes") {
indexer.store_text(&anno, "letterNotesText");
}
}
}
"Reference" => {
if let Some(cref) = body.get("tei:cRef").and_then(|v| v.as_str()) {
if cref.starts_with("bible-") {
indexer.index_bible_citation(body, cref)
}
}
}
_ => {
indexed = false;
}
}
if !args.quiet {
let target_map = if indexed {
&mut indexed_type_counts
} else {
&mut ignored_type_counts
};
if let Some(count) = target_map.get_mut(body_type) {
*count += 1;
} else {
target_map.insert(body_type.to_string(), 1);
}
}
}
}
indexer.sort_fields();
let mut writer = args.output.create();
let json = if args.pretty {
serde_json::to_string_pretty(&indexer.root)
} else {
serde_json::to_string(&indexer.root)
}?;
writeln!(writer, "{}", json)?;
writer.flush()?;
if !args.quiet {
if !indexed_type_counts.is_empty() {
eprintln!(" ✅ Indexed annotations: {:?}", indexed_type_counts);
}
if !ignored_type_counts.is_empty() {
eprintln!(" ⚠️ Ignored annotations: {:?}", ignored_type_counts);
}
}
Ok(())
}