cimvalidation/shapes.rs
1//! The shape table both families are validated against.
2//!
3//! Elements are property bags ([`cimmodel::Element`]) with no struct to
4//! downcast to, so the shapes are data and [`crate::bag`] interprets them, the
5//! same way classes are [`cimmodel::base::ClassDef`] rows interpreted by the
6//! decoder. `cimgen` emits the tables into `cgmes_shapes.rs` and
7//! `nc_shapes.rs`; with the `dynamic-shapes` feature they can be built from the
8//! SHACL TTL files at runtime instead.
9//!
10//! Everything a consumer would have to compute per element is resolved when
11//! the table is built: target classes are already family-qualified
12//! `CimDataset::by_type` keys with abstract classes expanded to their concrete
13//! descendants, and paths are already the field keys the decoder stores.
14
15/// Where a property shape's values come from.
16///
17/// Field keys are the local XML element name with the prefix stripped, which
18/// is exactly what the decoder puts in [`cimmodel::Element::fields`].
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Path {
21 /// A field on the element itself.
22 Forward(&'static str),
23 /// `sh:inversePath` — the field on whatever points *at* this element.
24 /// Needs the reverse index, which is why [`ShapeSet`] tracks whether any
25 /// active shape uses one.
26 Inverse(&'static str),
27 /// `sh:alternativePath` — the union of every branch's values. A branch may
28 /// itself be inverse, hence [`AltBranch`] rather than a plain name.
29 Alternative(&'static [AltBranch]),
30 /// A sequence path: `( ^cim:Terminal.ConductingEquipment
31 /// cim:Terminal.phases )`, `( cim:Location.mainAddress
32 /// cim:StreetAddress.status cim:Status.dateTime )`, and — 170 of NCP's 178
33 /// chains, its main association value-type check — `( nc:X.y rdf:type )`.
34 ///
35 /// Stepping *from* an element absent from the dataset makes the result
36 /// unknown and every check on it silent: phase 1 runs per file, so leaving
37 /// the file is normal. A final `rdf:type` step needs nothing from the
38 /// elements it reaches, so they count even when absent, and class checks
39 /// skip the absent ones, as [`Constraint::RefClass`] describes.
40 ///
41 /// The referenced element may belong to *either* family: NCP's value-type
42 /// lists name CGMES classes (`cim17:ACLineSegment`) alongside NC ones, and
43 /// `Element::type_name` answers for both.
44 Chain(&'static [Step]),
45}
46
47/// One step of a [`Path::Chain`].
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Step {
50 Forward(&'static str),
51 /// Needs the reverse index, like [`Path::Inverse`], but as a list of the
52 /// elements pointing here rather than a count.
53 Inverse(&'static str),
54 /// `rdf:type`, last only. Its values are the elements reached; the
55 /// constraints on such a path (`RefClass`, counts) read their classes.
56 Type,
57}
58
59/// One branch of an [`Path::Alternative`].
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum AltBranch {
62 Forward(&'static str),
63 Inverse(&'static str),
64}
65
66/// `sh:nodeKind`, restricted to the values the profiles actually use.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum NodeKind {
69 /// Written as `rdf:resource`.
70 Iri,
71 /// Written as element text.
72 Literal,
73 BlankNode,
74}
75
76/// A single check on the values a [`Path`] yields.
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum Constraint {
79 MinCount(u32),
80 MaxCount(u32),
81 /// The xsd type's local name (`"integer"`, `"dateTime"`, …). A bag holds
82 /// strings, so this is a real parse check rather than the tautology it was
83 /// against a typed struct.
84 Datatype(&'static str),
85 NodeKind(NodeKind),
86 /// `sh:class` — the referenced element must be an instance of this
87 /// family-qualified class, or a descendant of it. Already expanded, so the
88 /// check is a set membership.
89 Class(&'static [&'static str]),
90 /// `sh:in` — the value must be one of these. Enum IRIs are stored as the
91 /// decoder stores them: fragment only.
92 In(&'static [&'static str]),
93 /// `sh:hasValue`, and `sh:in` with exactly one member after
94 /// simplification.
95 HasValue(&'static str),
96 MaxLength(u32),
97 MinLength(u32),
98 /// The referenced element's class must be one of these family-qualified
99 /// names. Produced by `sh:in` on a path ending in `rdf:type`.
100 ///
101 /// Only checked when the referenced element is **present**. Phase-1
102 /// validation runs per file, so a reference out of the current file is
103 /// normal and is a different rule's business — reporting it here would make
104 /// every cross-file association a value-type violation.
105 RefClass(&'static [&'static str]),
106 /// `sh:minInclusive` and friends. Compared as `f64`; a value that does not
107 /// parse as a number is skipped, the way a typed numeric field that failed
108 /// to parse is `None` — malformed numbers are `sh:datatype`'s business.
109 MinInclusive(f64),
110 MaxInclusive(f64),
111 MinExclusive(f64),
112 MaxExclusive(f64),
113 /// `sh:lessThan` — this property's value must be below the named field's
114 /// on the same element. Only checked when both are present and numeric.
115 LessThan(&'static str),
116 LessThanOrEquals(&'static str),
117 /// `sh:not [ sh:class X ]` — the referenced element must not be an
118 /// instance of these family-qualified classes. Silent when the referenced
119 /// element is absent, like [`Constraint::Class`].
120 NotClass(&'static [&'static str]),
121 /// `sh:qualifiedValueShape [ sh:in (..) ]` with `sh:qualifiedMinCount` —
122 /// at least `min` values must be among `allowed`. NCP uses it to ask
123 /// whether a dataset declares conformance to one of a set of profiles.
124 QualifiedIn { allowed: &'static [&'static str], min: u32 },
125 /// `sh:length` — exactly this many characters, e.g. the 16 of an EIC code.
126 Length(u32),
127}
128
129/// One constraint together with how to report it.
130///
131/// SHACL attaches `sh:message`, `sh:name` and `sh:severity` to the constraint,
132/// not to the property shape, and one property shape routinely carries several
133/// constraints with different messages — a cardinality rule and a value-range
134/// rule on the same path. Pairing them here keeps the reported text the one the
135/// schema wrote for that specific check.
136#[derive(Debug)]
137pub struct Check {
138 pub constraint: Constraint,
139 /// Shape IRI, the stable machine-readable rule id a caller can silence.
140 pub rule_id: &'static str,
141 pub name: &'static str,
142 pub message: &'static str,
143 pub description: &'static str,
144 /// `"sh:Violation"`, `"sh:Warning"` or `"sh:Info"`. NC leans on `sh:Info`
145 /// far more than CGMES does — 842 occurrences against 7 — so a caller that
146 /// treats every violation as a failure will report an NC dataset as broken
147 /// when it is merely being advised.
148 pub severity: &'static str,
149}
150
151/// One `sh:property` of a node shape: a path, and what must hold of its values.
152#[derive(Debug)]
153pub struct PropShape {
154 pub path: Path,
155 pub checks: &'static [Check],
156}
157
158/// What a node shape applies to.
159#[derive(Debug)]
160pub enum Target {
161 /// Family-qualified `by_type` keys. An abstract target is expanded to its
162 /// concrete descendants when the table is built — exact-match lookup on an
163 /// abstract class would silently check nothing.
164 Class(&'static [&'static str]),
165 /// `sh:targetSubjectsOf` — every element carrying this field.
166 SubjectsOf(&'static str),
167}
168
169/// How a [`Logic`] combines its branches.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum LogicOp {
172 And,
173 Or,
174 Xone,
175}
176
177/// One branch of a [`Logic`]. It conforms when none of its checks fail — or,
178/// for `[ sh:not X ]` (`negate`), when at least one does. NCP writes material
179/// implication that way: `sh:or ( [ sh:not conformsToNCProfile ] [ P required ] )`.
180#[derive(Debug)]
181pub struct Branch {
182 pub props: &'static [PropShape],
183 pub negate: bool,
184}
185
186/// A node-level `sh:and` / `sh:or` / `sh:xone`. The shape reports once, with
187/// its own name and message, when the combination does not hold; the branch
188/// checks carry no report text.
189#[derive(Debug)]
190pub struct Logic {
191 pub op: LogicOp,
192 pub branches: &'static [Branch],
193 pub rule_id: &'static str,
194 pub name: &'static str,
195 pub message: &'static str,
196 pub description: &'static str,
197 pub severity: &'static str,
198}
199
200/// One `sh:NodeShape`.
201#[derive(Debug)]
202pub struct ShapeDef {
203 pub targets: &'static [Target],
204 /// Borrowed rather than owned: NCP defines ~4,200 distinct property shapes
205 /// and references them ~14,800 times, because a shape like
206 /// `IdentifiedObject.mRID-cardinality` applies to hundreds of classes.
207 /// Sharing one definition per shape keeps the generated table a third of
208 /// the size it would otherwise be.
209 pub props: &'static [&'static PropShape],
210 /// `sh:closed` — the allowed field keys. An element carrying any other key
211 /// is reported.
212 ///
213 /// This is the constraint that only a property bag can answer: generated
214 /// structs drop unknown properties at decode, leaving nothing to compare
215 /// against. 823 NCP shapes use it.
216 pub closed: Option<&'static ClosedShape>,
217 /// Node-level `sh:and` / `sh:or` / `sh:xone`.
218 pub logic: &'static [Logic],
219 /// Every NC profile code whose manifest imports the file this shape came
220 /// from. Plural because four shared constraint files are imported by 17 or
221 /// 18 of the 18 manifests.
222 pub profiles: &'static [&'static str],
223 /// TTL base file name, for reports.
224 pub file: &'static str,
225}
226
227/// The `sh:closed` half of a shape, kept separate because it carries its own
228/// message and severity.
229#[derive(Debug)]
230pub struct ClosedShape {
231 pub allowed: &'static [&'static str],
232 pub rule_id: &'static str,
233 pub name: &'static str,
234 pub message: &'static str,
235 pub description: &'static str,
236 pub severity: &'static str,
237}