blockworx/
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 #[test]
84 fn every_fixture_survives_a_write_and_a_read() {
85 let update = std::env::var_os("BLOCKWORX_UPDATE_GOLDENS").is_some();
86 let mut seen = 0;
87 for entry in std::fs::read_dir("fixtures").expect("fixtures/ is there") {
88 let path = entry.expect("a directory entry").path();
89 if path.extension().and_then(|e| e.to_str()) != Some("json") {
90 continue;
91 }
92 let name = path.display().to_string();
93 let src = std::fs::read_to_string(&path).expect("the fixture reads");
94 let doc = parse(&src, &name).unwrap_or_else(|e| panic!("{name} parses:\n{e:?}"));
95 let mut written = to_json(&doc);
96 written.push('\n');
97 if update {
98 std::fs::write(&path, &written).expect("the fixture writes");
99 }
100 let read =
101 parse(&written, &name).unwrap_or_else(|e| panic!("{name} re-parses:\n{e:?}"));
102 assert!(read == doc, "{name} did not survive the round trip");
103 seen += 1;
104 }
105 assert!(seen >= 13, "the corpus shrank to {seen} documents");
106 }
107
108 #[test]
113 fn the_document_format_is_byte_stable() {
114 const GOLDEN_PATH: &str = "src/goldens/document.json";
115 const GOLDEN: &str = include_str!("goldens/document.json");
116
117 let src = std::fs::read_to_string("fixtures/root-scope.json").expect("the fixture reads");
118 let mut text = to_json(&parse(&src, "root-scope.json").expect("it parses"));
119 text.push('\n');
120 if std::env::var_os("BLOCKWORX_UPDATE_GOLDENS").is_some() {
121 std::fs::write(GOLDEN_PATH, &text).expect("the golden is written");
122 return;
123 }
124 assert_eq!(
125 text, GOLDEN,
126 "the document format changed; if that is intended, regenerate with \
127 `BLOCKWORX_UPDATE_GOLDENS=1 cargo test --lib document_file` and review the diff",
128 );
129 }
130
131 #[test]
136 fn a_parse_failure_points_at_the_offending_line_with_its_source() {
137 let src = "{\n \"version\": 3,\n \"blocks\": [\n}\n";
138 let failure = parse(src, "broken.json").expect_err("it does not parse");
139 assert_eq!(
140 src[..failure.span.offset()].lines().count(),
141 3,
142 "the span lands on the line serde stopped at: {src:?}"
143 );
144 let attached = failure
145 .source_code()
146 .expect("the report carries its source")
147 .read_span(&failure.span, 1, 1)
148 .expect("the span reads back against it");
149 assert_eq!(attached.name(), Some("broken.json"));
150 assert!(
151 String::from_utf8_lossy(attached.data()).contains("\"blocks\""),
152 "the rendered line is the one that failed"
153 );
154 }
155
156 #[test]
158 fn a_document_from_a_newer_build_is_refused_with_a_span() {
159 let failure = parse(r#"{"version": 99}"#, "future.json").expect_err("it is refused");
160 assert!(
161 failure.to_string().contains("newer than this build reads"),
162 "{failure}"
163 );
164 }
165}