1use crate::elements::{Element, ElementKind};
12use indexmap::IndexMap;
13use std::collections::HashMap;
14
15pub struct RefResolver {
16 epoch_to_human: HashMap<String, String>,
17 human_to_epoch: HashMap<String, String>,
18}
19
20impl RefResolver {
21 pub fn new(elements: &IndexMap<String, Element>) -> Self {
23 let mut r = RefResolver {
24 epoch_to_human: HashMap::new(),
25 human_to_epoch: HashMap::new(),
26 };
27 r.build_maps(elements);
28 r
29 }
30
31 pub fn to_human(&self, epoch_ref: &str) -> Option<&str> {
33 self.epoch_to_human.get(epoch_ref).map(|s| s.as_str())
34 }
35
36 pub fn to_epoch(&self, human_ref: &str) -> Option<&str> {
38 self.human_to_epoch.get(human_ref).map(|s| s.as_str())
39 }
40
41 #[allow(dead_code)]
43 pub fn resolve<'a>(&'a self, ref_str: &'a str) -> Option<&'a str> {
44 if let Some(epoch) = self.human_to_epoch.get(ref_str) {
45 return Some(epoch.as_str());
46 }
47 if self.epoch_to_human.contains_key(ref_str) {
48 return Some(ref_str);
49 }
50 None
51 }
52
53 fn build_maps(&mut self, elements: &IndexMap<String, Element>) {
54 if elements.is_empty() {
55 return;
56 }
57
58 let mut sorted: Vec<(&String, &Element)> = elements.iter().collect();
60 sorted.sort_by(|(a, _), (b, _)| {
61 let a_parts: Vec<u64> = a.split('.').filter_map(|s| s.parse().ok()).collect();
62 let b_parts: Vec<u64> = b.split('.').filter_map(|s| s.parse().ok()).collect();
63 a_parts.cmp(&b_parts)
64 });
65
66 let root_depth = sorted
68 .first()
69 .map(|(k, _)| k.split('.').count())
70 .unwrap_or(1);
71 let mut type_counters: HashMap<String, usize> = HashMap::new();
72
73 for (epoch_ref, el) in &sorted {
74 let seg_count = epoch_ref.split('.').count();
75 let depth = seg_count - root_depth; if depth == 0 {
78 continue;
79 } let human = if depth == 1 {
82 if el.kind() == ElementKind::Unknown {
84 continue;
85 }
86 let type_name = el.sign().to_string();
87 let n = type_counters.entry(type_name.clone()).or_insert(0);
88 *n += 1;
89 format!("{}-{}", type_name, n)
90 } else {
91 let parts: Vec<&str> = epoch_ref.splitn(seg_count, '.').collect();
93 let parent_epoch = parts[..parts.len() - 1].join(".");
94 let child_idx = parts[parts.len() - 1];
95 match self.epoch_to_human.get(&parent_epoch) {
96 Some(parent_human) => format!("{}.{}", parent_human, child_idx),
97 None => continue, }
99 };
100
101 self.epoch_to_human
102 .insert(epoch_ref.to_string(), human.clone());
103 self.human_to_epoch.insert(human, epoch_ref.to_string());
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::parser;
112
113 fn parse(content: &str) -> IndexMap<String, Element> {
114 let wrapped = format!("@mps[]{{\n{}\n}}", content);
115 parser::parse_wrapped(&wrapped, 20260101).unwrap()
116 }
117
118 #[test]
119 fn test_empty_elements() {
120 let els = parse("");
121 let r = RefResolver::new(&els);
122 assert!(r.epoch_to_human.is_empty());
124 }
125
126 #[test]
127 fn test_top_level_task() {
128 let els = parse("@task[work]{ Do thing }");
129 let r = RefResolver::new(&els);
130 assert_eq!(r.to_human("20260101.1"), Some("task-1"));
131 assert_eq!(r.to_epoch("task-1"), Some("20260101.1"));
132 }
133
134 #[test]
135 fn test_sequential_types() {
136 let els = parse("@task{ A }\n@note{ B }\n@task{ C }");
137 let r = RefResolver::new(&els);
138 assert_eq!(r.to_human("20260101.1"), Some("task-1"));
139 assert_eq!(r.to_human("20260101.2"), Some("note-1"));
140 assert_eq!(r.to_human("20260101.3"), Some("task-2"));
141 }
142
143 #[test]
144 fn test_nested_inside_mps() {
145 let els = parse("@mps{\n @task{ Nested }\n @note{ also }\n}");
146 let r = RefResolver::new(&els);
147 assert_eq!(r.to_human("20260101.1"), Some("mps-1"));
149 assert_eq!(r.to_human("20260101.1.1"), Some("mps-1.1"));
151 assert_eq!(r.to_human("20260101.1.2"), Some("mps-1.2"));
152 }
153
154 #[test]
155 fn test_resolve_accepts_both_forms() {
156 let els = parse("@task{ A }");
157 let r = RefResolver::new(&els);
158 assert_eq!(r.resolve("task-1"), Some("20260101.1"));
159 assert_eq!(r.resolve("20260101.1"), Some("20260101.1"));
160 assert_eq!(r.resolve("bogus"), None);
161 }
162
163 #[test]
164 fn test_unknown_element_skipped() {
165 let els = parse("@widget{ unknown }\n@task{ real }");
166 let r = RefResolver::new(&els);
167 assert!(r.to_human("20260101.1").is_none());
169 assert_eq!(r.to_human("20260101.2"), Some("task-1"));
170 }
171
172 #[test]
173 fn test_roundtrip() {
174 let els = parse("@task[work]{ A }\n@note{ B }");
175 let r = RefResolver::new(&els);
176 for epoch_ref in r.epoch_to_human.keys() {
177 let human = r.to_human(epoch_ref).unwrap();
178 let back = r.to_epoch(human).unwrap();
179 assert_eq!(back, epoch_ref.as_str());
180 }
181 }
182}