serializer/mappings.rs
1/// DX Serializer: Mapping Management System
2///
3/// # Purpose
4/// Manages bidirectional key abbreviations for the DX serialization format.
5///
6/// # Architecture
7/// Uses a singleton pattern (OnceLock) with dual HashMaps for O(1) lookups:
8/// - `expand`: short → full (machine → human)
9/// - `compress`: full → short (human → machine)
10///
11/// # The Smart Logic
12/// ```text
13/// IF key exists in mappings.dx:
14/// abbreviate it (popular)
15/// ELSE:
16/// keep it as-is (custom)
17/// ```
18///
19/// # Performance
20/// - Load once: ~500μs (lazy, first call only)
21/// - Lookup: O(1) (HashMap)
22/// - Memory: ~15KB for 126 mappings
23///
24/// # Example
25/// ```rust
26/// use serializer::Mappings;
27///
28/// let mappings = Mappings::get();
29///
30/// // Popular keys get abbreviated
31/// assert_eq!(mappings.compress_key("name"), "n");
32/// assert_eq!(mappings.compress_key("version"), "v");
33///
34/// // Custom keys stay as-is
35/// assert_eq!(mappings.compress_key("myCustomKey"), "myCustomKey");
36/// ```
37use std::collections::HashMap;
38use std::fs;
39use std::path::PathBuf;
40use std::sync::OnceLock;
41
42/// Global singleton instance (loaded once, reused forever)
43static MAPPINGS: OnceLock<Mappings> = OnceLock::new();
44
45/// Mapping storage for bidirectional key translation
46///
47/// This is the ONLY caching mechanism needed. The HashMaps provide
48/// instant O(1) lookups with zero overhead after initial load.
49pub struct Mappings {
50 /// Short key → Full name (machine → human expansion)
51 /// Example: "n" → "name"
52 pub expand: HashMap<String, String>,
53
54 /// Full name → Short key (human → machine compression)
55 /// Example: "name" → "n"
56 pub compress: HashMap<String, String>,
57}
58
59impl Mappings {
60 /// Load mappings from .dx/serializer/mappings.dx
61 pub fn load() -> Result<Self, String> {
62 let mapping_path = Self::find_mappings_file()?;
63 let content = fs::read_to_string(&mapping_path)
64 .map_err(|e| format!("Failed to read mappings file: {}", e))?;
65
66 Self::parse(&content)
67 }
68
69 /// Parse mappings from content
70 fn parse(content: &str) -> Result<Self, String> {
71 let mut expand = HashMap::new();
72 let mut compress = HashMap::new();
73
74 for line in content.lines() {
75 let line = line.trim();
76
77 // Skip comments and empty lines
78 if line.is_empty() || line.starts_with('#') {
79 continue;
80 }
81
82 // Parse mapping: short_key=full_name
83 if let Some((short, full)) = line.split_once('=') {
84 let short = short.trim().to_string();
85 let full = full.trim().to_string();
86
87 expand.insert(short.clone(), full.clone());
88 compress.insert(full, short);
89 }
90 }
91
92 Ok(Self { expand, compress })
93 }
94
95 /// Find the mappings file (.dx/serializer/mappings.dx)
96 fn find_mappings_file() -> Result<PathBuf, String> {
97 // Start from current directory and search upwards
98 let mut current = std::env::current_dir()
99 .map_err(|e| format!("Failed to get current directory: {}", e))?;
100
101 loop {
102 let mappings_path = current.join(".dx").join("serializer").join("mappings.dx");
103 if mappings_path.exists() {
104 return Ok(mappings_path);
105 }
106
107 // Try parent directory
108 if !current.pop() {
109 break;
110 }
111 }
112
113 Err("Could not find .dx/serializer/mappings.dx in current directory or parents".to_string())
114 }
115
116 /// Get global mappings instance (lazy load)
117 pub fn get() -> &'static Mappings {
118 MAPPINGS.get_or_init(|| {
119 Self::load().unwrap_or_else(|e| {
120 eprintln!("Warning: Failed to load mappings: {}. Using defaults.", e);
121 Self::default()
122 })
123 })
124 }
125
126 /// Expand short key to full name (machine → human)
127 ///
128 /// # The Smart Logic
129 /// ```text
130 /// IF key exists in mappings.dx:
131 /// expand it (popular key)
132 /// ELSE:
133 /// keep it as-is (custom key)
134 /// ```
135 ///
136 /// # Examples
137 /// ```rust
138 /// # use serializer::Mappings;
139 /// let m = Mappings::get();
140 ///
141 /// // Popular keys: expand (using default mappings)
142 /// assert_eq!(m.expand_key("n"), "name");
143 /// assert_eq!(m.expand_key("v"), "version");
144 /// assert_eq!(m.expand_key("d"), "description");
145 ///
146 /// // Custom keys: preserve
147 /// assert_eq!(m.expand_key("myCustomKey"), "myCustomKey");
148 /// assert_eq!(m.expand_key("userPrefs"), "userPrefs");
149 /// ```
150 ///
151 /// # Performance
152 /// O(1) - Single HashMap lookup with instant fallback
153 #[inline]
154 pub fn expand_key(&self, key: &str) -> String {
155 // NO CACHE NEEDED: HashMap lookup IS the cache (O(1))
156 self.expand
157 .get(key)
158 .cloned()
159 .unwrap_or_else(|| key.to_string())
160 }
161
162 /// Compress full name to short key (human → machine)
163 ///
164 /// # The Smart Logic
165 /// ```text
166 /// IF key exists in mappings.dx:
167 /// abbreviate it (popular key)
168 /// ELSE:
169 /// keep it as-is (custom key)
170 /// ```
171 ///
172 /// # Examples
173 /// ```rust
174 /// # use serializer::Mappings;
175 /// let m = Mappings::get();
176 ///
177 /// // Popular keys: abbreviate (using default mappings)
178 /// assert_eq!(m.compress_key("name"), "n");
179 /// assert_eq!(m.compress_key("version"), "v");
180 /// assert_eq!(m.compress_key("description"), "d");
181 ///
182 /// // Custom keys: preserve
183 /// assert_eq!(m.compress_key("myCustomKey"), "myCustomKey");
184 /// assert_eq!(m.compress_key("userPrefs"), "userPrefs");
185 /// ```
186 ///
187 /// # Performance
188 /// O(1) - Single HashMap lookup with instant fallback
189 #[inline]
190 pub fn compress_key(&self, key: &str) -> String {
191 // NO CACHE NEEDED: HashMap lookup IS the cache (O(1))
192 self.compress
193 .get(key)
194 .cloned()
195 .unwrap_or_else(|| key.to_string())
196 }
197}
198
199impl Default for Mappings {
200 fn default() -> Self {
201 // Fallback mappings if file can't be loaded
202 let mut expand = HashMap::new();
203 let mut compress = HashMap::new();
204
205 let defaults = [
206 ("n", "name"),
207 ("v", "version"),
208 ("t", "title"),
209 ("d", "description"),
210 ("a", "author"),
211 ("c", "context"),
212 ("l", "languages"),
213 ("f", "forge"),
214 ("s", "style"),
215 ("m", "media"),
216 ("i", "i18n"),
217 ("u", "ui"),
218 ];
219
220 for (short, full) in defaults {
221 expand.insert(short.to_string(), full.to_string());
222 compress.insert(full.to_string(), short.to_string());
223 }
224
225 Self { expand, compress }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_parse_mappings() {
235 let content = r#"
236# Comment
237n=name
238v=version
239c=context
240"#;
241
242 let mappings = Mappings::parse(content).unwrap();
243 assert_eq!(mappings.expand_key("n"), "name");
244 assert_eq!(mappings.expand_key("v"), "version");
245 assert_eq!(mappings.compress_key("name"), "n");
246 assert_eq!(mappings.compress_key("version"), "v");
247 }
248
249 #[test]
250 fn test_roundtrip() {
251 let mappings = Mappings::default();
252 let short = "n";
253 let full = mappings.expand_key(short);
254 let back = mappings.compress_key(&full);
255 assert_eq!(short, back);
256 }
257}