hwpforge_foundation/
ids.rs

1//! String-based identifiers for fonts, templates, and styles.
2//!
3//! Each identifier is a validated, non-empty string wrapped in a distinct
4//! newtype for compile-time safety. You cannot accidentally pass a
5//! [`FontId`] where a [`StyleName`] is expected.
6//!
7//! # Phase 1 Migration
8//!
9//! The internal `String` will migrate to an interned representation
10//! (e.g. `lasso::Spur` or `string_cache::Atom`) for O(1) equality
11//! and memory deduplication. The public API (`new`, `as_str`) stays
12//! identical.
13//!
14//! # Examples
15//!
16//! ```
17//! use hwpforge_foundation::FontId;
18//!
19//! let font = FontId::new("Batang").unwrap();
20//! assert_eq!(font.as_str(), "Batang");
21//! assert_eq!(font.to_string(), "Batang");
22//!
23//! // Empty string is rejected
24//! assert!(FontId::new("").is_err());
25//! ```
26
27use crate::macros::string_newtype;
28
29string_newtype! {
30    /// A font identifier (e.g. `"Batang"`, `"Dotum"`, `"Arial"`).
31    ///
32    /// # Examples
33    ///
34    /// ```
35    /// use hwpforge_foundation::FontId;
36    ///
37    /// let f = FontId::new("한컴바탕").unwrap();
38    /// assert_eq!(f.as_str(), "한컴바탕");
39    /// ```
40    FontId, "FontId"
41}
42
43string_newtype! {
44    /// A template name (e.g. `"gov_proposal"`, `"letter"`).
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use hwpforge_foundation::TemplateName;
50    ///
51    /// let t = TemplateName::new("gov_proposal").unwrap();
52    /// assert_eq!(t.as_str(), "gov_proposal");
53    /// ```
54    TemplateName, "TemplateName"
55}
56
57string_newtype! {
58    /// A style name (e.g. `"heading1"`, `"본문"`, `"normal"`).
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use hwpforge_foundation::StyleName;
64    ///
65    /// let s = StyleName::new("heading1").unwrap();
66    /// assert_eq!(s.as_str(), "heading1");
67    /// ```
68    StyleName, "StyleName"
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::FoundationError;
75
76    // ===================================================================
77    // FontId edge cases (10+)
78    // ===================================================================
79
80    // Edge Case 1: Empty string -> error
81    #[test]
82    fn fontid_empty_is_error() {
83        let err = FontId::new("").unwrap_err();
84        match err {
85            FoundationError::EmptyIdentifier { ref item } => {
86                assert_eq!(item, "FontId");
87            }
88            other => panic!("unexpected error: {other}"),
89        }
90    }
91
92    // Edge Case 2: Single character -> OK
93    #[test]
94    fn fontid_single_char() {
95        let f = FontId::new("A").unwrap();
96        assert_eq!(f.as_str(), "A");
97    }
98
99    // Edge Case 3: Korean characters
100    #[test]
101    fn fontid_korean() {
102        let f = FontId::new("한컴바탕").unwrap();
103        assert_eq!(f.as_str(), "한컴바탕");
104    }
105
106    // Edge Case 4: Special characters (hyphen, underscore)
107    #[test]
108    fn fontid_special_chars() {
109        let f = FontId::new("D2Coding-Bold_Italic").unwrap();
110        assert_eq!(f.as_str(), "D2Coding-Bold_Italic");
111    }
112
113    // Edge Case 5: Unicode emoji
114    #[test]
115    fn fontid_unicode_emoji() {
116        let f = FontId::new("Font\u{1F600}").unwrap();
117        assert!(f.as_str().contains('\u{1F600}'));
118    }
119
120    // Edge Case 6: Very long name
121    #[test]
122    fn fontid_long_name() {
123        let long = "x".repeat(10_000);
124        let f = FontId::new(long.clone()).unwrap();
125        assert_eq!(f.as_str().len(), 10_000);
126    }
127
128    // Edge Case 7: Equality
129    #[test]
130    fn fontid_equality() {
131        let a = FontId::new("Arial").unwrap();
132        let b = FontId::new("Arial").unwrap();
133        let c = FontId::new("Batang").unwrap();
134        assert_eq!(a, b);
135        assert_ne!(a, c);
136    }
137
138    // Edge Case 8: Hash
139    #[test]
140    fn fontid_hash_in_map() {
141        use std::collections::HashMap;
142        let mut map = HashMap::new();
143        let key = FontId::new("Batang").unwrap();
144        map.insert(key.clone(), 42);
145        assert_eq!(map[&key], 42);
146    }
147
148    // Edge Case 9: Display
149    #[test]
150    fn fontid_display() {
151        let f = FontId::new("Arial").unwrap();
152        assert_eq!(f.to_string(), "Arial");
153    }
154
155    // Edge Case 10: Serde roundtrip
156    #[test]
157    fn fontid_serde_roundtrip() {
158        let f = FontId::new("Batang").unwrap();
159        let json = serde_json::to_string(&f).unwrap();
160        assert_eq!(json, "\"Batang\"");
161        let back: FontId = serde_json::from_str(&json).unwrap();
162        assert_eq!(back, f);
163    }
164
165    // ===================================================================
166    // TemplateName
167    // ===================================================================
168
169    #[test]
170    fn template_name_empty_is_error() {
171        assert!(TemplateName::new("").is_err());
172    }
173
174    #[test]
175    fn template_name_valid() {
176        let t = TemplateName::new("gov_proposal").unwrap();
177        assert_eq!(t.as_str(), "gov_proposal");
178    }
179
180    // ===================================================================
181    // StyleName
182    // ===================================================================
183
184    #[test]
185    fn style_name_empty_is_error() {
186        assert!(StyleName::new("").is_err());
187    }
188
189    #[test]
190    fn style_name_valid() {
191        let s = StyleName::new("heading1").unwrap();
192        assert_eq!(s.as_str(), "heading1");
193    }
194
195    // ===================================================================
196    // Cross-type safety
197    // ===================================================================
198
199    // Edge Case: FontId and TemplateName are distinct types
200    // (This is a compile-time guarantee; the test below just documents it.)
201    #[test]
202    fn id_types_are_distinct() {
203        fn accept_font(_: &FontId) {}
204        fn accept_template(_: &TemplateName) {}
205        let f = FontId::new("x").unwrap();
206        let t = TemplateName::new("x").unwrap();
207        accept_font(&f);
208        accept_template(&t);
209        // accept_font(&t); // Would not compile -- type safety!
210    }
211
212    // ===================================================================
213    // AsRef<str>
214    // ===================================================================
215
216    #[test]
217    fn fontid_as_ref() {
218        let f = FontId::new("test").unwrap();
219        let s: &str = f.as_ref();
220        assert_eq!(s, "test");
221    }
222}