Skip to main content

swls_core/systems/shapes/
mod.rs

1use 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/// System evaluates linked shapes
54#[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}