Skip to main content

noyalib/with/
singleton_map.rs

1//! Serialize enums as single-entry maps.
2//!
3//! This module provides `serialize` and `deserialize` functions for use with
4//! serde's `#[serde(with = "...")]` attribute to serialize enum variants as
5//! single-entry maps where the key is the variant name.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use noyalib::with::singleton_map;
11//!
12//! #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
13//! enum Status {
14//!     Active,
15//!     Pending { reason: String },
16//!     Error { code: i32, message: String },
17//! }
18//!
19//! #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
20//! struct Task {
21//!     name: String,
22//!     #[serde(with = "singleton_map")]
23//!     status: Status,
24//! }
25//!
26//! let task = Task {
27//!     name: "example".to_string(),
28//!     status: Status::Pending {
29//!         reason: "waiting".to_string(),
30//!     },
31//! };
32//!
33//! let yaml = noyalib::to_string(&task).unwrap();
34//! // Output:
35//! // name: example
36//! // status:
37//! //   Pending:
38//! //     reason: waiting
39//! ```
40
41// SPDX-License-Identifier: MIT OR Apache-2.0
42// Copyright (c) 2026 Noyalib. All rights reserved.
43
44/// Serialize a value as a singleton map.
45///
46/// For enums, this serializes the variant as a map where the key is the
47/// variant name and the value is the variant's data.
48///
49/// # Examples
50///
51/// ```rust
52/// use noyalib::with::singleton_map;
53///
54/// #[derive(Debug, serde::Serialize, serde::Deserialize)]
55/// enum Action {
56///     Start,
57///     Stop { graceful: bool },
58/// }
59///
60/// #[derive(Debug, serde::Serialize, serde::Deserialize)]
61/// struct Command {
62///     #[serde(with = "singleton_map")]
63///     action: Action,
64/// }
65/// ```
66pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
67where
68    T: serde_core::Serialize,
69    S: serde_core::Serializer,
70{
71    use serde_core::ser::SerializeMap as _;
72
73    // Serialize to Value to inspect structure
74    let yaml_value = crate::to_value(value).map_err(serde_core::ser::Error::custom)?;
75
76    match yaml_value {
77        crate::Value::Mapping(map) => {
78            // Already a map (struct variant), serialize directly
79            let mut ser_map = serializer.serialize_map(Some(map.len()))?;
80            for (k, v) in map {
81                ser_map.serialize_entry(&k, &v)?;
82            }
83            ser_map.end()
84        }
85        crate::Value::String(s) => {
86            // Unit variant - serialize as { VariantName: null }
87            let mut ser_map = serializer.serialize_map(Some(1))?;
88            ser_map.serialize_entry(&s, &())?;
89            ser_map.end()
90        }
91        other => {
92            // Fallback: serialize as-is
93            serde_core::Serialize::serialize(&other, serializer)
94        }
95    }
96}
97
98/// Deserialize a value from a singleton map.
99///
100/// This is the counterpart to [`serialize`], deserializing from the
101/// singleton map format back to the original type.
102///
103/// # Examples
104///
105/// ```
106/// use noyalib::with::singleton_map;
107///
108/// #[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
109/// enum Status { Active }
110///
111/// #[derive(serde::Deserialize, Debug)]
112/// struct Doc {
113///     #[serde(with = "singleton_map")]
114///     s: Status,
115/// }
116///
117/// let d: Doc = noyalib::from_str("s:\n  Active: null\n").unwrap();
118/// assert_eq!(d.s, Status::Active);
119/// ```
120pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
121where
122    T: serde_core::de::DeserializeOwned + 'static,
123    D: serde_core::Deserializer<'de>,
124{
125    use serde_core::Deserialize as _;
126    // Deserialize as Value first
127    let value = crate::Value::deserialize(deserializer)?;
128    crate::from_value(&value).map_err(serde_core::de::Error::custom)
129}
130
131#[cfg(test)]
132mod tests {
133
134    #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
135    enum TestEnum {
136        Unit,
137        Newtype(i32),
138        Struct { value: String },
139    }
140
141    #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
142    struct Container {
143        name: String,
144        #[serde(with = "crate::with::singleton_map")]
145        item: TestEnum,
146    }
147
148    #[test]
149    fn test_singleton_map_unit_variant() {
150        let container = Container {
151            name: "test".to_string(),
152            item: TestEnum::Unit,
153        };
154
155        let yaml = crate::to_string(&container).unwrap();
156        assert!(yaml.contains("Unit"));
157
158        let parsed: Container = crate::from_str(&yaml).unwrap();
159        assert_eq!(parsed, container);
160    }
161
162    #[test]
163    fn test_singleton_map_struct_variant() {
164        let container = Container {
165            name: "test".to_string(),
166            item: TestEnum::Struct {
167                value: "hello".to_string(),
168            },
169        };
170
171        let yaml = crate::to_string(&container).unwrap();
172        assert!(yaml.contains("Struct"));
173        assert!(yaml.contains("value: hello"));
174
175        let parsed: Container = crate::from_str(&yaml).unwrap();
176        assert_eq!(parsed, container);
177    }
178
179    #[test]
180    fn test_singleton_map_fallback_sequence() {
181        // Test the fallback path when value is a Sequence
182        #[derive(Debug, serde::Serialize)]
183        struct SeqContainer {
184            #[serde(with = "crate::with::singleton_map")]
185            items: Vec<i32>,
186        }
187
188        let container = SeqContainer {
189            items: vec![1, 2, 3],
190        };
191
192        let yaml = crate::to_string(&container).unwrap();
193        assert!(yaml.contains('1'));
194        assert!(yaml.contains('2'));
195        assert!(yaml.contains('3'));
196    }
197
198    #[test]
199    fn test_singleton_map_fallback_integer() {
200        // Test the fallback path when value is a Number
201        #[derive(Debug, serde::Serialize)]
202        struct NumContainer {
203            #[serde(with = "crate::with::singleton_map")]
204            value: i32,
205        }
206
207        let container = NumContainer { value: 42 };
208
209        let yaml = crate::to_string(&container).unwrap();
210        assert!(yaml.contains("42"));
211    }
212}