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}