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}