1use serde_json::Value;
20
21use crate::model::{Edge, Graph, GraphSet, Metadata, PROVENANCE_KEY, namespace};
22
23pub fn compose(set: &GraphSet) -> Graph {
25 let mut composed = Graph::composed();
26
27 for fragment in &set.graphs {
28 let namespace = namespace_of(fragment);
29
30 for (path, node) in &fragment.nodes {
31 let entry = composed.nodes.entry(path.clone()).or_default();
32 entry
33 .metadata
34 .insert(namespace.clone(), Value::Object(node.metadata.clone()));
35 add_provenance(&mut entry.metadata, &namespace);
36 }
37 }
38
39 let mut edge_index: std::collections::HashMap<(String, String), usize> =
42 std::collections::HashMap::new();
43
44 for fragment in &set.graphs {
45 let namespace = namespace_of(fragment);
46
47 for edge in &fragment.edges {
48 let key = (edge.source.clone(), edge.target.clone());
49 let idx = *edge_index.entry(key).or_insert_with(|| {
50 composed.edges.push(Edge::with_metadata(
51 &edge.source,
52 &edge.target,
53 Metadata::new(),
54 ));
55 composed.edges.len() - 1
56 });
57 let meta = &mut composed.edges[idx].metadata;
58 if !edge.metadata.is_empty() {
59 meta.insert(namespace.clone(), Value::Object(edge.metadata.clone()));
60 }
61 add_provenance(meta, &namespace);
62 }
63 }
64
65 composed.sort_edges();
66
67 composed
68}
69
70fn namespace_of(fragment: &Graph) -> String {
73 match &fragment.label {
74 Some(label) => namespace(label),
75 None => "@graph".to_string(),
76 }
77}
78
79fn add_provenance(metadata: &mut Metadata, namespace: &str) {
82 let list = metadata
83 .entry(PROVENANCE_KEY)
84 .or_insert_with(|| Value::Array(Vec::new()));
85 if let Value::Array(entries) = list {
86 let value = Value::String(namespace.to_string());
87 if !entries.contains(&value) {
88 entries.push(value);
89 entries.sort_by(|a, b| a.as_str().cmp(&b.as_str()));
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::model::Node;
98 use serde_json::json;
99
100 fn obj(value: Value) -> Metadata {
101 value.as_object().unwrap().clone()
102 }
103
104 fn fragment_with_node(label: &str, path: &str, meta: Value) -> Graph {
105 let mut g = Graph::labeled(label);
106 g.set_node(path, Node::new(obj(meta)));
107 g
108 }
109
110 #[test]
111 fn single_graph_namespaces_metadata() {
112 let fs = fragment_with_node("fs", "a.md", json!({ "type": "file", "hash": "b3:1" }));
113 let composed = compose(&GraphSet::new(vec![fs]));
114
115 let node = &composed.nodes["a.md"];
116 assert_eq!(
117 node.metadata["@fs"],
118 json!({ "type": "file", "hash": "b3:1" })
119 );
120 assert_eq!(node.metadata[PROVENANCE_KEY], json!(["@fs"]));
121 }
122
123 #[test]
124 fn multiple_graphs_merge_by_path() {
125 let fs = fragment_with_node("fs", "a.md", json!({ "type": "file", "hash": "b3:1" }));
126 let frontmatter = fragment_with_node(
127 "frontmatter",
128 "a.md",
129 json!({ "title": "A", "status": "draft" }),
130 );
131
132 let composed = compose(&GraphSet::new(vec![fs, frontmatter]));
133 let node = &composed.nodes["a.md"];
134 assert_eq!(node.metadata["@fs"]["type"], json!("file"));
135 assert_eq!(node.metadata["@frontmatter"]["title"], json!("A"));
136 assert_eq!(
137 node.metadata[PROVENANCE_KEY],
138 json!(["@frontmatter", "@fs"])
139 );
140 }
141
142 #[test]
143 fn edges_dedup_and_collect_provenance() {
144 let mut markdown = Graph::labeled("markdown");
145 markdown.add_edge(Edge::new("a.md", "b.md"));
146 let mut frontmatter = Graph::labeled("frontmatter");
147 frontmatter.add_edge(Edge::new("a.md", "b.md"));
148
149 let composed = compose(&GraphSet::new(vec![markdown, frontmatter]));
150 assert_eq!(composed.edges.len(), 1);
151 assert_eq!(
152 composed.edges[0].metadata[PROVENANCE_KEY],
153 json!(["@frontmatter", "@markdown"])
154 );
155 }
156
157 #[test]
158 fn edge_metadata_nests_under_namespace() {
159 let mut markdown = Graph::labeled("markdown");
160 markdown.add_edge(Edge::with_metadata(
161 "a.md",
162 "b.md",
163 obj(json!({ "link": "b.md#x" })),
164 ));
165 let composed = compose(&GraphSet::new(vec![markdown]));
166 assert_eq!(
167 composed.edges[0].metadata["@markdown"]["link"],
168 json!("b.md#x")
169 );
170 }
171}