use std::io::BufRead;
use serde::Deserialize;
use crate::error::AppError;
pub const SEEK_TO_FROM_META: &str = "$seek_from_line_starts_with";
struct SeekParameterNames {
from: &'static str,
to: &'static str,
maximum: &'static str,
}
const QUERY_PARAMETER_NAMES: SeekParameterNames = SeekParameterNames {
from: "seek_from_line_starts_with",
to: "seek_to_line_starts_with",
maximum: "seek_lines_maximum",
};
const BODY_PARAMETER_NAMES: SeekParameterNames = SeekParameterNames {
from: "seek.from_line_starts_with",
to: "seek.to_line_starts_with",
maximum: "seek.lines_maximum",
};
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SeekOptions {
pub seek_from_line_starts_with: Option<String>,
pub seek_to_line_starts_with: Option<String>,
pub seek_lines_maximum: Option<usize>,
}
impl SeekOptions {
pub fn parse(&self) -> Result<SeekFilter, AppError> {
let names = &QUERY_PARAMETER_NAMES;
let from_prefixes = self
.seek_from_line_starts_with
.as_deref()
.map(|raw| Self::parse_prefix_array(raw, names.from))
.transpose()?;
let to_prefixes = match self.seek_to_line_starts_with.as_deref() {
None => None,
Some(SEEK_TO_FROM_META) => Some(vec![SEEK_TO_FROM_META.to_string()]),
Some(raw) => Some(Self::parse_prefix_array(raw, names.to)?),
};
SeekFilter::build(from_prefixes, to_prefixes, self.seek_lines_maximum, names)
}
fn parse_prefix_array(raw: &str, parameter: &str) -> Result<Vec<String>, AppError> {
serde_json::from_str(raw).map_err(|_err| AppError::InvalidOperation {
reason: format!(
"{} must be a JSON array of strings, e.g. [\"## \"]",
parameter
),
})
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SeekBody {
pub from_line_starts_with: Option<Vec<String>>,
pub to_line_starts_with: Option<SeekBodyToPrefixes>,
pub lines_maximum: Option<usize>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum SeekBodyToPrefixes {
Value(String),
Values(Vec<String>),
}
impl SeekBody {
pub fn parse(&self) -> Result<SeekFilter, AppError> {
let names = &BODY_PARAMETER_NAMES;
let to_prefixes = match &self.to_line_starts_with {
None => None,
Some(SeekBodyToPrefixes::Value(value)) if value == SEEK_TO_FROM_META => {
Some(vec![SEEK_TO_FROM_META.to_string()])
}
Some(SeekBodyToPrefixes::Value(_)) => {
return Err(AppError::InvalidOperation {
reason: format!(
"{} must be a JSON array of strings, or the bare meta value {}",
names.to, SEEK_TO_FROM_META
),
});
}
Some(SeekBodyToPrefixes::Values(values)) => Some(values.clone()),
};
SeekFilter::build(
self.from_line_starts_with.clone(),
to_prefixes,
self.lines_maximum,
names,
)
}
}
#[derive(Debug, Clone, Default)]
pub struct SeekFilter {
pub from_prefixes: Option<Vec<String>>,
pub to_prefixes: Option<Vec<String>>,
pub lines_maximum: Option<usize>,
}
impl SeekFilter {
fn build(
from_prefixes: Option<Vec<String>>,
to_prefixes: Option<Vec<String>>,
lines_maximum: Option<usize>,
names: &SeekParameterNames,
) -> Result<Self, AppError> {
for (prefixes, parameter) in [(&from_prefixes, names.from), (&to_prefixes, names.to)] {
let Some(prefixes) = prefixes else { continue };
if prefixes.is_empty() {
return Err(AppError::InvalidOperation {
reason: format!("{} must contain at least one prefix", parameter),
});
}
if prefixes.iter().any(|prefix| prefix.is_empty()) {
return Err(AppError::InvalidOperation {
reason: format!("{} prefixes must not be empty", parameter),
});
}
}
if from_prefixes.is_none() {
if let Some(to_prefixes) = &to_prefixes {
if to_prefixes
.iter()
.any(|prefix| prefix.contains(SEEK_TO_FROM_META))
{
return Err(AppError::InvalidOperation {
reason: format!(
"{} uses {} but {} is not set",
names.to, SEEK_TO_FROM_META, names.from
),
});
}
}
}
if lines_maximum == Some(0) {
return Err(AppError::InvalidOperation {
reason: format!("{} must be at least 1", names.maximum),
});
}
Ok(Self {
from_prefixes,
to_prefixes,
lines_maximum,
})
}
pub fn is_noop(&self) -> bool {
self.from_prefixes.is_none() && self.to_prefixes.is_none() && self.lines_maximum.is_none()
}
pub fn apply_reader<R: BufRead>(
&self,
mut reader: R,
file_path: &str,
) -> Result<String, AppError> {
let from_prefixes = self.from_prefixes.as_deref();
let mut in_window = from_prefixes.is_none();
let mut to_prefixes: Option<Vec<String>> = match from_prefixes {
None => self.to_prefixes.clone(),
Some(_) => None,
};
let mut window: Vec<u8> = Vec::new();
let mut lines_taken: usize = 0;
let mut line: Vec<u8> = Vec::new();
let mut window_closed = false;
loop {
line.clear();
if reader.read_until(b'\n', &mut line)? == 0 {
break;
}
if !in_window {
let Some(matched) = from_prefixes
.unwrap_or_default()
.iter()
.find(|prefix| line.starts_with(prefix.as_bytes()))
else {
continue;
};
in_window = true;
to_prefixes = self.to_prefixes.as_ref().map(|prefixes| {
prefixes
.iter()
.map(|prefix| prefix.replace(SEEK_TO_FROM_META, matched))
.collect()
});
} else if lines_taken > 0 {
if let Some(prefixes) = &to_prefixes {
if prefixes
.iter()
.any(|prefix| line.starts_with(prefix.as_bytes()))
{
window_closed = true;
}
}
}
window.extend_from_slice(&line);
lines_taken += 1;
if window_closed || self.lines_maximum == Some(lines_taken) {
break;
}
}
String::from_utf8(window).map_err(|_err| AppError::InvalidUtf8 {
path: file_path.to_string(),
})
}
}