use std::io::BufRead;
use std::sync::LazyLock;
use regex::Regex;
use crate::error::{Error, Result};
use crate::output::writer::DocSink;
static REFERENCE_LINE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" \^ .*").unwrap());
pub fn process_cirrus_dump(input: impl BufRead, sink: &mut dyn DocSink) -> Result<u64> {
let mut docs = 0;
let mut lines = input.lines();
while let Some(index_line) = lines.next() {
let index_line = index_line?;
if index_line.trim().is_empty() {
continue;
}
let index: serde_json::Value = serde_json::from_str(&index_line)
.map_err(|e| Error::InvalidDump(format!("bad cirrus index line: {e}")))?;
let Some(content_line) = lines.next() else {
return Err(Error::InvalidDump(
"cirrus dump ends after an index line".to_string(),
));
};
let content: serde_json::Value = serde_json::from_str(&content_line?)
.map_err(|e| Error::InvalidDump(format!("bad cirrus content line: {e}")))?;
let doc_type = index["index"]["_type"].as_str().unwrap_or("");
if !matches!(doc_type, "page" | "_doc") || content["namespace"] != 0 {
continue;
}
let id = json_scalar(&index["index"]["_id"]);
let language = json_scalar(&content["language"]);
let revision = json_scalar(&content["version"]);
let title = content["title"].as_str().unwrap_or("");
let text = content["text"].as_str().unwrap_or("");
let text = REFERENCE_LINE.replace_all(text, "");
let doc = format!(
"<doc id=\"{id}\" url=\"http://it.wikipedia.org/wiki?curid={id}\" \
title=\"{title}\" language=\"{language}\" revision=\"{revision}\">\n\
{title}\n\n{text}\n</doc>\n"
);
sink.write_doc(&doc)?;
docs += 1;
}
Ok(docs)
}
fn json_scalar(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => String::new(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use std::io;
use super::*;
#[derive(Default)]
struct StringSink(String);
impl DocSink for StringSink {
fn write_doc(&mut self, doc: &str) -> io::Result<()> {
self.0.push_str(doc);
Ok(())
}
fn finish(&mut self) -> io::Result<()> {
Ok(())
}
}
fn dump() -> String {
[
r#"{"index":{"_type":"page","_id":"3825914"}}"#,
r#"{"namespace":0,"title":"Alpha","language":"en","version":42,"text":"Alpha body. ^ A reference\nMore."}"#,
r#"{"index":{"_type":"page","_id":"77"}}"#,
r#"{"namespace":4,"title":"Project:Skip","language":"en","version":1,"text":"nope"}"#,
r#"{"index":{"_type":"_doc","_id":"99"}}"#,
r#"{"namespace":0,"title":"Beta","language":"en","version":7,"text":"Beta body."}"#,
]
.join("\n")
}
#[test]
fn extracts_main_namespace_docs() {
let mut sink = StringSink::default();
let docs = process_cirrus_dump(dump().as_bytes(), &mut sink).unwrap();
assert_eq!(docs, 2);
assert_eq!(
sink.0,
"<doc id=\"3825914\" url=\"http://it.wikipedia.org/wiki?curid=3825914\" \
title=\"Alpha\" language=\"en\" revision=\"42\">\nAlpha\n\nAlpha body.\nMore.\n</doc>\n\
<doc id=\"99\" url=\"http://it.wikipedia.org/wiki?curid=99\" \
title=\"Beta\" language=\"en\" revision=\"7\">\nBeta\n\nBeta body.\n</doc>\n"
);
}
#[test]
fn truncated_dump_is_an_error() {
let mut sink = StringSink::default();
let result = process_cirrus_dump(
r#"{"index":{"_type":"page","_id":"1"}}"#.as_bytes(),
&mut sink,
);
assert!(result.is_err());
}
}