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}