Skip to main content

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}