Skip to main content

noyalib/with/
singleton_map_with.rs

1//! Serialize enums as single-entry maps with custom key transformation.
2//!
3//! This module extends [`singleton_map`](super::singleton_map) by allowing
4//! custom transformation of variant names during serialization. This is useful
5//! for cases where you want the YAML keys to differ from the Rust enum variant
6//! names (e.g., converting to snake_case, lowercase, or custom mappings).
7//!
8//! # Usage
9//!
10//! Unlike `singleton_map`, this module requires you to define your own
11//! serialize/deserialize functions that call the provided helpers with a
12//! transformation function.
13//!
14//! # Example: Snake Case Transformation
15//!
16//! ```rust
17//! use noyalib::with::singleton_map_with;
18//!
19//! // Define custom serialize/deserialize functions
20//! mod snake_case {
21//!     pub fn serialize<T, S>(
22//!         value: &T,
23//!         serializer: S,
24//!     ) -> Result<S::Ok, S::Error>
25//!     where
26//!         T: serde::Serialize,
27//!         S: serde_core::Serializer,
28//!     {
29//!         noyalib::with::singleton_map_with::serialize_with(
30//!             value,
31//!             serializer,
32//!             to_snake_case,
33//!         )
34//!     }
35//!
36//!     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
37//!     where
38//!         T: serde_core::de::DeserializeOwned + 'static,
39//!         D: serde_core::Deserializer<'de>,
40//!     {
41//!         noyalib::with::singleton_map_with::deserialize_with(
42//!             deserializer,
43//!             from_snake_case,
44//!         )
45//!     }
46//!
47//!     fn to_snake_case(s: &str) -> String {
48//!         let mut result = String::new();
49//!         for (i, c) in s.chars().enumerate() {
50//!             if c.is_uppercase() && i > 0 {
51//!                 result.push('_');
52//!             }
53//!             result.push(c.to_lowercase().next().unwrap());
54//!         }
55//!         result
56//!     }
57//!
58//!     fn from_snake_case(s: &str) -> String {
59//!         s.split('_')
60//!             .map(|word| {
61//!                 let mut chars = word.chars();
62//!                 match chars.next() {
63//!                     Some(first) => {
64//!                         first.to_uppercase().chain(chars).collect()
65//!                     },
66//!                     None => String::new(),
67//!                 }
68//!             })
69//!             .collect()
70//!     }
71//! }
72//!
73//! #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
74//! enum HttpMethod {
75//!     GetRequest,
76//!     PostData,
77//!     DeleteItem,
78//! }
79//!
80//! #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
81//! struct ApiCall {
82//!     #[serde(with = "snake_case")]
83//!     method: HttpMethod,
84//! }
85//!
86//! let call = ApiCall {
87//!     method: HttpMethod::GetRequest,
88//! };
89//! let yaml = noyalib::to_string(&call).unwrap();
90//! // The YAML will contain "get_request" instead of "GetRequest"
91//! ```
92
93// SPDX-License-Identifier: MIT OR Apache-2.0
94// Copyright (c) 2026 Noyalib. All rights reserved.
95
96use crate::prelude::*;
97
98/// Serialize a value as a singleton map with custom key transformation.
99///
100/// This function is similar to
101/// [`singleton_map::serialize`](super::singleton_map::serialize) but allows you
102/// to transform the key (variant name) before serialization.
103///
104/// # Arguments
105///
106/// * `value` - The value to serialize
107/// * `serializer` - The serializer to use
108/// * `transform` - A function that transforms the variant name
109///
110/// # Examples
111///
112/// ```rust
113/// fn my_serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
114/// where
115///     T: serde_core::Serialize,
116///     S: serde_core::Serializer,
117/// {
118///     noyalib::with::singleton_map_with::serialize_with(
119///         value,
120///         serializer,
121///         |s| s.to_lowercase(),
122///     )
123/// }
124/// ```
125pub fn serialize_with<T, S, F>(value: &T, serializer: S, transform: F) -> Result<S::Ok, S::Error>
126where
127    T: serde_core::Serialize,
128    S: serde_core::Serializer,
129    F: Fn(&str) -> String,
130{
131    use serde_core::ser::SerializeMap as _;
132
133    // Serialize to Value to inspect structure
134    let yaml_value = crate::to_value(value).map_err(serde_core::ser::Error::custom)?;
135
136    match yaml_value {
137        crate::Value::Mapping(map) => {
138            // Transform keys in the mapping
139            let mut ser_map = serializer.serialize_map(Some(map.len()))?;
140            for (k, v) in map {
141                let transformed_key = transform(&k);
142                ser_map.serialize_entry(&transformed_key, &v)?;
143            }
144            ser_map.end()
145        }
146        crate::Value::String(s) => {
147            // Unit variant - transform the variant name
148            let transformed = transform(&s);
149            let mut ser_map = serializer.serialize_map(Some(1))?;
150            ser_map.serialize_entry(&transformed, &())?;
151            ser_map.end()
152        }
153        other => {
154            // Fallback: serialize as-is (no transformation possible)
155            serde_core::Serialize::serialize(&other, serializer)
156        }
157    }
158}
159
160/// Deserialize a value from a singleton map with custom key transformation.
161///
162/// This function is similar to
163/// [`singleton_map::deserialize`](super::singleton_map::deserialize) but allows
164/// you to transform the key (variant name) before deserialization.
165///
166/// # Arguments
167///
168/// * `deserializer` - The deserializer to use
169/// * `transform` - A function that transforms the key back to the original
170///   variant name
171///
172/// # Examples
173///
174/// ```rust
175/// fn my_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
176/// where
177///     T: serde_core::de::DeserializeOwned + 'static,
178///     D: serde_core::Deserializer<'de>,
179/// {
180///     noyalib::with::singleton_map_with::deserialize_with(deserializer, |s| {
181///         s.to_uppercase()
182///     })
183/// }
184/// ```
185pub fn deserialize_with<'de, T, D, F>(deserializer: D, transform: F) -> Result<T, D::Error>
186where
187    T: serde_core::de::DeserializeOwned + 'static,
188    D: serde_core::Deserializer<'de>,
189    F: Fn(&str) -> String,
190{
191    use serde_core::Deserialize as _;
192
193    // Deserialize as Value first
194    let value = crate::Value::deserialize(deserializer)?;
195
196    // Transform the keys in the value
197    let transformed = transform_value_keys(value, &transform);
198
199    crate::from_value(&transformed).map_err(serde_core::de::Error::custom)
200}
201
202/// Transform all string keys in a Value according to the provided function.
203fn transform_value_keys<F>(value: crate::Value, transform: &F) -> crate::Value
204where
205    F: Fn(&str) -> String,
206{
207    match value {
208        crate::Value::Mapping(map) => {
209            let mut new_map = crate::Mapping::with_capacity(map.len());
210            for (k, v) in map {
211                let new_key = transform(&k);
212                let new_value = transform_value_keys(v, transform);
213                let _ = new_map.insert(new_key, new_value);
214            }
215            crate::Value::Mapping(new_map)
216        }
217        crate::Value::Sequence(seq) => {
218            let new_seq: Vec<_> = seq
219                .into_iter()
220                .map(|v| transform_value_keys(v, transform))
221                .collect();
222            crate::Value::Sequence(new_seq)
223        }
224        crate::Value::Tagged(tagged) => {
225            let (tag, inner) = tagged.into_parts();
226            let transformed_inner = transform_value_keys(inner, transform);
227            crate::Value::Tagged(Box::new(crate::TaggedValue::new(tag, transformed_inner)))
228        }
229        // Scalars pass through unchanged
230        other => other,
231    }
232}
233
234/// Common key transformation: convert to lowercase.
235///
236/// # Examples
237///
238/// ```rust
239/// use noyalib::with::singleton_map_with::to_lowercase;
240///
241/// assert_eq!(to_lowercase("GetRequest"), "getrequest");
242/// assert_eq!(to_lowercase("POST"), "post");
243/// ```
244#[must_use]
245pub fn to_lowercase(s: &str) -> String {
246    s.to_lowercase()
247}
248
249/// Common key transformation: convert to UPPERCASE.
250///
251/// # Examples
252///
253/// ```rust
254/// use noyalib::with::singleton_map_with::to_uppercase;
255///
256/// assert_eq!(to_uppercase("GetRequest"), "GETREQUEST");
257/// assert_eq!(to_uppercase("post"), "POST");
258/// ```
259#[must_use]
260pub fn to_uppercase(s: &str) -> String {
261    s.to_uppercase()
262}
263
264/// Common key transformation: convert PascalCase to snake_case.
265///
266/// # Examples
267///
268/// ```rust
269/// use noyalib::with::singleton_map_with::to_snake_case;
270///
271/// assert_eq!(to_snake_case("GetRequest"), "get_request");
272/// assert_eq!(to_snake_case("HTTPServer"), "h_t_t_p_server");
273/// assert_eq!(to_snake_case("already_snake"), "already_snake");
274/// ```
275#[must_use]
276pub fn to_snake_case(s: &str) -> String {
277    let mut result = String::with_capacity(s.len() + 4);
278    for (i, c) in s.chars().enumerate() {
279        if c.is_uppercase() && i > 0 {
280            result.push('_');
281        }
282        result.extend(c.to_lowercase());
283    }
284    result
285}
286
287/// Common key transformation: convert snake_case to PascalCase.
288///
289/// # Examples
290///
291/// ```rust
292/// use noyalib::with::singleton_map_with::to_pascal_case;
293///
294/// assert_eq!(to_pascal_case("get_request"), "GetRequest");
295/// assert_eq!(to_pascal_case("http_server"), "HttpServer");
296/// assert_eq!(to_pascal_case("AlreadyPascal"), "Alreadypascal");
297/// ```
298#[must_use]
299pub fn to_pascal_case(s: &str) -> String {
300    let mut result = String::with_capacity(s.len());
301    for word in s.split('_') {
302        let mut chars = word.chars();
303        if let Some(first) = chars.next() {
304            result.extend(first.to_uppercase());
305            for c in chars {
306                result.extend(c.to_lowercase());
307            }
308        }
309    }
310    result
311}
312
313/// Common key transformation: convert to kebab-case.
314///
315/// # Examples
316///
317/// ```rust
318/// use noyalib::with::singleton_map_with::to_kebab_case;
319///
320/// assert_eq!(to_kebab_case("GetRequest"), "get-request");
321/// assert_eq!(to_kebab_case("HTTPServer"), "h-t-t-p-server");
322/// ```
323#[must_use]
324pub fn to_kebab_case(s: &str) -> String {
325    let mut result = String::with_capacity(s.len() + 4);
326    for (i, c) in s.chars().enumerate() {
327        if c.is_uppercase() && i > 0 {
328            result.push('-');
329        }
330        result.extend(c.to_lowercase());
331    }
332    result
333}
334
335/// Common key transformation: convert kebab-case to PascalCase.
336///
337/// # Examples
338///
339/// ```rust
340/// use noyalib::with::singleton_map_with::from_kebab_case;
341///
342/// assert_eq!(from_kebab_case("get-request"), "GetRequest");
343/// assert_eq!(from_kebab_case("http-server"), "HttpServer");
344/// ```
345#[must_use]
346pub fn from_kebab_case(s: &str) -> String {
347    s.split('-')
348        .map(|word| {
349            let mut chars = word.chars();
350            match chars.next() {
351                Some(first) => {
352                    let upper: String = first.to_uppercase().collect();
353                    let rest: String = chars.flat_map(|c| c.to_lowercase()).collect();
354                    upper + &rest
355                }
356                None => String::new(),
357            }
358        })
359        .collect()
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_to_snake_case() {
368        assert_eq!(to_snake_case("GetRequest"), "get_request");
369        assert_eq!(to_snake_case("HTTPServer"), "h_t_t_p_server");
370        assert_eq!(to_snake_case("simple"), "simple");
371        assert_eq!(to_snake_case("Already_Snake"), "already__snake");
372    }
373
374    #[test]
375    fn test_to_pascal_case() {
376        assert_eq!(to_pascal_case("get_request"), "GetRequest");
377        assert_eq!(to_pascal_case("http_server"), "HttpServer");
378        assert_eq!(to_pascal_case("simple"), "Simple");
379    }
380
381    #[test]
382    fn test_to_kebab_case() {
383        assert_eq!(to_kebab_case("GetRequest"), "get-request");
384        assert_eq!(to_kebab_case("SimpleTest"), "simple-test");
385    }
386
387    #[test]
388    fn test_from_kebab_case() {
389        assert_eq!(from_kebab_case("get-request"), "GetRequest");
390        assert_eq!(from_kebab_case("http-server"), "HttpServer");
391    }
392
393    #[test]
394    fn test_lowercase_uppercase() {
395        assert_eq!(to_lowercase("GetRequest"), "getrequest");
396        assert_eq!(to_uppercase("GetRequest"), "GETREQUEST");
397    }
398
399    #[test]
400    fn test_transform_value_keys() {
401        let mut mapping = crate::Mapping::new();
402        let _ = mapping.insert("UnitVariant", crate::Value::Null);
403        let _ = mapping.insert("StructVariant", crate::Value::from(42));
404        let value = crate::Value::Mapping(mapping);
405
406        let transformed = transform_value_keys(value, &to_snake_case);
407
408        let map = transformed.as_mapping().unwrap();
409        assert!(map.contains_key("unit_variant"));
410        assert!(map.contains_key("struct_variant"));
411    }
412
413    #[test]
414    fn test_transform_value_keys_nested() {
415        let mut inner = crate::Mapping::new();
416        let _ = inner.insert("InnerKey", crate::Value::from("value"));
417
418        let mut outer = crate::Mapping::new();
419        let _ = outer.insert("OuterKey", crate::Value::Mapping(inner));
420
421        let value = crate::Value::Mapping(outer);
422        let transformed = transform_value_keys(value, &to_snake_case);
423
424        let outer_map = transformed.as_mapping().unwrap();
425        assert!(outer_map.contains_key("outer_key"));
426
427        let inner_map = outer_map.get("outer_key").unwrap().as_mapping().unwrap();
428        assert!(inner_map.contains_key("inner_key"));
429    }
430
431    #[test]
432    fn test_transform_value_keys_sequence() {
433        let mut item = crate::Mapping::new();
434        let _ = item.insert("ItemKey", crate::Value::from(1));
435
436        let seq = crate::Value::Sequence(vec![crate::Value::Mapping(item)]);
437        let transformed = transform_value_keys(seq, &to_snake_case);
438
439        let items = transformed.as_sequence().unwrap();
440        let first = items[0].as_mapping().unwrap();
441        assert!(first.contains_key("item_key"));
442    }
443
444    #[test]
445    fn test_serialize_with_directly() {
446        #[derive(serde::Serialize)]
447        struct TestStruct {
448            name: String,
449        }
450
451        let value = TestStruct {
452            name: "test".to_string(),
453        };
454
455        // Serialize using our function with lowercase transform
456        let yaml = crate::to_string(&value).unwrap();
457        // The struct should serialize normally
458        assert!(yaml.contains("name:"));
459    }
460}