Skip to main content

jjpwrgem_parse/
jsonlines.rs

1use 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    // jsonlines[impl utf8] — `S: AsRef<str>` is always valid UTF-8 in Rust.
15    pub fn parse(source: S) -> Result<Self> {
16        let text = source.as_ref();
17        // jsonlines[impl end-of-file]
18        let text = text.strip_suffix('\n').unwrap_or(text);
19
20        let mut values = Vec::new();
21        let mut offset = 0usize;
22
23        // jsonlines[impl newline-delimiter]
24        for line in text.split('\n') {
25            let r = offset..offset + line.len();
26
27            // jsonlines[impl each-line-is-a-valid-json-value]
28            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    // jsonlines[verify newline-delimiter]
78    #[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    // jsonlines[verify end-of-file]
86    #[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    // jsonlines[verify each-line-is-a-valid-json-value]
94    #[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}