use std::path::Path;
use miette::{NamedSource, SourceSpan};
use thiserror::Error;
#[derive(Debug, Error, miette::Diagnostic)]
#[error("invalid Circuit JSON: {message}")]
pub struct JsonDiagnostic {
pub message: String,
#[source_code]
pub src: NamedSource<String>,
#[label("deserialization failed here")]
pub span: SourceSpan,
}
impl JsonDiagnostic {
pub(super) fn new(err: serde_json::Error, source: &str, path: &Path) -> Self {
Self::at(err.to_string(), &err, source, path)
}
pub(super) fn element(index: usize, err: serde_json::Error, source: &str, path: &Path) -> Self {
let message = format!("element #{index}: {err}");
match serde_json::from_str::<Vec<super::CircuitElement>>(source) {
Err(located) => Self::at(message, &located, source, path),
Ok(_) => Self::at(message, &err, source, path),
}
}
fn at(message: String, located: &serde_json::Error, source: &str, path: &Path) -> Self {
let offset = byte_offset_at(source, located.line(), located.column());
let len = if offset < source.len() { 1 } else { 0 };
Self {
message,
src: NamedSource::new(path.display().to_string(), source.to_string()),
span: SourceSpan::new(offset.into(), len),
}
}
}
pub(super) fn byte_offset_at(source: &str, line: usize, column: usize) -> usize {
let mut remaining = line.max(1);
let mut offset = 0usize;
for segment in source.split_inclusive('\n') {
if remaining == 1 {
let text = segment.strip_suffix('\n').unwrap_or(segment);
let mut rel = column.saturating_sub(1).min(text.len());
while rel > 0 && !text.is_char_boundary(rel) {
rel -= 1;
}
return (offset + rel).min(source.len());
}
offset += segment.len();
remaining -= 1;
}
source.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
#[test]
fn rejected_input_carries_the_named_source() {
let err = parse("{\"type\": ", Path::new("circuit.json"))
.expect_err("an object root must not parse");
assert!(!err.message.is_empty());
assert!(err.src.inner().contains("type"));
}
#[test]
fn span_reaches_the_failing_line_of_a_multiline_file() {
let source = "[\n {\"type\": \"source_net\"},\n {,}\n]";
let line3_start = source.find("\n {,}").expect("line 3") + 1;
let line3_end = line3_start + source[line3_start..].find('\n').expect("line 3 end");
let err = parse(source, Path::new("circuit.json")).expect_err("bad element");
let offset = err.span.offset();
assert!(
(line3_start..line3_end).contains(&offset),
"span {offset} not on line 3 ({line3_start}..{line3_end})"
);
}
#[test]
fn empty_input_renders_a_zero_length_span_at_the_start() {
let err = parse("", Path::new("circuit.json")).expect_err("empty input");
assert!(err.message.contains("EOF"));
assert_eq!(err.span.offset(), 0);
assert_eq!(err.span.len(), 0);
}
}