use std::fs::File;
use std::io::{Read as _, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::envelope::Envelope;
use crate::vocabulary::Vocabulary;
#[derive(Debug, Clone, PartialEq)]
pub struct Record<V: Vocabulary> {
pub envelope: Envelope<V>,
pub position: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Torn {
pub at: u64,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refused {
pub at: u64,
pub position: u64,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Reading<V: Vocabulary> {
Record(Record<V>),
Torn(Torn),
Refused(Refused),
}
#[derive(Debug)]
pub struct Reader<V: Vocabulary> {
path: PathBuf,
readings: std::vec::IntoIter<Reading<V>>,
position: u64,
}
impl<V: Vocabulary> Reader<V> {
pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
Self::open_at(path, 0)
}
pub fn open_at(path: impl AsRef<Path>, position: u64) -> std::io::Result<Self> {
let path = path.as_ref().to_path_buf();
let mut file = File::open(&path)?;
if position > 0 {
file.seek(SeekFrom::Start(position - 1))?;
let mut before = [0u8; 1];
let why = match file.read(&mut before)? {
0 => Some("it is past the end of the file"),
_ if before[0] != b'\n' => {
Some("it is inside a record: no newline ends the byte before it")
}
_ => None,
};
if let Some(why) = why {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"cannot read {} from byte {position}: {why}; resume from a position a reading handed back, or 0",
path.display()
),
));
}
}
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let readings = read_lines::<V>(&bytes, position);
let resume = readings
.iter()
.rev()
.find_map(|reading| match reading {
Reading::Record(record) => Some(record.position),
Reading::Refused(refused) => Some(refused.position),
Reading::Torn(_) => None,
})
.unwrap_or(position);
Ok(Self {
path,
readings: readings.into_iter(),
position: resume,
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn position(&self) -> u64 {
self.position
}
pub fn collect_all(self) -> Readings<V> {
let mut readings = Readings {
position: self.position,
records: Vec::new(),
refused: Vec::new(),
torn: None,
};
for reading in self {
match reading {
Reading::Record(record) => readings.records.push(record),
Reading::Refused(refused) => readings.refused.push(refused),
Reading::Torn(torn) => readings.torn = Some(torn),
}
}
readings
}
}
impl<V: Vocabulary> Iterator for Reader<V> {
type Item = Reading<V>;
fn next(&mut self) -> Option<Self::Item> {
self.readings.next()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Readings<V: Vocabulary> {
pub position: u64,
pub records: Vec<Record<V>>,
pub refused: Vec<Refused>,
pub torn: Option<Torn>,
}
fn read_lines<V: Vocabulary>(bytes: &[u8], offset: u64) -> Vec<Reading<V>> {
let mut readings = Vec::new();
let mut start = 0usize;
while start < bytes.len() {
let Some(newline) = bytes[start..].iter().position(|byte| *byte == b'\n') else {
readings.push(Reading::Torn(Torn {
at: offset + start as u64,
bytes: (bytes.len() - start) as u64,
}));
break;
};
let end = start + newline;
let line = &bytes[start..end];
let position = offset + end as u64 + 1;
readings.push(match parse_line::<V>(line) {
Ok(envelope) => Reading::Record(Record { envelope, position }),
Err(reason) => Reading::Refused(Refused {
at: offset + start as u64,
position,
reason,
}),
});
start = end + 1;
}
readings
}
fn parse_line<V: Vocabulary>(line: &[u8]) -> Result<Envelope<V>, String> {
let text = std::str::from_utf8(line).map_err(|failure| format!("not UTF-8: {failure}"))?;
if text.trim().is_empty() {
return Err("an empty line, which no writer leaves".to_owned());
}
serde_json::from_str(text).map_err(|failure| failure.to_string())
}
#[derive(Debug)]
pub struct Merge<V: Vocabulary> {
records: Vec<Envelope<V>>,
refused: Vec<(PathBuf, Refused)>,
torn: Vec<(PathBuf, Torn)>,
}
impl<V: Vocabulary> Merge<V> {
#[must_use]
pub fn new(readers: impl IntoIterator<Item = Reader<V>>) -> Self {
let mut records = Vec::new();
let mut refused = Vec::new();
let mut torn = Vec::new();
for reader in readers {
let path = reader.path().to_path_buf();
for reading in reader {
match reading {
Reading::Record(record) => records.push(record.envelope),
Reading::Refused(line) => refused.push((path.clone(), line)),
Reading::Torn(tail) => torn.push((path.clone(), tail)),
}
}
}
records.sort_by(|a, b| a.order_key().cmp(&b.order_key()));
Self {
records,
refused,
torn,
}
}
pub fn open(paths: impl IntoIterator<Item = impl AsRef<Path>>) -> std::io::Result<Self> {
let mut readers = Vec::new();
for path in paths {
let path = path.as_ref();
readers.push(Reader::open(path).map_err(|failure| {
std::io::Error::new(
failure.kind(),
format!("cannot read {}: {failure}", path.display()),
)
})?);
}
Ok(Self::new(readers))
}
#[must_use]
pub fn records(&self) -> &[Envelope<V>] {
&self.records
}
#[must_use]
pub fn refused(&self) -> &[(PathBuf, Refused)] {
&self.refused
}
#[must_use]
pub fn torn(&self) -> &[(PathBuf, Torn)] {
&self.torn
}
#[must_use]
pub fn into_records(self) -> Vec<Envelope<V>> {
self.records
}
}