1use std::{cell::OnceCell, collections::HashMap};
2
3use bevy_ecs::prelude::*;
4use lsp_types::{DiagnosticSeverity, TextDocumentItem};
5use ropey::Rope;
6use rudof_lib::{
7 shacl_ast::{compiled::shape::CompiledShape, ShaclParser},
8 shacl_validation::{
9 shacl_processor::{GraphValidation, ShaclProcessor},
10 shape::Validate,
11 store::graph::Graph,
12 validation_report::result::ValidationResult,
13 },
14 srdf::{Object, SRDFGraph},
15 RdfData,
16};
17use sophia_api::quad::Quad as _;
18use tracing::{debug, error, info, instrument};
19
20use crate::prelude::*;
21
22fn get_reader<'a>(rope: &'a Rope) -> impl std::io::Read + 'a {
23 use std::io::prelude::*;
24 let reader: Box<dyn Read> = rope
25 .chunks()
26 .map(|x| std::io::Cursor::new(x.as_bytes()))
27 .fold(Box::new(std::io::Cursor::new(&[])), |acc, chunk| {
28 Box::new(acc.chain(chunk))
29 });
30 reader
31}
32
33type ShaclShape = CompiledShape<RdfData>;
34type ShaclShapes = Wrapped<Vec<CompiledShape<RdfData>>>;
35pub fn derive_shapes(
36 query: Query<(Entity, &RopeC, &Label), (Changed<Triples>, Without<Dirty>)>,
37 mut commands: Commands,
38) {
39 for (e, rope, label) in &query {
40 commands.entity(e).remove::<ShaclShapes>();
41
42 match SRDFGraph::from_reader(
43 get_reader(&rope),
44 &rudof_lib::RDFFormat::Turtle,
45 Some(label.0.as_str()),
46 &rudof_lib::ReaderMode::Lax,
47 )
48 .map_err(|x| x.to_string())
49 .and_then(|data| RdfData::from_graph(data).map_err(|e| e.to_string()))
50 .and_then(|data| {
51 let mut parser = ShaclParser::new(data.clone());
52 let result = parser.parse().map_err(|e| e.to_string());
53
54 if !parser.errors().is_empty() {
55 info!("Parsing shapes had some errors");
56 for e in parser.errors() {
57 info!("Error: {}", e);
58 }
59 }
60
61 result
62 }) {
63 Ok(schema) => {
64 let compiled: Vec<_> = schema
69 .iter()
70 .flat_map(
71 |(node, shape)| match ShaclShape::compile(shape.clone(), &schema) {
72 Ok(x) => Some(x),
73 Err(err) => {
74 info!("Failed to parse shacl shape {}: {}", node, err);
75 None
76 }
77 },
78 )
79 .collect();
80 let is_some = !compiled.is_empty();
81
82 debug!(
83 "Compiled shapes for {} (is some {})",
84 label.as_str(),
85 is_some
86 );
87 if !compiled.is_empty() {
88 commands.entity(e).insert(Wrapped(compiled));
89 }
90 }
91 Err(reason) => {
92 error!(
93 "Failed to compile shapes for {} ({})",
94 label.as_str(),
95 reason
96 );
97 }
98 }
99 }
100}
101
102fn get_path(
103 source: Option<&Object>,
104 s: &CompiledShape<RdfData>,
105 prefixes: &Prefixes,
106) -> Option<String> {
107 let source = source?;
108 let property = s
109 .property_shapes()
110 .iter()
111 .find(|x| match (x.id(), source) {
112 (rudof_lib::oxrdf::Term::NamedNode(named_node), Object::Iri(iri_s)) => {
113 named_node.as_str() == iri_s.as_str()
114 }
115 (rudof_lib::oxrdf::Term::BlankNode(blank_node), Object::BlankNode(st)) => {
116 blank_node.as_str() == st.as_str()
117 }
118 _ => false,
119 })?;
120 let path = property.path_str()?;
121 Some(prefixes.shorten(&path).unwrap_or(path))
122}
123
124fn group_per_fn_per_path(
125 res: &Vec<ValidationResult>,
126 s: &CompiledShape<RdfData>,
127 triples: &Triples,
128 prefixes: &Prefixes,
129) -> HashMap<std::ops::Range<usize>, HashMap<String, Vec<String>>> {
130 let mut per_fn_per_path = HashMap::new();
131 for r in res {
132 let foc = r.focus_node().to_string();
133
134 let mut done = std::collections::HashSet::new();
135 for t in &triples.0 {
136 if t.s().as_str() == &foc && !done.contains(t.s()) {
137 done.insert(t.s().to_owned());
138
139 let entry: &mut HashMap<String, Vec<String>> =
140 per_fn_per_path.entry(t.s().span.clone()).or_default();
141
142 let path = get_path(r.source(), s, prefixes).unwrap_or(String::new());
143 let entry = entry.entry(path).or_default();
144
145 let component = r.component().to_string();
146 let component = prefixes.shorten(&component).unwrap_or(component);
147
148 entry.push(component);
149 }
150 }
151 }
152
153 per_fn_per_path
154}
155
156fn push_diagnostics(
157 rope: &Rope,
158 res: &Vec<ValidationResult>,
159 s: &CompiledShape<RdfData>,
160 triples: &Triples,
161 prefixes: &Prefixes,
162 diagnostics: &mut Vec<lsp_types::Diagnostic>,
163) {
164 for (range, per_path) in group_per_fn_per_path(&res, s, triples, prefixes) {
165 if let Some(range) = range_to_range(&range, &rope) {
166 for (path, components) in per_path {
167 let mut comps = components[0].clone();
168 for c in components.into_iter().skip(1) {
169 comps += ", ";
170 comps += &c;
171 }
172
173 diagnostics.push(lsp_types::Diagnostic {
174 range: range.clone(),
175 severity: Some(DiagnosticSeverity::ERROR),
176 source: Some(String::from("SWLS")),
177 message: format!("Path {} violates {}", path, comps),
178 related_information: None,
179 ..Default::default()
180 });
181 }
182 }
183 }
184}
185
186fn derive_shapes_diagnostics_for(
187 rope: &RopeC,
188 label: &Label,
189 links: &DocumentLinks,
190 item: &Wrapped<TextDocumentItem>,
191 triples: &Triples,
192 other: &Query<(&Label, &ShaclShapes, &Prefixes)>,
193 client: &mut DiagnosticPublisher,
194) {
195 let mut diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
196
197 let build_validator = || {
198 SRDFGraph::from_reader(
199 get_reader(&rope),
200 &rudof_lib::RDFFormat::Turtle,
201 Some(label.0.as_str()),
202 &rudof_lib::ReaderMode::Lax,
203 )
204 .ok()
205 .and_then(|data| RdfData::from_graph(data).ok())
206 .map(|data| {
207 debug!("Created graph validator for {}", label.as_str());
208 GraphValidation::from_graph(
209 Graph::from_data(data),
210 rudof_lib::ShaclValidationMode::Native,
211 )
212 })
213 };
214
215 let validator = OnceCell::<Option<GraphValidation>>::new();
218 for (other_label, schema, prefixes) in other {
219 if links
220 .iter()
221 .find(|link| link.0.as_str().starts_with(other_label.0.as_str()))
222 .is_none()
223 && label.0 != other_label.0
224 {
225 continue;
226 }
227
228 if let Some(validator) = validator.get_or_init(build_validator) {
229 debug!("Schema {}", other_label.as_str());
230 for s in schema.iter() {
231 if let Ok(res) = s.validate(validator.store(), validator.runner(), None, None) {
232 if !res.is_empty() {
233 push_diagnostics(rope, &res, s, triples, prefixes, &mut diagnostics);
234 }
235 }
236 }
237 } else {
238 break;
239 }
240 }
241
242 let _ = client.publish(&item.0, diagnostics, "shacl_validation");
243}
244
245pub fn validate_shapes(
248 query: Query<
249 (
250 &RopeC,
251 &Label,
252 &DocumentLinks,
253 &Wrapped<TextDocumentItem>,
254 &Triples,
255 ),
256 (Changed<Triples>, Without<Dirty>, With<Open>),
257 >,
258 other: Query<(&Label, &ShaclShapes, &Prefixes)>,
259 mut client: ResMut<DiagnosticPublisher>,
260) {
261 for (rope, label, links, item, triples) in &query {
262 info!("Validate shapes {}", label.as_str());
263 derive_shapes_diagnostics_for(rope, label, links, item, triples, &other, &mut client);
264 }
265}
266
267#[instrument(skip(changed_schemas, query, other, client))]
269pub fn validate_with_updated_shapes(
270 changed_schemas: Query<&Label, (Changed<Wrapped<ShaclShape>>, With<Open>)>,
271 query: Query<
272 (
273 &RopeC,
274 &Label,
275 &DocumentLinks,
276 &Wrapped<TextDocumentItem>,
277 &Triples,
278 ),
279 With<Open>,
280 >,
281 other: Query<(&Label, &ShaclShapes, &Prefixes)>,
282 mut client: ResMut<DiagnosticPublisher>,
283) {
284 for l in &changed_schemas {
285 info!("Changed schema {}", l.as_str());
286 for (rope, label, links, item, triples) in &query {
287 if links
288 .iter()
289 .find(|(url, _)| url.as_str().starts_with(l.as_str()))
290 .is_some()
291 {
292 debug!("Found reverse linked document! {}", label.as_str());
293 derive_shapes_diagnostics_for(
294 rope,
295 label,
296 links,
297 item,
298 triples,
299 &other,
300 &mut client,
301 );
302 }
303 }
304 }
305}