1use super::{TrustedGroup, TrustedUser};
4use serde::{Deserialize, Serialize};
5use std::{convert::TryFrom, ffi::OsString};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(untagged)]
14pub(super) enum Serde {
15 Bool(bool),
21 Str(String),
27 Num(u32),
31 Name {
33 name: String,
37 },
38 Raw {
40 raw_name: OsString,
42 },
43 Special {
45 special: String,
47 },
48 Id {
50 id: u32,
52 },
53}
54
55impl Serde {
56 fn disambiguate(self) -> Self {
61 match self {
62 Serde::Str(s) if s.starts_with(':') => Self::Special { special: s },
63 Serde::Str(s) => Self::Name { name: s },
64 Serde::Num(id) => Self::Id { id },
65 other => other,
66 }
67 }
68}
69
70macro_rules! implement_serde {
72 { $struct:ident { $( $case:ident => $str:expr, )* [ $errcase:ident ] } } => {
73
74 impl $struct {
75 fn from_special_str(s: &str) -> Result<Self, crate::Error> {
77 match s {
78 $( $str => Ok($struct::$case), )*
79 _ => Err(crate::Error::$errcase(s.to_owned())),
80 }
81 }
82 fn from_boolean(b: bool) -> Result<Self, crate::Error> {
83 if b {
84 Err(crate::Error::$errcase("'true'".into()))
85 } else {
86 Self::from_special_str(":none")
87 }
88 }
89 }
90
91 impl From<$struct> for Serde {
92 fn from(value: $struct) -> Self {
93 match value {
94 $struct::Id(id) => Self::Num(id),
95 $struct::Name(name) => {
96 if let Some(name) = name.to_str() {
97 let name = name.to_string();
98 if name.starts_with(':') {
99 Self::Name { name }
100 } else {
101 Self::Str(name)
102 }
103 } else {
104 Self::Raw { raw_name: name }
105 }
106 }
107 $(
108 $struct::$case => Self::Str($str.to_owned())
109 ),*
110 }
111 }
112 }
113
114 impl TryFrom<Serde> for $struct {
115 type Error = crate::Error;
116 fn try_from(ent: Serde) -> Result<Self, Self::Error> {
117 Ok(match ent.disambiguate() {
118 Serde::Str(_) | Serde::Num(_) => {
119 panic!("These should have been caught by disambiguate.")
120 }
121 Serde::Bool(b) => $struct::from_boolean(b)?,
122 Serde::Name { name } => $struct::Name(name.into()),
123 Serde::Raw { raw_name } => $struct::Name(raw_name),
124 Serde::Special { special } => {
125 $struct::from_special_str(special.as_ref())?
126 }
127 Serde::Id { id } => $struct::Id(id),
128 })
129 }
130 }
131}}
132
133implement_serde! { TrustedUser {
134 None => ":none",
135 Current => ":current",
136 [NoSuchUser]
137}}
138
139implement_serde! { TrustedGroup {
140 None => ":none",
141 SelfNamed => ":username",
142 [NoSuchGroup]
143}}
144
145#[cfg(test)]
146mod test {
147 #![allow(clippy::bool_assert_comparison)]
149 #![allow(clippy::clone_on_copy)]
150 #![allow(clippy::dbg_macro)]
151 #![allow(clippy::mixed_attributes_style)]
152 #![allow(clippy::print_stderr)]
153 #![allow(clippy::print_stdout)]
154 #![allow(clippy::single_char_pattern)]
155 #![allow(clippy::unwrap_used)]
156 #![allow(clippy::unchecked_duration_subtraction)]
157 #![allow(clippy::useless_vec)]
158 #![allow(clippy::needless_pass_by_value)]
159 use super::*;
161
162 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
163 struct Chum {
164 handle: TrustedUser,
165 team: TrustedGroup,
166 }
167
168 #[test]
169 fn round_trips() {
170 let examples: Vec<(&'static str, &'static str, Chum)> = vec![
171 (
172 r#"handle = "gardenGnostic"
173 team = 413
174 "#,
175 r#"{ "handle": "gardenGnostic", "team": 413 }"#,
176 Chum {
177 handle: TrustedUser::Name("gardenGnostic".into()),
178 team: TrustedGroup::Id(413),
179 },
180 ),
181 (
182 r#"handle = "413"
183 team = false
184 "#,
185 r#"{ "handle": "413", "team": false }"#,
186 Chum {
187 handle: TrustedUser::Name("413".into()),
188 team: TrustedGroup::None,
189 },
190 ),
191 (
192 r#"handle = { id = 8 }
193 team = { name = "flarp" }
194 "#,
195 r#"{ "handle": { "id": 8 }, "team" : { "name" : "flarp" } }"#,
196 Chum {
197 handle: TrustedUser::Id(8),
198 team: TrustedGroup::Name("flarp".into()),
199 },
200 ),
201 (
202 r#"handle = ":current"
203 team = ":username"
204 "#,
205 r#"{ "handle": ":current", "team" : ":username" }"#,
206 Chum {
207 handle: TrustedUser::Current,
208 team: TrustedGroup::SelfNamed,
209 },
210 ),
211 (
212 r#"handle = { special = ":none" }
213 team = { special = ":none" }
214 "#,
215 r#"{ "handle": {"special" : ":none"}, "team" : { "special" : ":none"} }"#,
216 Chum {
217 handle: TrustedUser::None,
218 team: TrustedGroup::None,
219 },
220 ),
221 (
222 r#"handle = { name = ":none" }
223 team = { name = ":none" }
224 "#,
225 r#"{ "handle": {"name" : ":none"}, "team" : { "name" : ":none"} }"#,
226 Chum {
227 handle: TrustedUser::Name(":none".into()),
228 team: TrustedGroup::Name(":none".into()),
229 },
230 ),
231 ];
232
233 for (toml_string, json_string, chum) in examples {
234 let toml_obj: Chum = toml::from_str(toml_string).unwrap();
235 let json_obj: Chum = serde_json::from_str(json_string).unwrap();
236 assert_eq!(&toml_obj, &chum);
237 assert_eq!(&json_obj, &chum);
238
239 let s = toml::to_string(&chum).unwrap();
240 let toml_obj2: Chum = toml::from_str(&s).unwrap();
241 assert_eq!(&toml_obj2, &chum);
242
243 let s = serde_json::to_string(&chum).unwrap();
244 let json_obj2: Chum = serde_json::from_str(&s).unwrap();
245 assert_eq!(&json_obj2, &chum);
246 }
247 }
248
249 #[cfg(target_family = "unix")]
250 #[test]
251 fn os_string() {
252 use std::os::unix::ffi::OsStringExt as _;
254 let not_utf8 = OsString::from_vec(vec![255, 254, 253, 252]);
255 assert!(not_utf8.to_str().is_none());
256 let chum = Chum {
257 handle: TrustedUser::Name(not_utf8.clone()),
258 team: TrustedGroup::Name(not_utf8),
259 };
260
261 let s = serde_json::to_string(&chum).unwrap();
269 let toml_obj: Chum = serde_json::from_str(&s).unwrap();
270 assert_eq!(&toml_obj, &chum);
271 }
272
273 #[test]
274 fn bad_names() {
275 let s = r#"handle = 413
276 team = false"#;
277 let r: Result<Chum, _> = toml::from_str(s);
278 assert!(r.is_ok());
279
280 let s = r#"handle = true
281 team = false"#;
282 let r: Result<Chum, _> = toml::from_str(s);
283 assert!(r.is_err());
284
285 let s = r#"handle = ":foo"
286 team = false"#;
287 let r: Result<Chum, _> = toml::from_str(s);
288 assert!(r.is_err());
289 }
290}