use fehler::throws;
use serde_derive::Deserialize;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
#[derive(Debug, Deserialize)]
pub struct SkillTree {
pub group: Vec<Group>,
pub cluster: Option<Vec<Cluster>>,
pub graphviz: Option<Graphviz>,
pub doc: Option<Doc>,
}
#[derive(Debug, Deserialize)]
pub struct Graphviz {
pub rankdir: Option<String>,
}
#[derive(Default, Debug, Deserialize)]
pub struct Doc {
pub columns: Vec<String>,
pub defaults: Option<HashMap<String, String>>,
pub emoji: Option<HashMap<String, EmojiMap>>,
pub include: Option<Vec<PathBuf>>,
}
pub type EmojiMap = HashMap<String, String>;
#[derive(Debug, Deserialize)]
pub struct Cluster {
pub name: String,
pub label: String,
pub color: Option<String>,
pub style: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Group {
pub name: String,
pub cluster: Option<String>,
pub label: Option<String>,
pub requires: Option<Vec<String>>,
pub description: Option<Vec<String>>,
pub items: Vec<Item>,
pub width: Option<f64>,
pub status: Option<Status>,
pub href: Option<String>,
pub header_color: Option<String>,
pub description_color: Option<String>,
}
#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct GroupIndex(pub usize);
pub type Item = HashMap<String, String>;
#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct ItemIndex(pub usize);
#[derive(Copy, Clone, Debug, Deserialize)]
pub enum Status {
Blocked,
Unassigned,
Assigned,
Complete,
}
impl SkillTree {
pub fn load(path: &Path) -> anyhow::Result<SkillTree> {
let skill_tree_text = std::fs::read_to_string(path)?;
let mut tree = Self::parse(&skill_tree_text)?;
tree.import(path)?;
Ok(tree)
}
fn import(&mut self, root_path: &Path) -> anyhow::Result<()> {
if let Some(doc) = &mut self.doc {
if let Some(include) = &mut doc.include {
let include = include.clone();
for include_path in include {
let tree_path = root_path.parent().unwrap().join(&include_path);
let mut toml: SkillTree = SkillTree::load(&tree_path)?;
let self_doc = self.doc.get_or_insert(Doc::default());
let toml_doc = toml.doc.get_or_insert(Doc::default());
for column in &toml_doc.columns {
let columns = &mut self_doc.columns;
if !columns.contains(column) {
columns.push(column.clone());
if let Some(value) =
toml_doc.emoji.get_or_insert(HashMap::default()).get(column)
{
self_doc
.emoji
.get_or_insert(HashMap::default())
.insert(column.clone(), value.clone());
}
if let Some(value) = toml_doc
.defaults
.get_or_insert(HashMap::default())
.get(column)
{
self_doc
.defaults
.get_or_insert(HashMap::default())
.insert(column.clone(), value.clone());
}
}
}
self.group.extend(toml.group.into_iter());
self.cluster
.get_or_insert(vec![])
.extend(toml.cluster.into_iter().flatten());
}
}
}
Ok(())
}
#[throws(anyhow::Error)]
pub fn parse(text: &str) -> SkillTree {
toml::from_str(text)?
}
#[throws(anyhow::Error)]
pub fn validate(&self) {
for group in &self.group {
group.validate(self)?;
}
}
pub fn groups(&self) -> impl Iterator<Item = &Group> {
self.group.iter()
}
pub fn group_named(&self, name: &str) -> Option<&Group> {
self.group.iter().find(|g| g.name == name)
}
pub fn columns(&self) -> &[String] {
if let Some(doc) = &self.doc {
&doc.columns
} else {
&[]
}
}
pub fn emoji<'me>(&'me self, column: &str, input: &'me str) -> &'me str {
if let Some(doc) = &self.doc {
if let Some(emoji_maps) = &doc.emoji {
if let Some(emoji_map) = emoji_maps.get(column) {
if let Some(output) = emoji_map.get(input) {
return output;
}
}
}
}
input
}
}
impl Group {
#[throws(anyhow::Error)]
pub fn validate(&self, tree: &SkillTree) {
for group_name in self.requires.iter().flatten() {
if tree.group_named(group_name).is_none() {
anyhow::bail!(
"the group `{}` has a dependency on a group `{}` that does not exist",
self.name,
group_name,
)
}
}
for item in &self.items {
item.validate()?;
}
}
pub fn items(&self) -> impl Iterator<Item = &Item> {
self.items.iter()
}
}
pub trait ItemExt {
fn href(&self) -> Option<&String>;
fn label(&self) -> &String;
fn column_value<'me>(&'me self, tree: &'me SkillTree, c: &str) -> &'me str;
#[allow(redundant_semicolons)] #[throws(anyhow::Error)]
fn validate(&self);
}
impl ItemExt for Item {
fn href(&self) -> Option<&String> {
self.get("href")
}
fn label(&self) -> &String {
self.get("label").unwrap()
}
fn column_value<'me>(&'me self, tree: &'me SkillTree, c: &str) -> &'me str {
if let Some(v) = self.get(c) {
return v;
}
if let Some(doc) = &tree.doc {
if let Some(defaults) = &doc.defaults {
if let Some(default_value) = defaults.get(c) {
return default_value;
}
}
}
""
}
#[throws(anyhow::Error)]
fn validate(&self) {
}
}