1use std::collections::{HashMap, HashSet};
17
18use panproto_gat::Name;
19use panproto_schema::Schema;
20
21use crate::functor::FInstance;
22use crate::value::Value;
23use crate::wtype::WInstance;
24
25#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct AttributeDecl {
29 pub name: String,
32 pub kind: Name,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct AttributeSchema {
40 pub attrs: HashMap<Name, Vec<AttributeDecl>>,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum AttributeViolation {
48 Undeclared {
51 node_id: u32,
53 anchor: Name,
55 key: String,
57 },
58 Mistyped {
60 node_id: u32,
62 anchor: Name,
64 key: String,
66 expected: Name,
68 found: String,
70 },
71}
72
73impl AttributeSchema {
74 #[must_use]
82 pub fn from_schema(schema: &Schema) -> Self {
83 let mut attrs: HashMap<Name, Vec<AttributeDecl>> = HashMap::new();
84 for vertex_id in schema.vertices.keys() {
85 let mut decls: Vec<AttributeDecl> = Vec::new();
86 let mut seen: HashSet<String> = HashSet::new();
87 for edge in schema.outgoing_edges(vertex_id.as_ref()) {
88 if !schema.outgoing_edges(edge.tgt.as_ref()).is_empty() {
90 continue;
91 }
92 let Some(tgt_vertex) = schema.vertex(edge.tgt.as_ref()) else {
93 continue;
94 };
95 let name = edge
96 .name
97 .as_ref()
98 .map_or_else(|| edge.tgt.to_string(), Name::to_string);
99 if !seen.insert(name.clone()) {
100 continue;
101 }
102 decls.push(AttributeDecl {
103 name,
104 kind: tgt_vertex.kind.clone(),
105 });
106 }
107 if !decls.is_empty() {
108 decls.sort_by(|a, b| a.name.cmp(&b.name));
109 attrs.insert(vertex_id.clone(), decls);
110 }
111 }
112 Self { attrs }
113 }
114
115 #[must_use]
117 pub fn for_vertex(&self, vertex: &str) -> &[AttributeDecl] {
118 self.attrs.get(vertex).map_or(&[], Vec::as_slice)
119 }
120
121 #[must_use]
128 pub fn validate_wtype(&self, instance: &WInstance) -> Vec<AttributeViolation> {
129 let mut violations = Vec::new();
130 for node in instance.nodes.values() {
131 let Some(decls) = self.attrs.get(&node.anchor) else {
132 continue;
133 };
134 for (key, value) in &node.extra_fields {
135 Self::check_one(node.id, &node.anchor, key, value, decls, &mut violations);
136 }
137 }
138 violations
139 }
140
141 #[must_use]
144 pub fn validate_finstance(&self, instance: &FInstance) -> Vec<AttributeViolation> {
145 let mut violations = Vec::new();
146 for (table, rows) in &instance.tables {
147 let anchor = Name::from(table.as_str());
148 let Some(decls) = self.attrs.get(&anchor) else {
149 continue;
150 };
151 for (row_idx, row) in rows.iter().enumerate() {
152 let row_id = u32::try_from(row_idx).unwrap_or(u32::MAX);
153 for (key, value) in row {
154 Self::check_one(row_id, &anchor, key, value, decls, &mut violations);
155 }
156 }
157 }
158 violations
159 }
160
161 fn check_one(
162 node_id: u32,
163 anchor: &Name,
164 key: &str,
165 value: &Value,
166 decls: &[AttributeDecl],
167 out: &mut Vec<AttributeViolation>,
168 ) {
169 match decls.iter().find(|d| d.name == key) {
170 None => out.push(AttributeViolation::Undeclared {
171 node_id,
172 anchor: anchor.clone(),
173 key: key.to_owned(),
174 }),
175 Some(decl) if !kind_accepts(&decl.kind, value) => {
176 out.push(AttributeViolation::Mistyped {
177 node_id,
178 anchor: anchor.clone(),
179 key: key.to_owned(),
180 expected: decl.kind.clone(),
181 found: value.type_name().to_owned(),
182 });
183 }
184 Some(_) => {}
185 }
186 }
187}
188
189fn kind_accepts(kind: &Name, value: &Value) -> bool {
195 if matches!(value, Value::Null) {
196 return true;
197 }
198 match kind.as_ref() {
199 "string" | "str" | "text" => {
200 matches!(value, Value::Str(_) | Value::Token(_) | Value::CidLink(_))
201 }
202 "int" | "integer" => matches!(value, Value::Int(_)),
203 "float" | "number" | "double" | "decimal" => {
204 matches!(value, Value::Float(_) | Value::Int(_))
205 }
206 "bool" | "boolean" => matches!(value, Value::Bool(_)),
207 "bytes" | "blob" => matches!(value, Value::Bytes(_) | Value::Blob { .. }),
208 _ => true,
210 }
211}
212
213#[cfg(test)]
214#[allow(clippy::expect_used, clippy::unwrap_used)]
215mod tests {
216 use std::collections::HashMap;
217
218 use panproto_schema::{Edge, Schema, Vertex};
219
220 use super::*;
221 use crate::metadata::Node;
222
223 fn attr_schema() -> Schema {
224 use smallvec::{SmallVec, smallvec};
225
226 let vertices = [
227 ("post", "object"),
228 ("post.title", "string"),
229 ("post.count", "int"),
230 ]
231 .into_iter()
232 .map(|(id, kind)| {
233 (
234 Name::from(id),
235 Vertex {
236 id: Name::from(id),
237 kind: Name::from(kind),
238 nsid: None,
239 },
240 )
241 })
242 .collect();
243
244 let title = Edge {
245 src: "post".into(),
246 tgt: "post.title".into(),
247 kind: "prop".into(),
248 name: Some("title".into()),
249 };
250 let count = Edge {
251 src: "post".into(),
252 tgt: "post.count".into(),
253 kind: "prop".into(),
254 name: Some("count".into()),
255 };
256 let mut outgoing: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
257 outgoing.insert("post".into(), smallvec![title.clone(), count.clone()]);
258 let edges = HashMap::from([(title, Name::from("prop")), (count, Name::from("prop"))]);
259
260 Schema {
261 protocol: "test".into(),
262 vertices,
263 edges,
264 hyper_edges: HashMap::new(),
265 constraints: HashMap::new(),
266 required: HashMap::new(),
267 nsids: HashMap::new(),
268 entries: Vec::new(),
269 variants: HashMap::new(),
270 orderings: HashMap::new(),
271 recursion_points: HashMap::new(),
272 spans: HashMap::new(),
273 usage_modes: HashMap::new(),
274 nominal: HashMap::new(),
275 coercions: HashMap::new(),
276 mergers: HashMap::new(),
277 defaults: HashMap::new(),
278 policies: HashMap::new(),
279 outgoing,
280 incoming: HashMap::new(),
281 between: HashMap::new(),
282 }
283 }
284
285 #[test]
286 fn derives_ordered_attributes() {
287 let attrs = AttributeSchema::from_schema(&attr_schema());
288 let post = attrs.for_vertex("post");
289 assert_eq!(post.len(), 2);
290 assert_eq!(post[0].name, "count");
292 assert_eq!(post[0].kind.as_ref(), "int");
293 assert_eq!(post[1].name, "title");
294 assert_eq!(post[1].kind.as_ref(), "string");
295 }
296
297 #[test]
298 fn accepts_declared_and_well_typed() {
299 let attrs = AttributeSchema::from_schema(&attr_schema());
300 let mut nodes = HashMap::new();
301 nodes.insert(
302 0,
303 Node::new(0, "post")
304 .with_extra_field("title", Value::Str("hello".into()))
305 .with_extra_field("count", Value::Int(3)),
306 );
307 let inst = WInstance::new(nodes, vec![], vec![], 0, "post".into());
308 assert!(attrs.validate_wtype(&inst).is_empty());
309 }
310
311 #[test]
312 fn flags_undeclared_key() {
313 let attrs = AttributeSchema::from_schema(&attr_schema());
314 let mut nodes = HashMap::new();
315 nodes.insert(
316 0,
317 Node::new(0, "post").with_extra_field("bogus", Value::Int(1)),
318 );
319 let inst = WInstance::new(nodes, vec![], vec![], 0, "post".into());
320 let v = attrs.validate_wtype(&inst);
321 assert!(matches!(
322 v.as_slice(),
323 [AttributeViolation::Undeclared { key, node_id: 0, .. }] if key == "bogus"
324 ));
325 }
326
327 #[test]
328 fn flags_mistyped_value() {
329 let attrs = AttributeSchema::from_schema(&attr_schema());
330 let mut nodes = HashMap::new();
331 nodes.insert(
332 0,
333 Node::new(0, "post").with_extra_field("count", Value::Str("three".into())),
334 );
335 let inst = WInstance::new(nodes, vec![], vec![], 0, "post".into());
336 let v = attrs.validate_wtype(&inst);
337 assert!(matches!(
338 v.as_slice(),
339 [AttributeViolation::Mistyped { key, expected, .. }]
340 if key == "count" && expected.as_ref() == "int"
341 ));
342 }
343
344 #[test]
345 fn validates_finstance_rows() {
346 let attrs = AttributeSchema::from_schema(&attr_schema());
347 let good = HashMap::from([
348 ("title".to_owned(), Value::Str("x".into())),
349 ("count".to_owned(), Value::Int(1)),
350 ]);
351 let bad = HashMap::from([("count".to_owned(), Value::Str("nope".into()))]);
352 let inst = FInstance::new().with_table("post", vec![good, bad]);
353 let v = attrs.validate_finstance(&inst);
354 assert_eq!(v.len(), 1);
355 assert!(matches!(
356 &v[0],
357 AttributeViolation::Mistyped { node_id: 1, key, .. } if key == "count"
358 ));
359 }
360}