use std::collections::HashMap;
use std::path::{Path, PathBuf};
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use crate::error::{Result, TixError};
use crate::records::{HasName, RecordStore};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TagDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
}
impl HasName for TagDefinition {
fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone)]
pub struct TagStore {
inner: RecordStore,
}
impl TagStore {
pub fn new(dir: impl Into<PathBuf>) -> Self {
Self {
inner: RecordStore::new(dir),
}
}
pub fn dir(&self) -> &Path {
self.inner.dir()
}
pub fn list(&self) -> Result<Vec<TagDefinition>> {
Ok(self
.inner
.list::<TagDefinition>()?
.into_iter()
.map(|record| record.value)
.collect())
}
pub fn colors(&self) -> Result<HashMap<String, Color>> {
let mut out = HashMap::new();
for record in self.inner.list::<TagDefinition>()? {
if let Some(color) = record
.value
.color
.as_deref()
.and_then(crate::ui::color::parse)
{
out.insert(record.name.to_ascii_lowercase(), color);
}
}
Ok(out)
}
pub fn set(&self, name: &str, color: Option<&str>) -> Result<PathBuf> {
let name = name.trim();
if name.is_empty() {
return Err(TixError::InvalidInput(
"a tag name must not be blank".into(),
));
}
let definition = TagDefinition {
name: name.to_string(),
color: color.map(str::to_string),
};
let filename = format!("{}.md", crate::slug::slugify(name));
self.inner.overwrite(&filename, &definition, None)
}
}
#[cfg(test)]
mod tests {
#[allow(clippy::wildcard_imports)]
use super::*;
use tempfile::TempDir;
#[test]
fn a_tag_round_trips_and_supplies_its_color() {
let tmp = TempDir::new().unwrap();
let store = TagStore::new(tmp.path());
store.set("Urgent", Some("red")).unwrap();
store.set("later", Some("dark_gray")).unwrap();
let list = store.list().unwrap();
assert_eq!(list.len(), 2);
let colors = store.colors().unwrap();
assert_eq!(colors.get("urgent"), Some(&Color::Red));
assert_eq!(colors.get("later"), Some(&Color::DarkGray));
}
#[test]
fn an_unparseable_color_is_skipped_not_an_error() {
let tmp = TempDir::new().unwrap();
let store = TagStore::new(tmp.path());
store.set("weird", Some("not-a-color")).unwrap();
assert_eq!(store.list().unwrap().len(), 1);
assert!(store.colors().unwrap().is_empty());
}
#[test]
fn a_missing_directory_is_an_empty_store() {
let tmp = TempDir::new().unwrap();
let store = TagStore::new(tmp.path().join("absent"));
assert_eq!(store.list().unwrap(), [] as [crate::tags::TagDefinition; 0]);
assert!(store.colors().unwrap().is_empty());
}
#[test]
fn a_blank_tag_name_is_refused() {
let tmp = TempDir::new().unwrap();
let store = TagStore::new(tmp.path());
assert!(store.set(" ", Some("red")).is_err());
}
}