use std::collections::HashMap;
use serde::Serialize;
use super::{Adr, Status};
#[derive(Debug, Clone, Serialize)]
pub struct FacetValue {
pub value: String,
pub count: usize,
}
impl FacetValue {
#[must_use]
pub fn new(value: impl Into<String>, count: usize) -> Self {
Self {
value: value.into(),
count,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Facet {
pub name: String,
pub values: Vec<FacetValue>,
}
impl Facet {
#[must_use]
pub fn new(name: impl Into<String>, mut values: Vec<FacetValue>) -> Self {
values.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.value.cmp(&b.value)));
Self {
name: name.into(),
values,
}
}
#[must_use]
pub fn from_counts(name: impl Into<String>, counts: HashMap<String, usize>) -> Self {
let values = counts
.into_iter()
.map(|(value, count)| FacetValue::new(value, count))
.collect();
Self::new(name, values)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Facets {
pub statuses: Vec<FacetValue>,
pub categories: Vec<FacetValue>,
pub tags: Vec<FacetValue>,
pub authors: Vec<FacetValue>,
pub projects: Vec<FacetValue>,
pub technologies: Vec<FacetValue>,
}
impl Facets {
#[must_use]
pub fn from_adrs(adrs: &[Adr]) -> Self {
let mut statuses: HashMap<String, usize> = HashMap::new();
let mut categories: HashMap<String, usize> = HashMap::new();
let mut tags: HashMap<String, usize> = HashMap::new();
let mut authors: HashMap<String, usize> = HashMap::new();
let mut projects: HashMap<String, usize> = HashMap::new();
let mut technologies: HashMap<String, usize> = HashMap::new();
for status in Status::all() {
statuses.insert(status.as_str().to_string(), 0);
}
for adr in adrs {
*statuses
.entry(adr.status().as_str().to_string())
.or_insert(0) += 1;
if !adr.category().is_empty() {
*categories.entry(adr.category().to_string()).or_insert(0) += 1;
}
for tag in adr.tags() {
*tags.entry(tag.clone()).or_insert(0) += 1;
}
if !adr.author().is_empty() {
*authors.entry(adr.author().to_string()).or_insert(0) += 1;
}
if !adr.project().is_empty() {
*projects.entry(adr.project().to_string()).or_insert(0) += 1;
}
for tech in adr.technologies() {
*technologies.entry(tech.clone()).or_insert(0) += 1;
}
}
Self {
statuses: sorted_facet_values(statuses),
categories: sorted_facet_values(categories),
tags: sorted_facet_values(tags),
authors: sorted_facet_values(authors),
projects: sorted_facet_values(projects),
technologies: sorted_facet_values(technologies),
}
}
}
fn sorted_facet_values(counts: HashMap<String, usize>) -> Vec<FacetValue> {
let mut values: Vec<_> = counts
.into_iter()
.map(|(value, count)| FacetValue::new(value, count))
.collect();
values.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.value.cmp(&b.value)));
values
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_facet_value_creation() {
let fv = FacetValue::new("accepted", 10);
assert_eq!(fv.value, "accepted");
assert_eq!(fv.count, 10);
}
#[test]
fn test_facet_sorting() {
let values = vec![
FacetValue::new("a", 1),
FacetValue::new("b", 5),
FacetValue::new("c", 3),
];
let facet = Facet::new("test", values);
assert_eq!(facet.values[0].value, "b"); assert_eq!(facet.values[1].value, "c"); assert_eq!(facet.values[2].value, "a"); }
#[test]
fn test_facet_from_counts() {
let mut counts = HashMap::new();
counts.insert("proposed".to_string(), 5);
counts.insert("accepted".to_string(), 10);
let facet = Facet::from_counts("status", counts);
assert_eq!(facet.name, "status");
assert_eq!(facet.values[0].value, "accepted");
assert_eq!(facet.values[0].count, 10);
}
#[test]
fn test_sorted_facet_values_alphabetical_tie() {
let mut counts = HashMap::new();
counts.insert("zebra".to_string(), 5);
counts.insert("apple".to_string(), 5);
let values = sorted_facet_values(counts);
assert_eq!(values[0].value, "apple");
assert_eq!(values[1].value, "zebra");
}
#[test]
#[allow(clippy::too_many_lines)]
fn test_facets_from_adrs_with_all_fields() {
use crate::domain::{Adr, AdrId, Frontmatter, Status};
use std::path::PathBuf;
let frontmatter1 = Frontmatter::new("ADR 1")
.with_status(Status::Accepted)
.with_category("architecture")
.with_author("Alice")
.with_project("project-alpha")
.with_tags(vec!["database".to_string(), "performance".to_string()])
.with_technologies(vec!["rust".to_string(), "postgres".to_string()]);
let frontmatter2 = Frontmatter::new("ADR 2")
.with_status(Status::Proposed)
.with_category("api")
.with_author("Bob")
.with_project("project-beta")
.with_tags(vec!["rest".to_string(), "database".to_string()])
.with_technologies(vec!["rust".to_string(), "redis".to_string()]);
let adr1 = Adr::new(
AdrId::new("adr_0001"),
"adr_0001.md".to_string(),
PathBuf::from("adr_0001.md"),
frontmatter1,
String::new(),
String::new(),
String::new(),
);
let adr2 = Adr::new(
AdrId::new("adr_0002"),
"adr_0002.md".to_string(),
PathBuf::from("adr_0002.md"),
frontmatter2,
String::new(),
String::new(),
String::new(),
);
let facets = Facets::from_adrs(&[adr1, adr2]);
assert!(
facets
.statuses
.iter()
.any(|f| f.value == "accepted" && f.count == 1)
);
assert!(
facets
.statuses
.iter()
.any(|f| f.value == "proposed" && f.count == 1)
);
assert!(
facets
.categories
.iter()
.any(|f| f.value == "architecture" && f.count == 1)
);
assert!(
facets
.categories
.iter()
.any(|f| f.value == "api" && f.count == 1)
);
assert!(
facets
.authors
.iter()
.any(|f| f.value == "Alice" && f.count == 1)
);
assert!(
facets
.authors
.iter()
.any(|f| f.value == "Bob" && f.count == 1)
);
assert!(
facets
.projects
.iter()
.any(|f| f.value == "project-alpha" && f.count == 1)
);
assert!(
facets
.projects
.iter()
.any(|f| f.value == "project-beta" && f.count == 1)
);
assert!(
facets
.tags
.iter()
.any(|f| f.value == "database" && f.count == 2)
);
assert!(
facets
.technologies
.iter()
.any(|f| f.value == "rust" && f.count == 2)
);
assert!(
facets
.technologies
.iter()
.any(|f| f.value == "postgres" && f.count == 1)
);
assert!(
facets
.technologies
.iter()
.any(|f| f.value == "redis" && f.count == 1)
);
}
}