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_time_subtraction)]
157 #![allow(clippy::useless_vec)]
158 #![allow(clippy::needless_pass_by_value)]
159 #![allow(clippy::string_slice)] use super::*;
162
163 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
164 struct Chum {
165 handle: TrustedUser,
166 team: TrustedGroup,
167 }
168
169 #[test]
170 fn round_trips() {
171 let examples: Vec<(&'static str, &'static str, Chum)> = vec![
172 (
173 r#"handle = "gardenGnostic"
174 team = 413
175 "#,
176 r#"{ "handle": "gardenGnostic", "team": 413 }"#,
177 Chum {
178 handle: TrustedUser::Name("gardenGnostic".into()),
179 team: TrustedGroup::Id(413),
180 },
181 ),
182 (
183 r#"handle = "413"
184 team = false
185 "#,
186 r#"{ "handle": "413", "team": false }"#,
187 Chum {
188 handle: TrustedUser::Name("413".into()),
189 team: TrustedGroup::None,
190 },
191 ),
192 (
193 r#"handle = { id = 8 }
194 team = { name = "flarp" }
195 "#,
196 r#"{ "handle": { "id": 8 }, "team" : { "name" : "flarp" } }"#,
197 Chum {
198 handle: TrustedUser::Id(8),
199 team: TrustedGroup::Name("flarp".into()),
200 },
201 ),
202 (
203 r#"handle = ":current"
204 team = ":username"
205 "#,
206 r#"{ "handle": ":current", "team" : ":username" }"#,
207 Chum {
208 handle: TrustedUser::Current,
209 team: TrustedGroup::SelfNamed,
210 },
211 ),
212 (
213 r#"handle = { special = ":none" }
214 team = { special = ":none" }
215 "#,
216 r#"{ "handle": {"special" : ":none"}, "team" : { "special" : ":none"} }"#,
217 Chum {
218 handle: TrustedUser::None,
219 team: TrustedGroup::None,
220 },
221 ),
222 (
223 r#"handle = { name = ":none" }
224 team = { name = ":none" }
225 "#,
226 r#"{ "handle": {"name" : ":none"}, "team" : { "name" : ":none"} }"#,
227 Chum {
228 handle: TrustedUser::Name(":none".into()),
229 team: TrustedGroup::Name(":none".into()),
230 },
231 ),
232 ];
233
234 for (toml_string, json_string, chum) in examples {
235 let toml_obj: Chum = toml::from_str(toml_string).unwrap();
236 let json_obj: Chum = serde_json::from_str(json_string).unwrap();
237 assert_eq!(&toml_obj, &chum);
238 assert_eq!(&json_obj, &chum);
239
240 let s = toml::to_string(&chum).unwrap();
241 let toml_obj2: Chum = toml::from_str(&s).unwrap();
242 assert_eq!(&toml_obj2, &chum);
243
244 let s = serde_json::to_string(&chum).unwrap();
245 let json_obj2: Chum = serde_json::from_str(&s).unwrap();
246 assert_eq!(&json_obj2, &chum);
247 }
248 }
249
250 #[cfg(target_family = "unix")]
251 #[test]
252 fn os_string() {
253 use std::os::unix::ffi::OsStringExt as _;
255 let not_utf8 = OsString::from_vec(vec![255, 254, 253, 252]);
256 assert!(not_utf8.to_str().is_none());
257 let chum = Chum {
258 handle: TrustedUser::Name(not_utf8.clone()),
259 team: TrustedGroup::Name(not_utf8),
260 };
261
262 let s = serde_json::to_string(&chum).unwrap();
270 let toml_obj: Chum = serde_json::from_str(&s).unwrap();
271 assert_eq!(&toml_obj, &chum);
272 }
273
274 #[test]
275 fn bad_names() {
276 let s = r#"handle = 413
277 team = false"#;
278 let r: Result<Chum, _> = toml::from_str(s);
279 assert!(r.is_ok());
280
281 let s = r#"handle = true
282 team = false"#;
283 let r: Result<Chum, _> = toml::from_str(s);
284 assert!(r.is_err());
285
286 let s = r#"handle = ":foo"
287 team = false"#;
288 let r: Result<Chum, _> = toml::from_str(s);
289 assert!(r.is_err());
290 }
291}