1use crate::bindings::ResolvedBindings;
4use crate::contracts::{BindingReference, ExtensionId, PropertyRequirement};
5use crate::registry::ActiveExtensions;
6use knowledge_base_models::{Cardinality, EntityTypeId, Property, PropertyId, PropertyUsage, ValueType};
7use knowledge_base_snapshot::RepositorySnapshot;
8use std::collections::{BTreeMap, BTreeSet};
9use std::fmt;
10
11#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
13pub enum OntologyContractField {
14 ValueType,
15 Usage,
16 Cardinality,
17 SubjectTypes,
18 TargetTypes,
19 AllowedQualifiers,
20}
21
22impl fmt::Display for OntologyContractField {
23 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24 formatter.write_str(match self {
25 Self::ValueType => "value_type",
26 Self::Usage => "usage",
27 Self::Cardinality => "cardinality",
28 Self::SubjectTypes => "subject_types",
29 Self::TargetTypes => "target_types",
30 Self::AllowedQualifiers => "allowed_qualifiers",
31 })
32 }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct OntologyContractDiagnostic {
38 pub extension: ExtensionId,
39 pub binding: BindingReference,
40 pub id: PropertyId,
41 pub field: OntologyContractField,
42 pub message: String,
43}
44
45impl fmt::Display for OntologyContractDiagnostic {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 write!(
48 formatter,
49 "extension {} binding {} resolves to property {} with incompatible {}: {}",
50 self.extension, self.binding, self.id, self.field, self.message
51 )
52 }
53}
54
55pub fn verify_ontology_contracts(snapshot: &RepositorySnapshot, active: &ActiveExtensions, bindings: &ResolvedBindings) -> Vec<OntologyContractDiagnostic> {
60 let mut diagnostics = Vec::new();
61 let mut requirements = BTreeMap::<PropertyId, MergedPropertyRequirement>::new();
62
63 for extension in active.extensions() {
64 let metadata = extension.metadata();
65 for requirement in &metadata.ontology_requirements.properties {
68 let id = bindings
69 .property(metadata, &requirement.binding)
70 .expect("active requirements have validated property bindings")
71 .clone();
72 requirements
73 .entry(id.clone())
74 .or_default()
75 .merge(id, metadata.id.clone(), requirement, bindings, metadata, &mut diagnostics);
76 }
77 }
78
79 for (id, requirement) in requirements {
80 let property = snapshot.properties().get(&id).expect("manifest activation confirms all bound properties exist");
81 requirement.verify(property, &mut diagnostics);
82 }
83
84 diagnostics.sort_by(|left, right| {
85 (&left.binding, &left.id, left.field, &left.message, &left.extension).cmp(&(&right.binding, &right.id, right.field, &right.message, &right.extension))
86 });
87 diagnostics
88}
89
90#[derive(Default)]
91struct MergedPropertyRequirement {
92 value_type: Scalar<ValueType>,
93 usage: Scalar<PropertyUsage>,
94 cardinality: Scalar<Cardinality>,
95 subject_types: BTreeMap<EntityTypeId, Contributor>,
96 target_types: BTreeMap<EntityTypeId, Contributor>,
97 allowed_qualifiers: BTreeMap<PropertyId, Contributor>,
98}
99
100struct Scalar<T> {
101 expected: Option<(T, Contributor)>,
102 conflict: bool,
103}
104
105impl<T> Default for Scalar<T> {
106 fn default() -> Self {
107 Self { expected: None, conflict: false }
108 }
109}
110
111#[derive(Clone)]
112struct Contributor {
113 extension: ExtensionId,
114 binding: BindingReference,
115}
116
117impl MergedPropertyRequirement {
118 fn merge(
119 &mut self,
120 id: PropertyId,
121 extension: ExtensionId,
122 requirement: &PropertyRequirement,
123 bindings: &ResolvedBindings,
124 metadata: &crate::contracts::ExtensionMetadata,
125 diagnostics: &mut Vec<OntologyContractDiagnostic>,
126 ) {
127 let contributor = Contributor {
128 extension,
129 binding: requirement.binding.clone(),
130 };
131 self.value_type
132 .merge(id.clone(), requirement.value_type, OntologyContractField::ValueType, &contributor, diagnostics);
133 self.usage.merge(id.clone(), requirement.usage, OntologyContractField::Usage, &contributor, diagnostics);
134 self.cardinality
135 .merge(id, requirement.cardinality, OntologyContractField::Cardinality, &contributor, diagnostics);
136 for binding in &requirement.subject_types {
137 let id = bindings
138 .entity_type(metadata, binding)
139 .expect("active requirements have validated entity-type bindings")
140 .clone();
141 self.subject_types.entry(id).or_insert_with(|| contributor.clone());
142 }
143 if let Some(target_types) = &requirement.target_types {
144 for binding in target_types {
145 let id = bindings
146 .entity_type(metadata, binding)
147 .expect("active requirements have validated entity-type bindings")
148 .clone();
149 self.target_types.entry(id).or_insert_with(|| contributor.clone());
150 }
151 }
152 for binding in &requirement.allowed_qualifiers {
153 let id = bindings.property(metadata, binding).expect("active requirements have validated property bindings").clone();
154 self.allowed_qualifiers.entry(id).or_insert_with(|| contributor.clone());
155 }
156 }
157
158 fn verify(&self, property: &Property, diagnostics: &mut Vec<OntologyContractDiagnostic>) {
159 self.value_type.verify(property, OntologyContractField::ValueType, property.value_type, diagnostics);
160 self.usage.verify(property, OntologyContractField::Usage, property.usage, diagnostics);
161 self.cardinality.verify(property, OntologyContractField::Cardinality, property.cardinality, diagnostics);
162 verify_collection(
163 property,
164 OntologyContractField::SubjectTypes,
165 &self.subject_types,
166 property.subject_types.iter().cloned().collect(),
167 diagnostics,
168 );
169 if !self.target_types.is_empty() && property.value_type != ValueType::Entity {
170 let contributor = self.target_types.values().next().expect("non-empty map has a contributor");
171 diagnostics.push(diagnostic(
172 contributor,
173 property,
174 OntologyContractField::TargetTypes,
175 format!("property value_type is {}, but target types require entity", render(property.value_type)),
176 ));
177 } else {
178 verify_collection(
179 property,
180 OntologyContractField::TargetTypes,
181 &self.target_types,
182 property.target_types.clone().unwrap_or_default().into_iter().collect(),
183 diagnostics,
184 );
185 }
186 verify_collection(
187 property,
188 OntologyContractField::AllowedQualifiers,
189 &self.allowed_qualifiers,
190 property.allowed_qualifiers.iter().cloned().collect(),
191 diagnostics,
192 );
193 }
194}
195
196impl<T> Scalar<T>
197where
198 T: Copy + Eq + fmt::Debug,
199{
200 fn merge(&mut self, id: PropertyId, expected: Option<T>, field: OntologyContractField, contributor: &Contributor, diagnostics: &mut Vec<OntologyContractDiagnostic>) {
201 let Some(expected) = expected else { return };
202 match &self.expected {
203 None => self.expected = Some((expected, contributor.clone())),
204 Some((existing, _first)) if *existing == expected => {}
205 Some((existing, first)) => {
206 self.conflict = true;
207 diagnostics.push(OntologyContractDiagnostic {
208 extension: contributor.extension.clone(),
209 binding: contributor.binding.clone(),
210 id,
211 field,
212 message: format!(
213 "requires {}, conflicting with {} required by extension {} binding {}",
214 render(expected),
215 render(*existing),
216 first.extension,
217 first.binding
218 ),
219 });
220 }
221 }
222 }
223
224 fn verify(&self, property: &Property, field: OntologyContractField, actual: T, diagnostics: &mut Vec<OntologyContractDiagnostic>) {
225 let Some((expected, contributor)) = &self.expected else { return };
226 if !self.conflict && *expected != actual {
227 diagnostics.push(diagnostic(
228 contributor,
229 property,
230 field,
231 format!("expected {}, found {}", render(*expected), render(actual)),
232 ));
233 }
234 }
235}
236
237fn verify_collection<T>(
238 property: &Property,
239 field: OntologyContractField,
240 expected: &BTreeMap<T, Contributor>,
241 actual: BTreeSet<T>,
242 diagnostics: &mut Vec<OntologyContractDiagnostic>,
243) where
244 T: Ord + fmt::Display,
245{
246 for (id, contributor) in expected {
247 if !actual.contains(id) {
248 diagnostics.push(diagnostic(contributor, property, field, format!("missing required entry {id}")));
249 }
250 }
251}
252
253fn diagnostic(contributor: &Contributor, property: &Property, field: OntologyContractField, message: String) -> OntologyContractDiagnostic {
254 OntologyContractDiagnostic {
255 extension: contributor.extension.clone(),
256 binding: contributor.binding.clone(),
257 id: property.id.clone(),
258 field,
259 message,
260 }
261}
262
263fn render(value: impl fmt::Debug) -> String {
264 format!("{value:?}").to_lowercase()
265}