1use std::collections::{HashMap, HashSet};
2
3pub trait Scoreable: Send + Sync {
13 fn id(&self) -> &str;
14
15 fn score_proxy(&self) -> f32;
18
19 fn category(&self) -> &str;
22
23 fn prerequisites(&self) -> &[String] {
26 &[]
27 }
28
29 fn metadata(&self) -> &HashMap<String, String>;
31}
32
33#[derive(Debug, Clone)]
36pub struct Item {
37 pub id: String,
38 pub score_proxy: f32,
39 pub category: String,
40 pub prerequisites: Vec<String>,
41 pub metadata: HashMap<String, String>,
42}
43
44impl Item {
45 pub fn new(id: impl Into<String>, score_proxy: f32, category: impl Into<String>) -> Self {
46 Self {
47 id: id.into(),
48 score_proxy: score_proxy.clamp(0.0, 1.0),
49 category: category.into(),
50 prerequisites: Vec::new(),
51 metadata: HashMap::new(),
52 }
53 }
54
55 pub fn with_prereqs(mut self, prereqs: Vec<String>) -> Self {
56 self.prerequisites = prereqs;
57 self
58 }
59
60 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
61 self.metadata = metadata;
62 self
63 }
64}
65
66impl Scoreable for Item {
67 fn id(&self) -> &str {
68 &self.id
69 }
70
71 fn score_proxy(&self) -> f32 {
72 self.score_proxy
73 }
74
75 fn category(&self) -> &str {
76 &self.category
77 }
78
79 fn prerequisites(&self) -> &[String] {
80 &self.prerequisites
81 }
82
83 fn metadata(&self) -> &HashMap<String, String> {
84 &self.metadata
85 }
86}
87
88pub struct ItemRegistry {
90 items: HashMap<String, Item>,
91}
92
93impl ItemRegistry {
94 pub fn new() -> Self {
95 Self {
96 items: HashMap::new(),
97 }
98 }
99
100 pub fn register(&mut self, items: Vec<Item>) -> Result<(), crate::error::AriaError> {
102 for item in items {
103 self.items.insert(item.id.clone(), item);
104 }
105 self.validate_prereq_graph()
106 }
107
108 pub fn eligible<'a>(&'a self, resolved_set: &HashSet<String>) -> Vec<&'a Item> {
110 self.items
111 .values()
112 .filter(|item| {
113 item.prerequisites
114 .iter()
115 .all(|prereq| resolved_set.contains(prereq))
116 })
117 .collect()
118 }
119
120 pub fn get(&self, id: &str) -> Option<&Item> {
121 self.items.get(id)
122 }
123
124 pub fn len(&self) -> usize {
125 self.items.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
129 self.items.is_empty()
130 }
131
132 fn validate_prereq_graph(&self) -> Result<(), crate::error::AriaError> {
134 let mut visited: HashSet<&str> = HashSet::new();
135 let mut in_stack: HashSet<&str> = HashSet::new();
136
137 for id in self.items.keys() {
138 if !visited.contains(id.as_str()) {
139 self.dfs(id, &mut visited, &mut in_stack)?;
140 }
141 }
142 Ok(())
143 }
144
145 fn dfs<'a>(
146 &'a self,
147 id: &'a str,
148 visited: &mut HashSet<&'a str>,
149 in_stack: &mut HashSet<&'a str>,
150 ) -> Result<(), crate::error::AriaError> {
151 visited.insert(id);
152 in_stack.insert(id);
153
154 if let Some(item) = self.items.get(id) {
155 for prereq in &item.prerequisites {
156 if !visited.contains(prereq.as_str()) {
157 self.dfs(prereq, visited, in_stack)?;
158 } else if in_stack.contains(prereq.as_str()) {
159 return Err(crate::error::AriaError::CyclicPrerequisite(prereq.clone()));
160 }
161 }
162 }
163
164 in_stack.remove(id);
165 Ok(())
166 }
167}
168
169impl Default for ItemRegistry {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn cycle_detection() {
181 let mut registry = ItemRegistry::new();
182 let items = vec![
183 Item::new("a", 0.3, "cat").with_prereqs(vec!["b".into()]),
184 Item::new("b", 0.5, "cat").with_prereqs(vec!["a".into()]),
185 ];
186 assert!(registry.register(items).is_err());
187 }
188
189 #[test]
190 fn eligible_filters_unsatisfied_prereqs() {
191 let mut registry = ItemRegistry::new();
192 registry
193 .register(vec![
194 Item::new("a", 0.3, "cat"),
195 Item::new("b", 0.5, "cat").with_prereqs(vec!["a".into()]),
196 ])
197 .unwrap();
198
199 let resolved: HashSet<String> = HashSet::new();
200 let eligible = registry.eligible(&resolved);
201 assert_eq!(eligible.len(), 1);
202 assert_eq!(eligible[0].id(), "a");
203 }
204
205 #[test]
206 fn eligible_after_prereq_resolved() {
207 let mut registry = ItemRegistry::new();
208 registry
209 .register(vec![
210 Item::new("a", 0.3, "cat"),
211 Item::new("b", 0.5, "cat").with_prereqs(vec!["a".into()]),
212 ])
213 .unwrap();
214
215 let mut resolved: HashSet<String> = HashSet::new();
216 resolved.insert("a".into());
217 let eligible = registry.eligible(&resolved);
218 assert_eq!(eligible.len(), 2);
219 }
220}