Skip to main content

json_ld/ext/
miette.rs

1use std::{
2	collections::{HashMap, hash_map::Entry},
3	sync::Arc,
4};
5
6use json_syntax::{
7	JsonValue,
8	locspan::Span,
9	tracing::{JsonBacktraceFrame, JsonFragmentPath},
10};
11use miette::{Diagnostic, NamedSource, SourceSpan};
12use rdf_syntax::Iri;
13
14use crate::{
15	AsyncJsonLdLoader, JsonLdDocument, JsonLdDocumentForm, JsonLdError, JsonLdLocatedError,
16	JsonLdSourceCode, JsonLdSourceFile,
17};
18
19#[derive(Debug, thiserror::Error)]
20#[error("{error}")]
21pub struct JsonLdDiagnostic {
22	pub error: JsonLdError,
23
24	pub src: Option<(NamedSource<Arc<JsonLdDiagnosticSourceCode>>, SourceSpan)>,
25
26	/// Related diagnostic.
27	pub related: Vec<JsonLdRelatedDiagnostic>,
28}
29
30impl JsonLdDiagnostic {
31	pub async fn new_async(
32		loader: &impl AsyncJsonLdLoader,
33		input: &JsonLdDocument,
34		error: JsonLdLocatedError,
35	) -> Self {
36		let mut map = HashMap::new();
37
38		map.insert(
39			JsonLdSourceFile::original(input.url.clone()),
40			named_source_from_document(input.clone()),
41		);
42
43		let mut src = None;
44		let mut related = Vec::new();
45
46		if let Some((location, rest)) = error.location.split_last() {
47			src = locate_error(&mut map, loader, location).await;
48
49			for location in rest.iter().rev() {
50				if let Some((src, span)) = locate_error(&mut map, loader, location).await {
51					related.push(JsonLdRelatedDiagnostic { src, span })
52				}
53			}
54		};
55
56		Self {
57			error: error.value,
58			src,
59			related,
60		}
61	}
62}
63
64impl Diagnostic for JsonLdDiagnostic {
65	fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
66		Some(Box::new(self.error.code()))
67	}
68
69	fn source_code(&self) -> Option<&dyn miette::SourceCode> {
70		self.src.as_ref().map(|c| &c.0 as &dyn miette::SourceCode)
71	}
72
73	fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
74		self.src.as_ref().map(|c| {
75			Box::new(
76				[miette::LabeledSpan::new_with_span(
77					Some("here".to_owned()),
78					c.1,
79				)]
80				.into_iter(),
81			) as Box<dyn Iterator<Item = miette::LabeledSpan>>
82		})
83	}
84
85	fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
86		Some(Box::new(self.related.iter().map(|d| d as &dyn Diagnostic)))
87	}
88}
89
90fn url_to_name(url: Option<&Iri>) -> &str {
91	match url {
92		Some(url) => url.as_str(),
93		None => "<input>",
94	}
95}
96
97fn named_source_from_document(
98	document: JsonLdDocument,
99) -> NamedSource<Arc<JsonLdDiagnosticSourceCode>> {
100	let source = document
101		.source
102		.unwrap_or_else(|| JsonLdSourceCode::from_value(&document.document));
103
104	NamedSource::new(
105		url_to_name(document.url.as_deref()),
106		Arc::new(JsonLdDiagnosticSourceCode {
107			value: document.document,
108			source,
109		}),
110	)
111}
112
113async fn get_named_source(
114	map: &mut HashMap<JsonLdSourceFile, NamedSource<Arc<JsonLdDiagnosticSourceCode>>>,
115	loader: &impl AsyncJsonLdLoader,
116	file: &JsonLdSourceFile,
117) -> Option<NamedSource<Arc<JsonLdDiagnosticSourceCode>>> {
118	match map.entry(file.clone()) {
119		Entry::Occupied(e) => Some(e.get().clone()),
120		Entry::Vacant(e) => {
121			let named_source = match &file.form {
122				JsonLdDocumentForm::Original => {
123					let url = file.iri.as_deref()?;
124					let document = loader.async_load(url).await.ok()?;
125					named_source_from_document(document)
126				}
127				JsonLdDocumentForm::Expanded(document) => {
128					let value = document.to_json();
129					let source = JsonLdSourceCode::from_value(&value);
130					NamedSource::new(
131						url_to_name(file.iri.as_deref()),
132						Arc::new(JsonLdDiagnosticSourceCode { value, source }),
133					)
134				}
135				JsonLdDocumentForm::Flattened(document) => {
136					let value = json_syntax::to_value(&**document).unwrap();
137					let source = JsonLdSourceCode::from_value(&value);
138					NamedSource::new(
139						url_to_name(file.iri.as_deref()),
140						Arc::new(JsonLdDiagnosticSourceCode { value, source }),
141					)
142				}
143			};
144
145			Some(e.insert(named_source).clone())
146		}
147	}
148}
149
150async fn locate_error(
151	map: &mut HashMap<JsonLdSourceFile, NamedSource<Arc<JsonLdDiagnosticSourceCode>>>,
152	loader: &impl AsyncJsonLdLoader,
153	location: &JsonBacktraceFrame<JsonLdSourceFile>,
154) -> Option<(NamedSource<Arc<JsonLdDiagnosticSourceCode>>, SourceSpan)> {
155	let named_source = get_named_source(map, loader, &location.file).await?;
156	let span = named_source.inner().locate_fragment(&location.fragment)?;
157
158	Some((named_source, span.into()))
159}
160
161impl miette::SourceCode for JsonLdSourceCode {
162	fn read_span<'a>(
163		&'a self,
164		span: &miette::SourceSpan,
165		context_lines_before: usize,
166		context_lines_after: usize,
167	) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
168		self.text
169			.read_span(span, context_lines_before, context_lines_after)
170	}
171}
172
173pub struct JsonLdDiagnosticSourceCode {
174	pub source: JsonLdSourceCode,
175	pub value: JsonValue,
176}
177
178impl JsonLdDiagnosticSourceCode {
179	pub fn locate_fragment(&self, path: &JsonFragmentPath) -> Option<Span> {
180		let offset = self.value.locate_fragment(&self.source.code_map, path)?;
181		Some(self.source.code_map.get(offset)?.span)
182	}
183}
184
185impl miette::SourceCode for JsonLdDiagnosticSourceCode {
186	fn read_span<'a>(
187		&'a self,
188		span: &SourceSpan,
189		context_lines_before: usize,
190		context_lines_after: usize,
191	) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
192		self.source
193			.read_span(span, context_lines_before, context_lines_after)
194	}
195}
196
197#[derive(Debug, thiserror::Error)]
198#[error("required by")]
199pub struct JsonLdRelatedDiagnostic {
200	pub src: NamedSource<Arc<JsonLdDiagnosticSourceCode>>,
201
202	pub span: SourceSpan,
203}
204
205impl Diagnostic for JsonLdRelatedDiagnostic {
206	fn source_code(&self) -> Option<&dyn miette::SourceCode> {
207		Some(&self.src)
208	}
209
210	fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
211		Some(Box::new(
212			[miette::LabeledSpan::new_with_span(
213				Some("here".to_owned()),
214				self.span,
215			)]
216			.into_iter(),
217		) as Box<dyn Iterator<Item = miette::LabeledSpan>>)
218	}
219}