jjpwrgem_parse/
jsonlines.rs1use crate::{
2 Result,
3 ast::{Value, parse_at},
4 format::{LineEnding, uglify_str_into},
5};
6
7#[derive(Debug)]
8pub struct JsonlinesDocument<S> {
9 source: S,
10 values: Vec<Value>,
11}
12
13impl<S: AsRef<str>> JsonlinesDocument<S> {
14 pub fn parse(source: S) -> Result<Self> {
16 let text = source.as_ref();
17 let text = text.strip_suffix('\n').unwrap_or(text);
19
20 let mut values = Vec::new();
21 let mut offset = 0usize;
22
23 for line in text.split('\n') {
25 let r = offset..offset + line.len();
26
27 values.push(parse_at(text, r)?.root);
29
30 offset = r.end + 1;
31 }
32
33 Ok(Self { source, values })
34 }
35
36 pub fn source(&self) -> &S {
37 &self.source
38 }
39
40 pub fn len(&self) -> usize {
41 self.values.len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.values.is_empty()
46 }
47}
48
49pub fn parse(source: &str) -> Result<JsonlinesDocument<&str>> {
50 JsonlinesDocument::parse(source)
51}
52
53pub fn format(source: &str, line_ending: LineEnding) -> Result<String> {
54 let text = source.strip_suffix('\n').unwrap_or(source);
55 let mut lines = text.split('\n');
56
57 let Some(line) = lines.next() else {
58 return Ok(String::new());
59 };
60
61 let mut result = String::new();
62 uglify_str_into(&mut result, line)?;
63
64 for line in lines {
65 result.push_str(line_ending.as_str());
66 uglify_str_into(&mut result, line)?;
67 }
68 Ok(result)
69}
70
71#[cfg(test)]
72mod tests {
73 use rstest::rstest;
74
75 use super::*;
76
77 #[rstest]
79 #[case(r#"{"a":1}"#, 1)]
80 #[case("{\"a\":1}\n{\"b\":2}", 2)]
81 fn lines_split_on_newline(#[case] input: &str, #[case] count: usize) {
82 assert_eq!(parse(input).unwrap().len(), count);
83 }
84
85 #[rstest]
87 #[case("{\"a\":1}\n", 1)]
88 #[case("{\"a\":1}\n{\"b\":2}\n", 2)]
89 fn trailing_newline_is_allowed(#[case] input: &str, #[case] count: usize) {
90 assert_eq!(parse(input).unwrap().len(), count);
91 }
92
93 #[rstest]
95 #[case("\n{\"a\":1}", 1)]
96 #[case("{\"a\":1}\n\n{\"b\":2}", 2)]
97 #[case("{\"a\":1}\n{bad}", 2)]
98 fn invalid_lines_fail(#[case] input: &str, #[case] expected_line: usize) {
99 let err = parse(input).unwrap_err();
100 let line = input[..err.range().start]
101 .chars()
102 .filter(|&c| c == '\n')
103 .count()
104 + 1;
105 assert_eq!(line, expected_line);
106 }
107}