1use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ShapeType {
10 NodeShape,
12 PropertyShape,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ShapeConstraint {
19 pub constraint_type: String,
21 pub value: String,
23}
24
25#[derive(Debug, Clone)]
27pub struct ShapeNode {
28 pub id: String,
30 pub shape_type: ShapeType,
32 pub target_class: Option<String>,
34 pub target_node: Option<String>,
36 pub constraints: Vec<ShapeConstraint>,
38 pub property_shapes: Vec<String>,
40 pub path: Option<String>,
42 pub deactivated: bool,
44}
45
46#[derive(Debug, Default)]
48pub struct ShapeGraph {
49 shapes: HashMap<String, ShapeNode>,
50}
51
52#[derive(Debug, Clone)]
54pub struct LoadResult {
55 pub shapes_loaded: usize,
57 pub warnings: Vec<String>,
59}
60
61impl ShapeGraph {
62 pub fn new() -> Self {
64 Self {
65 shapes: HashMap::new(),
66 }
67 }
68
69 pub fn add_shape(&mut self, shape: ShapeNode) -> Result<(), ShapeGraphError> {
73 if self.shapes.contains_key(&shape.id) {
74 return Err(ShapeGraphError::DuplicateId(shape.id.clone()));
75 }
76 self.shapes.insert(shape.id.clone(), shape);
77 Ok(())
78 }
79
80 pub fn get_shape(&self, id: &str) -> Option<&ShapeNode> {
82 self.shapes.get(id)
83 }
84
85 pub fn node_shapes(&self) -> Vec<&ShapeNode> {
87 self.shapes
88 .values()
89 .filter(|s| s.shape_type == ShapeType::NodeShape)
90 .collect()
91 }
92
93 pub fn property_shapes(&self) -> Vec<&ShapeNode> {
95 self.shapes
96 .values()
97 .filter(|s| s.shape_type == ShapeType::PropertyShape)
98 .collect()
99 }
100
101 pub fn shapes_for_class(&self, class_iri: &str) -> Vec<&ShapeNode> {
103 self.shapes
104 .values()
105 .filter(|s| {
106 s.shape_type == ShapeType::NodeShape && s.target_class.as_deref() == Some(class_iri)
107 })
108 .collect()
109 }
110
111 pub fn resolve_property_shapes(&self, node_shape_id: &str) -> Vec<&ShapeNode> {
113 match self.shapes.get(node_shape_id) {
114 None => vec![],
115 Some(node_shape) => node_shape
116 .property_shapes
117 .iter()
118 .filter_map(|ps_id| self.shapes.get(ps_id))
119 .collect(),
120 }
121 }
122
123 pub fn shape_count(&self) -> usize {
125 self.shapes.len()
126 }
127
128 pub fn validate_graph(&self) -> Vec<String> {
130 let mut issues = Vec::new();
131 for shape in self.shapes.values() {
132 if shape.shape_type == ShapeType::PropertyShape && shape.path.is_none() {
134 issues.push(format!(
135 "PropertyShape <{}> has no sh:path defined.",
136 shape.id
137 ));
138 }
139 for ps_id in &shape.property_shapes {
141 if !self.shapes.contains_key(ps_id) {
142 issues.push(format!(
143 "NodeShape <{}> references missing PropertyShape <{}>.",
144 shape.id, ps_id
145 ));
146 }
147 }
148 if shape.deactivated {
150 issues.push(format!("Shape <{}> is deactivated.", shape.id));
151 }
152 }
153 issues
154 }
155
156 pub fn remove_shape(&mut self, id: &str) -> bool {
158 self.shapes.remove(id).is_some()
159 }
160
161 pub fn deactivated_shapes(&self) -> Vec<&ShapeNode> {
163 self.shapes.values().filter(|s| s.deactivated).collect()
164 }
165}
166
167#[derive(Debug)]
169pub enum ShapeGraphError {
170 DuplicateId(String),
172 InvalidShape(String),
174}
175
176impl std::fmt::Display for ShapeGraphError {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 match self {
179 Self::DuplicateId(id) => write!(f, "Duplicate shape id: {id}"),
180 Self::InvalidShape(msg) => write!(f, "Invalid shape: {msg}"),
181 }
182 }
183}
184
185impl std::error::Error for ShapeGraphError {}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn node_shape(id: &str, class: Option<&str>) -> ShapeNode {
192 ShapeNode {
193 id: id.to_string(),
194 shape_type: ShapeType::NodeShape,
195 target_class: class.map(|s| s.to_string()),
196 target_node: None,
197 constraints: vec![],
198 property_shapes: vec![],
199 path: None,
200 deactivated: false,
201 }
202 }
203
204 fn property_shape(id: &str, path: Option<&str>) -> ShapeNode {
205 ShapeNode {
206 id: id.to_string(),
207 shape_type: ShapeType::PropertyShape,
208 target_class: None,
209 target_node: None,
210 constraints: vec![],
211 property_shapes: vec![],
212 path: path.map(|s| s.to_string()),
213 deactivated: false,
214 }
215 }
216
217 #[test]
220 fn test_add_and_get_shape() {
221 let mut sg = ShapeGraph::new();
222 sg.add_shape(node_shape("http://s1", Some("http://Person")))
223 .expect("should succeed");
224 assert!(sg.get_shape("http://s1").is_some());
225 }
226
227 #[test]
228 fn test_get_missing_shape() {
229 let sg = ShapeGraph::new();
230 assert!(sg.get_shape("http://missing").is_none());
231 }
232
233 #[test]
234 fn test_duplicate_id_error() {
235 let mut sg = ShapeGraph::new();
236 sg.add_shape(node_shape("http://s1", None))
237 .expect("should succeed");
238 let err = sg.add_shape(node_shape("http://s1", None));
239 assert!(matches!(err, Err(ShapeGraphError::DuplicateId(_))));
240 }
241
242 #[test]
243 fn test_shape_count_empty() {
244 let sg = ShapeGraph::new();
245 assert_eq!(sg.shape_count(), 0);
246 }
247
248 #[test]
249 fn test_shape_count_after_adds() {
250 let mut sg = ShapeGraph::new();
251 sg.add_shape(node_shape("http://s1", None))
252 .expect("should succeed");
253 sg.add_shape(property_shape("http://ps1", Some("http://name")))
254 .expect("should succeed");
255 assert_eq!(sg.shape_count(), 2);
256 }
257
258 #[test]
261 fn test_node_shapes_filter() {
262 let mut sg = ShapeGraph::new();
263 sg.add_shape(node_shape("http://ns1", None))
264 .expect("should succeed");
265 sg.add_shape(property_shape("http://ps1", Some("http://p")))
266 .expect("should succeed");
267 let ns = sg.node_shapes();
268 assert_eq!(ns.len(), 1);
269 assert_eq!(ns[0].id, "http://ns1");
270 }
271
272 #[test]
273 fn test_property_shapes_filter() {
274 let mut sg = ShapeGraph::new();
275 sg.add_shape(node_shape("http://ns1", None))
276 .expect("should succeed");
277 sg.add_shape(property_shape("http://ps1", Some("http://p")))
278 .expect("should succeed");
279 sg.add_shape(property_shape("http://ps2", Some("http://q")))
280 .expect("should succeed");
281 let ps = sg.property_shapes();
282 assert_eq!(ps.len(), 2);
283 }
284
285 #[test]
288 fn test_shapes_for_class_match() {
289 let mut sg = ShapeGraph::new();
290 sg.add_shape(node_shape("http://s1", Some("http://Person")))
291 .expect("should succeed");
292 sg.add_shape(node_shape("http://s2", Some("http://Organization")))
293 .expect("should succeed");
294 let result = sg.shapes_for_class("http://Person");
295 assert_eq!(result.len(), 1);
296 assert_eq!(result[0].id, "http://s1");
297 }
298
299 #[test]
300 fn test_shapes_for_class_no_match() {
301 let mut sg = ShapeGraph::new();
302 sg.add_shape(node_shape("http://s1", Some("http://Person")))
303 .expect("should succeed");
304 let result = sg.shapes_for_class("http://Animal");
305 assert!(result.is_empty());
306 }
307
308 #[test]
309 fn test_shapes_for_class_multiple() {
310 let mut sg = ShapeGraph::new();
311 sg.add_shape(node_shape("http://s1", Some("http://Person")))
312 .expect("should succeed");
313 sg.add_shape(node_shape("http://s2", Some("http://Person")))
314 .expect("should succeed");
315 let result = sg.shapes_for_class("http://Person");
316 assert_eq!(result.len(), 2);
317 }
318
319 #[test]
322 fn test_resolve_property_shapes() {
323 let mut sg = ShapeGraph::new();
324 let mut ns = node_shape("http://ns1", None);
325 ns.property_shapes = vec!["http://ps1".to_string()];
326 sg.add_shape(ns).expect("should succeed");
327 sg.add_shape(property_shape("http://ps1", Some("http://name")))
328 .expect("should succeed");
329 let resolved = sg.resolve_property_shapes("http://ns1");
330 assert_eq!(resolved.len(), 1);
331 assert_eq!(resolved[0].id, "http://ps1");
332 }
333
334 #[test]
335 fn test_resolve_property_shapes_missing_ref() {
336 let mut sg = ShapeGraph::new();
337 let mut ns = node_shape("http://ns1", None);
338 ns.property_shapes = vec!["http://missing_ps".to_string()];
339 sg.add_shape(ns).expect("should succeed");
340 let resolved = sg.resolve_property_shapes("http://ns1");
341 assert!(resolved.is_empty());
342 }
343
344 #[test]
345 fn test_resolve_property_shapes_unknown_node() {
346 let sg = ShapeGraph::new();
347 let resolved = sg.resolve_property_shapes("http://unknown");
348 assert!(resolved.is_empty());
349 }
350
351 #[test]
354 fn test_validate_no_warnings() {
355 let mut sg = ShapeGraph::new();
356 let mut ns = node_shape("http://ns1", None);
357 ns.property_shapes = vec!["http://ps1".to_string()];
358 sg.add_shape(ns).expect("should succeed");
359 sg.add_shape(property_shape("http://ps1", Some("http://name")))
360 .expect("should succeed");
361 let warnings: Vec<_> = sg
362 .validate_graph()
363 .into_iter()
364 .filter(|w| w.contains("missing"))
365 .collect();
366 assert!(warnings.is_empty());
367 }
368
369 #[test]
370 fn test_validate_missing_property_shape() {
371 let mut sg = ShapeGraph::new();
372 let mut ns = node_shape("http://ns1", None);
373 ns.property_shapes = vec!["http://missing_ps".to_string()];
374 sg.add_shape(ns).expect("should succeed");
375 let warnings = sg.validate_graph();
376 assert!(warnings.iter().any(|w| w.contains("missing")));
377 }
378
379 #[test]
380 fn test_validate_property_shape_no_path() {
381 let mut sg = ShapeGraph::new();
382 sg.add_shape(property_shape("http://ps1", None))
383 .expect("should succeed");
384 let warnings = sg.validate_graph();
385 assert!(warnings.iter().any(|w| w.contains("no sh:path")));
386 }
387
388 #[test]
389 fn test_validate_deactivated_shape_warning() {
390 let mut sg = ShapeGraph::new();
391 let mut s = node_shape("http://s1", None);
392 s.deactivated = true;
393 sg.add_shape(s).expect("should succeed");
394 let warnings = sg.validate_graph();
395 assert!(warnings.iter().any(|w| w.contains("deactivated")));
396 }
397
398 #[test]
401 fn test_remove_shape_present() {
402 let mut sg = ShapeGraph::new();
403 sg.add_shape(node_shape("http://s1", None))
404 .expect("should succeed");
405 assert!(sg.remove_shape("http://s1"));
406 assert_eq!(sg.shape_count(), 0);
407 }
408
409 #[test]
410 fn test_remove_shape_absent() {
411 let mut sg = ShapeGraph::new();
412 assert!(!sg.remove_shape("http://missing"));
413 }
414
415 #[test]
418 fn test_deactivated_shapes_none() {
419 let mut sg = ShapeGraph::new();
420 sg.add_shape(node_shape("http://s1", None))
421 .expect("should succeed");
422 assert!(sg.deactivated_shapes().is_empty());
423 }
424
425 #[test]
426 fn test_deactivated_shapes_some() {
427 let mut sg = ShapeGraph::new();
428 let mut s = node_shape("http://s1", None);
429 s.deactivated = true;
430 sg.add_shape(s).expect("should succeed");
431 sg.add_shape(node_shape("http://s2", None))
432 .expect("should succeed");
433 assert_eq!(sg.deactivated_shapes().len(), 1);
434 }
435
436 #[test]
439 fn test_constraints_stored() {
440 let mut sg = ShapeGraph::new();
441 let mut s = node_shape("http://s1", None);
442 s.constraints = vec![ShapeConstraint {
443 constraint_type: "sh:minCount".to_string(),
444 value: "1".to_string(),
445 }];
446 sg.add_shape(s).expect("should succeed");
447 let stored = sg.get_shape("http://s1").expect("should succeed");
448 assert_eq!(stored.constraints.len(), 1);
449 assert_eq!(stored.constraints[0].constraint_type, "sh:minCount");
450 assert_eq!(stored.constraints[0].value, "1");
451 }
452
453 #[test]
454 fn test_multiple_constraints() {
455 let mut sg = ShapeGraph::new();
456 let mut s = property_shape("http://ps1", Some("http://name"));
457 s.constraints = vec![
458 ShapeConstraint {
459 constraint_type: "sh:minCount".to_string(),
460 value: "1".to_string(),
461 },
462 ShapeConstraint {
463 constraint_type: "sh:datatype".to_string(),
464 value: "xsd:string".to_string(),
465 },
466 ];
467 sg.add_shape(s).expect("should succeed");
468 let stored = sg.get_shape("http://ps1").expect("should succeed");
469 assert_eq!(stored.constraints.len(), 2);
470 }
471
472 #[test]
475 fn test_duplicate_id_error_display() {
476 let err = ShapeGraphError::DuplicateId("http://s1".to_string());
477 assert!(format!("{err}").contains("http://s1"));
478 }
479
480 #[test]
481 fn test_invalid_shape_error_display() {
482 let err = ShapeGraphError::InvalidShape("bad".to_string());
483 assert!(format!("{err}").contains("bad"));
484 }
485
486 #[test]
489 fn test_shape_type_node() {
490 let mut sg = ShapeGraph::new();
491 sg.add_shape(node_shape("http://ns1", None))
492 .expect("should succeed");
493 assert_eq!(
494 sg.get_shape("http://ns1")
495 .expect("should succeed")
496 .shape_type,
497 ShapeType::NodeShape
498 );
499 }
500
501 #[test]
502 fn test_shape_type_property() {
503 let mut sg = ShapeGraph::new();
504 sg.add_shape(property_shape("http://ps1", Some("http://p")))
505 .expect("should succeed");
506 assert_eq!(
507 sg.get_shape("http://ps1")
508 .expect("should succeed")
509 .shape_type,
510 ShapeType::PropertyShape
511 );
512 }
513
514 #[test]
515 fn test_target_node_stored() {
516 let mut sg = ShapeGraph::new();
517 let mut s = node_shape("http://ns1", None);
518 s.target_node = Some("http://alice".to_string());
519 sg.add_shape(s).expect("should succeed");
520 assert_eq!(
521 sg.get_shape("http://ns1")
522 .expect("should succeed")
523 .target_node
524 .as_deref(),
525 Some("http://alice")
526 );
527 }
528
529 #[test]
530 fn test_path_stored_in_property_shape() {
531 let mut sg = ShapeGraph::new();
532 sg.add_shape(property_shape("http://ps1", Some("http://schema.org/name")))
533 .expect("should succeed");
534 assert_eq!(
535 sg.get_shape("http://ps1")
536 .expect("should succeed")
537 .path
538 .as_deref(),
539 Some("http://schema.org/name")
540 );
541 }
542
543 #[test]
544 fn test_shape_graph_default() {
545 let sg = ShapeGraph::default();
546 assert_eq!(sg.shape_count(), 0);
547 }
548}