mif_rh/
harness_assert_graph.rs1use std::path::Path;
8
9use serde_json::Value;
10
11use crate::error::MifRhError;
12use crate::harness_project::read_json;
13
14#[derive(Debug, Clone)]
16pub struct CheckResult {
17 pub message: String,
20 pub passed: bool,
22}
23
24#[derive(Debug, Clone)]
27pub struct GraphAssertion {
28 pub checks: Vec<CheckResult>,
30 pub passed: bool,
32}
33
34fn is_urn_mif(value: &Value) -> bool {
35 value.as_str().is_some_and(|s| s.starts_with("urn:mif:"))
36}
37
38fn check(message: &str, passed: bool) -> CheckResult {
39 CheckResult {
40 message: message.to_string(),
41 passed,
42 }
43}
44
45#[must_use]
52pub fn assert_graph_mif(graph: &Value) -> GraphAssertion {
53 let nodes: Vec<&Value> = graph
54 .get("nodes")
55 .and_then(Value::as_array)
56 .into_iter()
57 .flatten()
58 .collect();
59 let edges: Vec<&Value> = graph
60 .get("edges")
61 .and_then(Value::as_array)
62 .into_iter()
63 .flatten()
64 .collect();
65
66 let node_ids: std::collections::HashSet<&str> = nodes
67 .iter()
68 .filter_map(|n| n.get("id").and_then(Value::as_str))
69 .collect();
70
71 let entity_node_ids: std::collections::HashSet<&str> = nodes
72 .iter()
73 .filter(|n| n.get("kind").and_then(Value::as_str) == Some("entity"))
74 .filter_map(|n| n.get("id").and_then(Value::as_str))
75 .filter(|id| id.starts_with("urn:mif:entity:"))
76 .collect();
77 let every_entity_edge_resolves = edges
84 .iter()
85 .filter(|e| e.get("via").and_then(Value::as_str) == Some("entity"))
86 .all(|e| {
87 e.get("target")
88 .and_then(Value::as_str)
89 .is_some_and(|t| entity_node_ids.contains(t))
90 });
91
92 let checks = vec![
93 check("graph has nodes", !nodes.is_empty()),
94 check("graph has edges", !edges.is_empty()),
95 check(
96 "every node id is a urn:mif: identifier (not a tag)",
97 nodes.iter().all(|n| is_urn_mif(&n["id"])),
98 ),
99 check(
100 "every edge source is a urn:mif: concept",
101 edges.iter().all(|e| is_urn_mif(&e["source"])),
102 ),
103 check(
104 "every edge target is a urn:mif: id",
105 edges.iter().all(|e| is_urn_mif(&e["target"])),
106 ),
107 check(
108 "at least one edge derives from a typed MIF relationship",
109 edges
110 .iter()
111 .any(|e| e.get("via").and_then(Value::as_str) == Some("relationship")),
112 ),
113 check(
114 "every referenced MIF entity is an entity node",
115 every_entity_edge_resolves,
116 ),
117 check(
118 "every edge target resolves to a node in the graph",
119 edges.iter().all(|e| {
120 e.get("target")
121 .and_then(Value::as_str)
122 .is_some_and(|t| node_ids.contains(t))
123 }),
124 ),
125 ];
126
127 let passed = checks.iter().all(|c| c.passed);
128 GraphAssertion { checks, passed }
129}
130
131pub fn assert_graph_mif_file(graph_path: &Path) -> Result<GraphAssertion, MifRhError> {
138 let graph = read_json(graph_path)?;
139 Ok(assert_graph_mif(&graph))
140}
141
142#[cfg(test)]
143mod tests {
144 use super::assert_graph_mif;
145 use serde_json::json;
146
147 #[test]
148 fn passes_a_well_formed_mif_graph() {
149 let graph = json!({
150 "nodes": [
151 {"id": "urn:mif:f1", "kind": "concept"},
152 {"id": "urn:mif:entity:e1", "kind": "entity"}
153 ],
154 "edges": [
155 {"source": "urn:mif:f1", "target": "urn:mif:f1", "via": "relationship"},
156 {"source": "urn:mif:f1", "target": "urn:mif:entity:e1", "via": "entity"}
157 ]
158 });
159
160 let assertion = assert_graph_mif(&graph);
161 assert!(assertion.passed, "{:?}", assertion.checks);
162 }
163
164 #[test]
165 fn fails_when_a_node_id_is_a_bare_tag() {
166 let graph = json!({
167 "nodes": [{"id": "some-tag", "kind": "concept"}],
168 "edges": [{"source": "some-tag", "target": "some-tag", "via": "relationship"}]
169 });
170
171 let assertion = assert_graph_mif(&graph);
172 assert!(!assertion.passed);
173 assert!(
174 !assertion.checks[2].passed,
175 "node-id-is-urn check should fail"
176 );
177 }
178
179 #[test]
180 fn fails_when_no_edge_is_a_typed_relationship() {
181 let graph = json!({
182 "nodes": [{"id": "urn:mif:f1", "kind": "concept"}],
183 "edges": [{"source": "urn:mif:f1", "target": "urn:mif:f1", "via": "entity"}]
184 });
185
186 let assertion = assert_graph_mif(&graph);
187 assert!(!assertion.checks[5].passed);
188 }
189
190 #[test]
191 fn requires_an_entity_node_only_when_an_entity_mention_edge_exists() {
192 let graph = json!({
195 "nodes": [{"id": "urn:mif:f1", "kind": "concept"}],
196 "edges": [{"source": "urn:mif:f1", "target": "urn:mif:f1", "via": "relationship"}]
197 });
198 let assertion = assert_graph_mif(&graph);
199 assert!(assertion.checks[6].passed);
200 }
201
202 #[test]
203 fn fails_when_an_entity_mention_edge_has_no_matching_entity_node() {
204 let graph = json!({
205 "nodes": [{"id": "urn:mif:f1", "kind": "concept"}],
206 "edges": [
207 {"source": "urn:mif:f1", "target": "urn:mif:f1", "via": "relationship"},
208 {"source": "urn:mif:f1", "target": "urn:mif:entity:missing", "via": "entity"}
209 ]
210 });
211 let assertion = assert_graph_mif(&graph);
212 assert!(!assertion.checks[6].passed);
213 }
214
215 #[test]
216 fn fails_when_one_of_several_entity_mention_edges_targets_a_non_entity_node() {
217 let graph = json!({
222 "nodes": [
223 {"id": "urn:mif:f1", "kind": "concept"},
224 {"id": "urn:mif:entity:acme", "kind": "entity"}
225 ],
226 "edges": [
227 {"source": "urn:mif:f1", "target": "urn:mif:entity:acme", "via": "entity"},
228 {"source": "urn:mif:f1", "target": "urn:mif:f1", "via": "entity"}
229 ]
230 });
231 let assertion = assert_graph_mif(&graph);
232 assert!(!assertion.checks[6].passed);
233 }
234
235 #[test]
236 fn fails_when_an_edge_target_does_not_resolve_to_a_node() {
237 let graph = json!({
238 "nodes": [{"id": "urn:mif:f1", "kind": "concept"}],
239 "edges": [
240 {"source": "urn:mif:f1", "target": "urn:mif:f1", "via": "relationship"},
241 {"source": "urn:mif:f1", "target": "urn:mif:ghost", "via": "relationship"}
242 ]
243 });
244 let assertion = assert_graph_mif(&graph);
245 assert!(!assertion.checks[7].passed);
246 }
247
248 #[test]
249 fn fails_closed_on_an_empty_graph() {
250 let graph = json!({"nodes": [], "edges": []});
251 let assertion = assert_graph_mif(&graph);
252 assert!(!assertion.passed);
253 assert!(!assertion.checks[0].passed);
254 assert!(!assertion.checks[1].passed);
255 }
256}