blockworx_store/
document_file.rs1#![allow(clippy::result_large_err)]
10
11use blockworx_doc::document::Document;
12use miette::{Diagnostic, NamedSource, SourceSpan};
13use thiserror::Error;
14
15#[derive(Debug, Error, Diagnostic)]
20#[error("{message}")]
21pub struct ParseFailure {
22 message: String,
23 #[label("here")]
24 span: SourceSpan,
25 #[source_code]
26 src: NamedSource<String>,
27}
28
29pub fn parse(src: &str, src_name: &str) -> Result<Document, ParseFailure> {
40 serde_json::from_str(src).map_err(|error| {
41 let offset = byte_offset(src, error.line(), error.column());
42 ParseFailure {
43 message: error.to_string(),
44 span: (offset, 1.min(src.len().saturating_sub(offset))).into(),
45 src: NamedSource::new(src_name, src.to_owned()),
46 }
47 })
48}
49
50#[expect(clippy::expect_used, clippy::missing_panics_doc)]
55pub fn to_json(doc: &Document) -> String {
56 serde_json::to_string_pretty(doc).expect("the document model serializes infallibly")
57}
58
59fn byte_offset(src: &str, line: usize, column: usize) -> usize {
62 let start: usize = src
63 .split_inclusive('\n')
64 .take(line.saturating_sub(1))
65 .map(str::len)
66 .sum();
67 (start + column.saturating_sub(1)).min(src.len())
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../fixtures");
77
78 #[test]
87 fn every_fixture_survives_a_write_and_a_read() {
88 let update = std::env::var_os("BLOCKWORX_UPDATE_GOLDENS").is_some();
89 let mut seen = 0;
90 for entry in std::fs::read_dir(FIXTURES).expect("fixtures/ is there") {
91 let path = entry.expect("a directory entry").path();
92 if path.extension().and_then(|e| e.to_str()) != Some("json") {
93 continue;
94 }
95 let name = path.display().to_string();
96 let src = std::fs::read_to_string(&path).expect("the fixture reads");
97 let doc = parse(&src, &name).unwrap_or_else(|e| panic!("{name} parses:\n{e:?}"));
98 let mut written = to_json(&doc);
99 written.push('\n');
100 if update {
101 std::fs::write(&path, &written).expect("the fixture writes");
102 }
103 let read =
104 parse(&written, &name).unwrap_or_else(|e| panic!("{name} re-parses:\n{e:?}"));
105 assert!(read == doc, "{name} did not survive the round trip");
106 seen += 1;
107 }
108 assert!(seen >= 13, "the corpus shrank to {seen} documents");
109 }
110
111 #[test]
116 fn the_document_format_is_byte_stable() {
117 const GOLDEN_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/goldens/document.json");
118 const GOLDEN: &str = include_str!("goldens/document.json");
119
120 let src = std::fs::read_to_string(format!("{FIXTURES}/root-scope.json"))
121 .expect("the fixture reads");
122 let mut text = to_json(&parse(&src, "root-scope.json").expect("it parses"));
123 text.push('\n');
124 if std::env::var_os("BLOCKWORX_UPDATE_GOLDENS").is_some() {
125 std::fs::write(GOLDEN_PATH, &text).expect("the golden is written");
126 return;
127 }
128 assert_eq!(
129 text, GOLDEN,
130 "the document format changed; if that is intended, regenerate with \
131 `BLOCKWORX_UPDATE_GOLDENS=1 cargo test -p blockworx-store document_file` and \
132 review the diff",
133 );
134 }
135
136 #[test]
141 fn a_parse_failure_points_at_the_offending_line_with_its_source() {
142 let src = "{\n \"version\": 3,\n \"blocks\": [\n}\n";
143 let failure = parse(src, "broken.json").expect_err("it does not parse");
144 assert_eq!(
145 src[..failure.span.offset()].lines().count(),
146 3,
147 "the span lands on the line serde stopped at: {src:?}"
148 );
149 let attached = failure
150 .source_code()
151 .expect("the report carries its source")
152 .read_span(&failure.span, 1, 1)
153 .expect("the span reads back against it");
154 assert_eq!(attached.name(), Some("broken.json"));
155 assert!(
156 String::from_utf8_lossy(attached.data()).contains("\"blocks\""),
157 "the rendered line is the one that failed"
158 );
159 }
160
161 #[test]
163 fn a_document_from_a_newer_build_is_refused_with_a_span() {
164 let failure = parse(r#"{"version": 99}"#, "future.json").expect_err("it is refused");
165 assert!(
166 failure.to_string().contains("newer than this build reads"),
167 "{failure}"
168 );
169 }
170}