swls_core/systems/shapes/
mod.rs1use std::{collections::HashMap, fmt::Write as _};
2
3use bevy_ecs::prelude::*;
4use rudof_rdf::rdf_core::FocusRDF;
5use shacl_ast::ast::ShaclSchema;
6use shacl_ir::compiled::schema_ir::SchemaIR as ShaclSchemaIR;
7use shacl_rdf::ShaclParser;
8use shacl_validation::{
9 shacl_engine::native::NativeEngine, shape_validation::Validate as _,
10 validation_report::result::ValidationResult,
11};
12use sophia_api::quad::Quad as _;
13use tracing::{info_span, instrument};
14
15use crate::{lsp_types::TextDocumentItem, prelude::*, util};
16
17mod data;
18use data::*;
19
20fn shacl_schema_from_data<RDF: FocusRDF + std::fmt::Debug>(
21 rdf_data: RDF,
22) -> Option<ShaclSchema<RDF>> {
23 ShaclParser::new(rdf_data).parse().ok()
24}
25
26pub fn derive_shapes(
27 query: Query<(Entity, &Label, &Triples), (Changed<Triples>, Without<Dirty>)>,
28 mut commands: Commands,
29 res: Res<ServerConfig>,
30) {
31 if res.config.local.disabled.contains(&Disabled::Shapes) {
32 return;
33 }
34 for (e, label, data) in &query {
35 let _entered = info_span!("derive_shape", label = label.as_str()).entered();
36 commands.entity(e).remove::<ShaclShapes>();
37
38 let rdf = Rdf::new(data);
39
40 let Some(schema) = shacl_schema_from_data(rdf) else {
41 continue;
42 };
43
44 let Some(shacl_ir) = ShaclSchemaIR::compile(&schema).ok() else {
45 continue;
46 };
47 tracing::debug!("Found shacl ir with {} shapes", shacl_ir.iter().count());
48
49 commands.entity(e).insert(ShaclShapes::new(shacl_ir));
50 }
51}
52
53#[instrument(skip(query, other, client, res))]
55pub fn validate_shapes(
56 query: Query<
57 (
58 &RopeC,
59 &Label,
60 &DocumentLinks,
61 &Wrapped<TextDocumentItem>,
62 &Prefixes,
63 &Triples,
64 &DynLang,
65 ),
66 (Changed<Triples>, With<Open>),
67 >,
68 other: Query<(&Label, &ShaclShapes, Option<&Global>, &Triples)>,
69 mut client: ResMut<DiagnosticPublisher>,
70 res: Res<ServerConfig>,
71) {
72 if res.config.local.disabled.contains(&Disabled::Shapes) {
73 return;
74 }
75
76 for (rope, label, links, item, prefixes, triples, lang) in &query {
77 if !lang.supports_shape_validation() {
78 continue;
79 }
80 tracing::debug!("Validate shapes {}", label.as_str());
81 let client: &mut DiagnosticPublisher = &mut client;
82 let mut diagnostics: Vec<crate::lsp_types::Diagnostic> = Vec::new();
83
84 let rdf = Rdf::new(triples);
85 let mut validation_results = Vec::new();
86 let mut runner = NativeEngine::new();
87
88 for (other_label, schema, global, shape_triples) in &other {
89 if global.is_none()
90 && links
91 .iter()
92 .find(|link| link.0.as_str().starts_with(other_label.0.as_str()))
93 .is_none()
94 && label.0 != other_label.0
95 {
96 continue;
97 }
98
99 let _entered = info_span!(
100 "validate_shapes",
101 data = label.as_str(),
102 shapes = other_label.as_str()
103 )
104 .entered();
105
106 let ir = schema.ir();
107 for (_, shape) in ir.iter_with_targets() {
108 let results = match shape.validate(&rdf, &mut runner, None, Some(shape), ir) {
109 Ok(r) => r,
110 Err(e) => {
111 tracing::debug!("errors {}", e);
112 continue;
113 }
114 };
115
116 let mut map = HashMap::new();
117
118 for res in &results {
119 let key = (res.source(), res.path());
120 let entry: &mut Vec<&ValidationResult> = map.entry(key).or_default();
121 entry.push(res);
122 }
123
124 for (_, all) in map {
125 let res = all[0];
126 let severity = match res.severity() {
127 shacl_ir::severity::CompiledSeverity::Info => {
128 Some(crate::lsp_types::DiagnosticSeverity::INFORMATION)
129 }
130 shacl_ir::severity::CompiledSeverity::Warning => {
131 Some(crate::lsp_types::DiagnosticSeverity::WARNING)
132 }
133 shacl_ir::severity::CompiledSeverity::Violation => {
134 Some(crate::lsp_types::DiagnosticSeverity::ERROR)
135 }
136 _ => None,
137 };
138
139 let term: MyTerm = MyTerm::from(res.focus_node().clone());
140 let related_subjects = triples.iter().map(|x| x.s()).filter(|&x| x == &term);
141 let related_objects = triples.iter().map(|x| x.o()).filter(|&x| x == &term);
142 let Some(span) = related_subjects
143 .chain(related_objects)
144 .min_by_key(|x| x.span.start)
145 .map(|x| x.span.clone())
146 else {
147 continue;
148 };
149 let Some(range) = util::range_to_range(&span, rope) else {
150 continue;
151 };
152
153 let mut writer = String::new();
154
155 for res in all {
156 if let Some(m) = res.message() {
157 write!(&mut writer, "{}\n", m).unwrap();
158 } else {
159 write!(&mut writer, "Component {} ", res.component().to_string())
160 .unwrap();
161 }
162 }
163
164 if let Some(path) = res.path() {
165 write!(
166 &mut writer,
167 "on path {} ",
168 PrefixedPath::new(path, prefixes)
169 )
170 .unwrap();
171 }
172
173 let id = MyTerm::from(shape.id().clone());
174 if id.ty == Some(sophia_api::term::TermKind::Iri) {
175 write!(&mut writer, "({})\n", id.value).unwrap();
176 } else {
177 write!(&mut writer, "\n",).unwrap();
178 }
179
180 if let Some(source) = res.source().cloned() {
181 let source = MyTerm::from(source);
182
183 if let Some(message) = shape_triples.object(
184 [&source],
185 [
186 &MyTerm::named_node(
187 "http://www.w3.org/2000/01/rdf-schema#comment",
188 0..0,
189 ),
190 &MyTerm::named_node("http://www.w3.org/ns/shacl#message", 0..0),
191 ],
192 ) {
193 write!(
194 &mut writer,
195 "\n{}\n",
196 message
197 .value
198 .split_whitespace()
199 .collect::<Vec<_>>()
200 .join(" ")
201 )
202 .unwrap();
203 }
204 }
205
206 diagnostics.push(crate::lsp_types::Diagnostic {
207 range,
208 severity,
209 source: Some(String::from("SWLS")),
210 message: writer,
211 ..Default::default()
212 });
213 }
214
215 validation_results.extend(results);
216 }
217 }
218
219 let _ = client.publish(&item.0, diagnostics, "shacl_validation");
220 }
221}