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 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}