use crate::{
Result,
ast::{Value, parse_at},
format::{LineEnding, uglify_str_into},
};
#[derive(Debug)]
pub struct JsonlinesDocument<S> {
source: S,
values: Vec<Value>,
}
impl<S: AsRef<str>> JsonlinesDocument<S> {
pub fn parse(source: S) -> Result<Self> {
let text = source.as_ref();
let text = text.strip_suffix('\n').unwrap_or(text);
let mut values = Vec::new();
let mut offset = 0usize;
for line in text.split('\n') {
let r = offset..offset + line.len();
values.push(parse_at(text, r)?.root);
offset = r.end + 1;
}
Ok(Self { source, values })
}
pub fn source(&self) -> &S {
&self.source
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
pub fn parse(source: &str) -> Result<JsonlinesDocument<&str>> {
JsonlinesDocument::parse(source)
}
pub fn format(source: &str, line_ending: LineEnding) -> Result<String> {
let text = source.strip_suffix('\n').unwrap_or(source);
let mut lines = text.split('\n');
let Some(line) = lines.next() else {
return Ok(String::new());
};
let mut result = String::new();
uglify_str_into(&mut result, line)?;
for line in lines {
result.push_str(line_ending.as_str());
uglify_str_into(&mut result, line)?;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(r#"{"a":1}"#, 1)]
#[case("{\"a\":1}\n{\"b\":2}", 2)]
fn lines_split_on_newline(#[case] input: &str, #[case] count: usize) {
assert_eq!(parse(input).unwrap().len(), count);
}
#[rstest]
#[case("{\"a\":1}\n", 1)]
#[case("{\"a\":1}\n{\"b\":2}\n", 2)]
fn trailing_newline_is_allowed(#[case] input: &str, #[case] count: usize) {
assert_eq!(parse(input).unwrap().len(), count);
}
#[rstest]
#[case("\n{\"a\":1}", 1)]
#[case("{\"a\":1}\n\n{\"b\":2}", 2)]
#[case("{\"a\":1}\n{bad}", 2)]
fn invalid_lines_fail(#[case] input: &str, #[case] expected_line: usize) {
let err = parse(input).unwrap_err();
let line = input[..err.range().start]
.chars()
.filter(|&c| c == '\n')
.count()
+ 1;
assert_eq!(line, expected_line);
}
}