use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Pillar {
Reality,
Contracts,
DevX,
Cloud,
Ai,
}
impl Pillar {
pub fn as_str(&self) -> &'static str {
match self {
Pillar::Reality => "reality",
Pillar::Contracts => "contracts",
Pillar::DevX => "devx",
Pillar::Cloud => "cloud",
Pillar::Ai => "ai",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"reality" => Some(Pillar::Reality),
"contracts" => Some(Pillar::Contracts),
"devx" => Some(Pillar::DevX),
"cloud" => Some(Pillar::Cloud),
"ai" => Some(Pillar::Ai),
_ => None,
}
}
pub fn display_name(&self) -> String {
match self {
Pillar::Reality => "[Reality]".to_string(),
Pillar::Contracts => "[Contracts]".to_string(),
Pillar::DevX => "[DevX]".to_string(),
Pillar::Cloud => "[Cloud]".to_string(),
Pillar::Ai => "[AI]".to_string(),
}
}
pub fn all() -> Vec<Pillar> {
vec![
Pillar::Reality,
Pillar::Contracts,
Pillar::DevX,
Pillar::Cloud,
Pillar::Ai,
]
}
}
impl fmt::Display for Pillar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PillarMetadata {
pillars: HashSet<Pillar>,
}
impl PillarMetadata {
pub fn new() -> Self {
Self {
pillars: HashSet::new(),
}
}
pub fn with_pillar(mut self, pillar: Pillar) -> Self {
self.pillars.insert(pillar);
self
}
pub fn with_pillars(mut self, pillars: &[Pillar]) -> Self {
for pillar in pillars {
self.pillars.insert(*pillar);
}
self
}
pub fn add_pillar(&mut self, pillar: Pillar) {
self.pillars.insert(pillar);
}
pub fn has_pillar(&self, pillar: Pillar) -> bool {
self.pillars.contains(&pillar)
}
pub fn pillars(&self) -> Vec<Pillar> {
let mut result: Vec<Pillar> = self.pillars.iter().copied().collect();
result.sort_by_key(|p| p.as_str());
result
}
pub fn is_empty(&self) -> bool {
self.pillars.is_empty()
}
pub fn to_changelog_tags(&self) -> String {
let mut pillars: Vec<Pillar> = self.pillars.iter().copied().collect();
pillars.sort_by_key(|p| p.as_str());
pillars.iter().map(|p| p.display_name()).collect::<Vec<_>>().join("")
}
pub fn from_doc_comment(doc: &str) -> Option<Self> {
let re = regex::Regex::new(r"(?i)(?:pillars?):\s*(\[[^\]]+\])+").ok()?;
let caps = re.captures(doc)?;
let full_match = caps.get(0)?;
let tag_re = regex::Regex::new(r"\[([^\]]+)\]").ok()?;
let mut metadata = Self::new();
for cap in tag_re.captures_iter(full_match.as_str()) {
if let Some(pillar_name) = cap.get(1) {
if let Some(pillar) = Pillar::from_str(pillar_name.as_str()) {
metadata.add_pillar(pillar);
}
}
}
if metadata.is_empty() {
None
} else {
Some(metadata)
}
}
}
impl Default for PillarMetadata {
fn default() -> Self {
Self::new()
}
}
pub fn parse_pillar_tags_from_scenario_tags(tags: &[String]) -> Vec<Pillar> {
let mut pillars = Vec::new();
let tag_re = regex::Regex::new(r"\[([^\]]+)\]").ok();
if tag_re.is_none() {
return pillars;
}
let tag_re = tag_re.unwrap();
for tag in tags {
for cap in tag_re.captures_iter(tag) {
if let Some(pillar_name) = cap.get(1) {
if let Some(pillar) = Pillar::from_str(pillar_name.as_str()) {
if !pillars.contains(&pillar) {
pillars.push(pillar);
}
}
}
}
}
pillars
}
pub fn has_pillar_tags(tag: &str) -> bool {
let tag_re = regex::Regex::new(r"\[([^\]]+)\]").ok();
if let Some(tag_re) = tag_re {
for cap in tag_re.captures_iter(tag) {
if let Some(pillar_name) = cap.get(1) {
if Pillar::from_str(pillar_name.as_str()).is_some() {
return true;
}
}
}
}
false
}
pub fn pillar_metadata_from_scenario_tags(tags: &[String]) -> PillarMetadata {
let pillars = parse_pillar_tags_from_scenario_tags(tags);
PillarMetadata::from(pillars)
}
impl From<Vec<Pillar>> for PillarMetadata {
fn from(pillars: Vec<Pillar>) -> Self {
Self {
pillars: pillars.into_iter().collect(),
}
}
}
impl From<&[Pillar]> for PillarMetadata {
fn from(pillars: &[Pillar]) -> Self {
Self {
pillars: pillars.iter().copied().collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pillar_as_str() {
assert_eq!(Pillar::Reality.as_str(), "reality");
assert_eq!(Pillar::Contracts.as_str(), "contracts");
assert_eq!(Pillar::DevX.as_str(), "devx");
assert_eq!(Pillar::Cloud.as_str(), "cloud");
assert_eq!(Pillar::Ai.as_str(), "ai");
}
#[test]
fn test_pillar_from_str() {
assert_eq!(Pillar::from_str("reality"), Some(Pillar::Reality));
assert_eq!(Pillar::from_str("REALITY"), Some(Pillar::Reality));
assert_eq!(Pillar::from_str("Contracts"), Some(Pillar::Contracts));
assert_eq!(Pillar::from_str("devx"), Some(Pillar::DevX));
assert_eq!(Pillar::from_str("invalid"), None);
}
#[test]
fn test_pillar_display_name() {
assert_eq!(Pillar::Reality.display_name(), "[Reality]");
assert_eq!(Pillar::Contracts.display_name(), "[Contracts]");
assert_eq!(Pillar::DevX.display_name(), "[DevX]");
assert_eq!(Pillar::Cloud.display_name(), "[Cloud]");
assert_eq!(Pillar::Ai.display_name(), "[AI]");
}
#[test]
fn test_pillar_metadata() {
let mut metadata = PillarMetadata::new();
assert!(metadata.is_empty());
metadata.add_pillar(Pillar::Reality);
assert!(!metadata.is_empty());
assert!(metadata.has_pillar(Pillar::Reality));
assert!(!metadata.has_pillar(Pillar::Contracts));
metadata.add_pillar(Pillar::Ai);
assert!(metadata.has_pillar(Pillar::Reality));
assert!(metadata.has_pillar(Pillar::Ai));
assert_eq!(metadata.pillars().len(), 2);
}
#[test]
fn test_pillar_metadata_builder() {
let metadata = PillarMetadata::new().with_pillar(Pillar::Reality).with_pillar(Pillar::Ai);
assert!(metadata.has_pillar(Pillar::Reality));
assert!(metadata.has_pillar(Pillar::Ai));
assert_eq!(metadata.pillars().len(), 2);
}
#[test]
fn test_pillar_metadata_changelog_tags() {
let metadata = PillarMetadata::from(vec![Pillar::Reality, Pillar::Ai]);
let tags = metadata.to_changelog_tags();
assert!(tags.contains("[AI]"));
assert!(tags.contains("[Reality]"));
}
#[test]
fn test_pillar_metadata_from_doc_comment() {
let doc = "//! Pillars: [Reality][AI]\n//! This module does something";
let metadata = PillarMetadata::from_doc_comment(doc).unwrap();
assert!(metadata.has_pillar(Pillar::Reality));
assert!(metadata.has_pillar(Pillar::Ai));
let doc2 = "/// Pillar: [Contracts]";
let metadata2 = PillarMetadata::from_doc_comment(doc2).unwrap();
assert!(metadata2.has_pillar(Pillar::Contracts));
let doc3 = "//! No pillars here";
assert!(PillarMetadata::from_doc_comment(doc3).is_none());
}
#[test]
fn test_parse_pillar_tags_from_scenario_tags() {
use super::{parse_pillar_tags_from_scenario_tags, Pillar};
let tags = vec![
"[Cloud]".to_string(),
"auth".to_string(),
"[Contracts][Reality]".to_string(),
];
let pillars = parse_pillar_tags_from_scenario_tags(&tags);
assert!(pillars.contains(&Pillar::Cloud));
assert!(pillars.contains(&Pillar::Contracts));
assert!(pillars.contains(&Pillar::Reality));
assert_eq!(pillars.len(), 3);
let tags2 = vec!["normal".to_string(), "test".to_string()];
let pillars2 = parse_pillar_tags_from_scenario_tags(&tags2);
assert!(pillars2.is_empty());
let tags3 = vec!["[Cloud][Contracts][Reality]".to_string()];
let pillars3 = parse_pillar_tags_from_scenario_tags(&tags3);
assert_eq!(pillars3.len(), 3);
assert!(pillars3.contains(&Pillar::Cloud));
assert!(pillars3.contains(&Pillar::Contracts));
assert!(pillars3.contains(&Pillar::Reality));
}
#[test]
fn test_has_pillar_tags() {
use super::has_pillar_tags;
assert!(has_pillar_tags("[Cloud]"));
assert!(has_pillar_tags("[Contracts][Reality]"));
assert!(has_pillar_tags("auth-[Cloud]-test"));
assert!(!has_pillar_tags("auth"));
assert!(!has_pillar_tags("[Invalid]"));
assert!(!has_pillar_tags(""));
}
#[test]
fn test_pillar_metadata_from_scenario_tags() {
use super::{pillar_metadata_from_scenario_tags, Pillar};
let tags = vec![
"[Cloud]".to_string(),
"[Contracts]".to_string(),
"auth".to_string(),
];
let metadata = pillar_metadata_from_scenario_tags(&tags);
assert!(metadata.has_pillar(Pillar::Cloud));
assert!(metadata.has_pillar(Pillar::Contracts));
assert!(!metadata.has_pillar(Pillar::Reality));
let tags2 = vec!["[Cloud][Contracts][Reality]".to_string()];
let metadata2 = pillar_metadata_from_scenario_tags(&tags2);
assert!(metadata2.has_pillar(Pillar::Cloud));
assert!(metadata2.has_pillar(Pillar::Contracts));
assert!(metadata2.has_pillar(Pillar::Reality));
}
}