data_modelling_sdk/models/
tag.rs

1//! Enhanced tag support with Simple, Pair, and List formats
2//!
3//! Tags support three formats:
4//! - Simple: "finance" (single word)
5//! - Pair: "Environment:Dev" (key:value)
6//! - List: "SecondaryDomains:[XXXXX, PPPP]" (key:[value1, value2, ...])
7
8use std::fmt;
9use std::str::FromStr;
10
11/// Tag enum supporting Simple, Pair, and List formats
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum Tag {
14    /// Simple tag: single word (e.g., "finance")
15    Simple(String),
16    /// Pair tag: key:value format (e.g., "Environment:Dev")
17    Pair(String, String),
18    /// List tag: key:[value1, value2, ...] format (e.g., "SecondaryDomains:[XXXXX, PPPP]")
19    List(String, Vec<String>),
20}
21
22impl FromStr for Tag {
23    type Err = ();
24
25    /// Parse a tag string into a Tag enum with auto-detection
26    ///
27    /// Parsing logic:
28    /// - No colon = Simple tag
29    /// - Single colon (not followed by bracket, and no more colons) = Pair tag
30    /// - Colon followed by bracket = List tag
31    /// - Multiple colons without brackets = Simple tag (malformed, graceful degradation)
32    ///
33    /// Malformed tags are treated as Simple tags (graceful degradation)
34    fn from_str(s: &str) -> Result<Self, Self::Err> {
35        let s = s.trim();
36
37        // Check for List format: "Key:[Value1, Value2, ...]"
38        if let Some(colon_pos) = s.find(':') {
39            let key = s[..colon_pos].trim().to_string();
40            let value_part = s[colon_pos + 1..].trim();
41
42            // Check if value part starts with '[' (List format)
43            if value_part.starts_with('[') && value_part.ends_with(']') {
44                // Extract values between brackets
45                let values_str = &value_part[1..value_part.len() - 1];
46                let values: Vec<String> = values_str
47                    .split(',')
48                    .map(|v| v.trim().to_string())
49                    .filter(|v| !v.is_empty())
50                    .collect();
51
52                if !key.is_empty() && !values.is_empty() {
53                    return Ok(Tag::List(key, values));
54                }
55            } else {
56                // Check if there are multiple colons (malformed - treat as Simple)
57                if value_part.contains(':') {
58                    // Multiple colons without brackets -> Simple tag
59                    return Ok(Tag::Simple(s.to_string()));
60                }
61
62                // Pair format: "Key:Value" (single colon, no brackets)
63                let value = value_part.to_string();
64                if !key.is_empty() && !value.is_empty() {
65                    return Ok(Tag::Pair(key, value));
66                }
67            }
68        }
69
70        // Simple tag: no colon, or malformed (fallback to Simple)
71        if !s.is_empty() {
72            Ok(Tag::Simple(s.to_string()))
73        } else {
74            Err(())
75        }
76    }
77}
78
79impl fmt::Display for Tag {
80    /// Serialize Tag enum to string format
81    ///
82    /// Formats:
83    /// - Simple: "finance"
84    /// - Pair: "Environment:Dev"
85    /// - List: "SecondaryDomains:[XXXXX, PPPP]"
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Tag::Simple(s) => write!(f, "{}", s),
89            Tag::Pair(key, value) => write!(f, "{}:{}", key, value),
90            Tag::List(key, values) => {
91                let values_str = values.join(", ");
92                write!(f, "{}:[{}]", key, values_str)
93            }
94        }
95    }
96}
97
98impl serde::Serialize for Tag {
99    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100    where
101        S: serde::Serializer,
102    {
103        serializer.serialize_str(&self.to_string())
104    }
105}
106
107impl<'de> serde::Deserialize<'de> for Tag {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: serde::Deserializer<'de>,
111    {
112        let s = String::deserialize(deserializer)?;
113        Tag::from_str(&s).map_err(|_| serde::de::Error::custom("Invalid tag format"))
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_simple_tag_parsing() {
123        let tag = Tag::from_str("finance").unwrap();
124        assert_eq!(tag, Tag::Simple("finance".to_string()));
125        assert_eq!(tag.to_string(), "finance");
126    }
127
128    #[test]
129    fn test_pair_tag_parsing() {
130        let tag = Tag::from_str("Environment:Dev").unwrap();
131        assert_eq!(tag, Tag::Pair("Environment".to_string(), "Dev".to_string()));
132        assert_eq!(tag.to_string(), "Environment:Dev");
133    }
134
135    #[test]
136    fn test_list_tag_parsing() {
137        let tag = Tag::from_str("SecondaryDomains:[XXXXX, PPPP]").unwrap();
138        assert_eq!(
139            tag,
140            Tag::List(
141                "SecondaryDomains".to_string(),
142                vec!["XXXXX".to_string(), "PPPP".to_string()]
143            )
144        );
145        assert_eq!(tag.to_string(), "SecondaryDomains:[XXXXX, PPPP]");
146    }
147
148    #[test]
149    fn test_list_tag_with_spaces() {
150        let tag = Tag::from_str("SecondaryDomains:[XXXXX,  PPPP  ,  QQQQ]").unwrap();
151        assert_eq!(
152            tag,
153            Tag::List(
154                "SecondaryDomains".to_string(),
155                vec!["XXXXX".to_string(), "PPPP".to_string(), "QQQQ".to_string()]
156            )
157        );
158    }
159
160    #[test]
161    fn test_malformed_tag_fallback() {
162        // Multiple colons without brackets -> Simple tag
163        let tag = Tag::from_str("Key:Value1:Value2").unwrap();
164        assert_eq!(tag, Tag::Simple("Key:Value1:Value2".to_string()));
165    }
166
167    #[test]
168    fn test_empty_tag_error() {
169        assert!(Tag::from_str("").is_err());
170        assert!(Tag::from_str("   ").is_err());
171    }
172
173    #[test]
174    fn test_tag_serialization() {
175        let simple = Tag::Simple("finance".to_string());
176        let pair = Tag::Pair("Environment".to_string(), "Dev".to_string());
177        let list = Tag::List(
178            "SecondaryDomains".to_string(),
179            vec!["XXXXX".to_string(), "PPPP".to_string()],
180        );
181
182        assert_eq!(simple.to_string(), "finance");
183        assert_eq!(pair.to_string(), "Environment:Dev");
184        assert_eq!(list.to_string(), "SecondaryDomains:[XXXXX, PPPP]");
185    }
186}