1use fehler::throws;
2use serde_derive::Deserialize;
3use std::{
4 collections::HashMap,
5 path::{Path, PathBuf},
6};
7
8#[derive(Debug, Deserialize)]
9pub struct SkillTree {
10 pub group: Vec<Group>,
11 pub cluster: Option<Vec<Cluster>>,
12 pub graphviz: Option<Graphviz>,
13 pub doc: Option<Doc>,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct Graphviz {
18 pub rankdir: Option<String>,
19}
20
21#[derive(Default, Debug, Deserialize)]
22pub struct Doc {
23 pub columns: Vec<String>,
24 pub defaults: Option<HashMap<String, String>>,
25 pub emoji: Option<HashMap<String, EmojiMap>>,
26 pub include: Option<Vec<PathBuf>>,
27}
28
29pub type EmojiMap = HashMap<String, String>;
30
31#[derive(Debug, Deserialize)]
32pub struct Cluster {
33 pub name: String,
34 pub label: String,
35 pub color: Option<String>,
36 pub style: Option<String>,
37}
38
39#[derive(Debug, Deserialize)]
40pub struct Group {
41 pub name: String,
42 pub cluster: Option<String>,
43 pub label: Option<String>,
44 pub requires: Option<Vec<String>>,
45 pub description: Option<Vec<String>>,
46 pub items: Vec<Item>,
47 pub width: Option<f64>,
48 pub status: Option<Status>,
49 pub href: Option<String>,
50 pub header_color: Option<String>,
51 pub description_color: Option<String>,
52}
53
54#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
55pub struct GroupIndex(pub usize);
56
57pub type Item = HashMap<String, String>;
58
59#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
60pub struct ItemIndex(pub usize);
61
62#[derive(Copy, Clone, Debug, Deserialize)]
63pub enum Status {
64 Blocked,
66
67 Unassigned,
69
70 Assigned,
72
73 Complete,
75}
76
77impl SkillTree {
78 pub fn load(path: &Path) -> anyhow::Result<SkillTree> {
79 let skill_tree_text = std::fs::read_to_string(path)?;
80 let mut tree = Self::parse(&skill_tree_text)?;
81 tree.import(path)?;
82 Ok(tree)
83 }
84
85 fn import(&mut self, root_path: &Path) -> anyhow::Result<()> {
86 if let Some(doc) = &mut self.doc {
87 if let Some(include) = &mut doc.include {
88 let include = include.clone();
89 for include_path in include {
90 let tree_path = root_path.parent().unwrap().join(&include_path);
91 let mut toml: SkillTree = SkillTree::load(&tree_path)?;
92
93 let self_doc = self.doc.get_or_insert(Doc::default());
95 let toml_doc = toml.doc.get_or_insert(Doc::default());
96 for column in &toml_doc.columns {
97 let columns = &mut self_doc.columns;
98 if !columns.contains(column) {
99 columns.push(column.clone());
100
101 if let Some(value) =
102 toml_doc.emoji.get_or_insert(HashMap::default()).get(column)
103 {
104 self_doc
105 .emoji
106 .get_or_insert(HashMap::default())
107 .insert(column.clone(), value.clone());
108 }
109
110 if let Some(value) = toml_doc
111 .defaults
112 .get_or_insert(HashMap::default())
113 .get(column)
114 {
115 self_doc
116 .defaults
117 .get_or_insert(HashMap::default())
118 .insert(column.clone(), value.clone());
119 }
120 }
121 }
122
123 self.group.extend(toml.group.into_iter());
124
125 self.cluster
126 .get_or_insert(vec![])
127 .extend(toml.cluster.into_iter().flatten());
128 }
129 }
130 }
131 Ok(())
132 }
133
134 #[throws(anyhow::Error)]
135 pub fn parse(text: &str) -> SkillTree {
136 toml::from_str(text)?
137 }
138
139 #[throws(anyhow::Error)]
140 pub fn validate(&self) {
141 for group in &self.group {
144 group.validate(self)?;
145 }
146 }
147
148 pub fn groups(&self) -> impl Iterator<Item = &Group> {
149 self.group.iter()
150 }
151
152 pub fn group_named(&self, name: &str) -> Option<&Group> {
153 self.group.iter().find(|g| g.name == name)
154 }
155
156 pub fn columns(&self) -> &[String] {
158 if let Some(doc) = &self.doc {
159 &doc.columns
160 } else {
161 &[]
162 }
163 }
164
165 pub fn emoji<'me>(&'me self, column: &str, input: &'me str) -> &'me str {
167 if let Some(doc) = &self.doc {
168 if let Some(emoji_maps) = &doc.emoji {
169 if let Some(emoji_map) = emoji_maps.get(column) {
170 if let Some(output) = emoji_map.get(input) {
171 return output;
172 }
173 }
174 }
175 }
176 input
177 }
178}
179
180impl Group {
181 #[throws(anyhow::Error)]
182 pub fn validate(&self, tree: &SkillTree) {
183 for group_name in self.requires.iter().flatten() {
190 if tree.group_named(group_name).is_none() {
191 anyhow::bail!(
192 "the group `{}` has a dependency on a group `{}` that does not exist",
193 self.name,
194 group_name,
195 )
196 }
197 }
198
199 for item in &self.items {
200 item.validate()?;
201 }
202 }
203
204 pub fn items(&self) -> impl Iterator<Item = &Item> {
205 self.items.iter()
206 }
207}
208
209pub trait ItemExt {
210 fn href(&self) -> Option<&String>;
211 fn label(&self) -> &String;
212 fn column_value<'me>(&'me self, tree: &'me SkillTree, c: &str) -> &'me str;
213
214 #[allow(redundant_semicolons)] #[throws(anyhow::Error)]
216 fn validate(&self);
217}
218
219impl ItemExt for Item {
220 fn href(&self) -> Option<&String> {
221 self.get("href")
222 }
223
224 fn label(&self) -> &String {
225 self.get("label").unwrap()
226 }
227
228 fn column_value<'me>(&'me self, tree: &'me SkillTree, c: &str) -> &'me str {
229 if let Some(v) = self.get(c) {
230 return v;
231 }
232
233 if let Some(doc) = &tree.doc {
234 if let Some(defaults) = &doc.defaults {
235 if let Some(default_value) = defaults.get(c) {
236 return default_value;
237 }
238 }
239 }
240
241 ""
242 }
243
244 #[throws(anyhow::Error)]
245 fn validate(&self) {
246 }
252}