Skip to main content

uptrakit_web_api_types/
registration.rs

1use serde::{Deserialize, Serialize};
2use std::str::FromStr;
3use thiserror::Error;
4
5/// Registration mode controlling how new users can sign up.
6#[non_exhaustive]
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[cfg_attr(test, derive(strum::EnumIter))]
10#[serde(rename_all = "snake_case")]
11pub enum RegistrationMode {
12    /// Anyone can register without a token.
13    Open,
14    /// Registration requires a valid token.
15    Invite,
16    /// Registration is disabled.
17    Closed,
18}
19
20impl RegistrationMode {
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            Self::Open => "open",
24            Self::Invite => "invite",
25            Self::Closed => "closed",
26        }
27    }
28}
29
30impl std::fmt::Display for RegistrationMode {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.write_str(self.as_str())
33    }
34}
35
36#[derive(Debug, Error)]
37#[error("invalid registration mode value")]
38pub struct ParseRegistrationModeError;
39
40impl FromStr for RegistrationMode {
41    type Err = ParseRegistrationModeError;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s {
45            "open" => Ok(Self::Open),
46            "invite" => Ok(Self::Invite),
47            "closed" => Ok(Self::Closed),
48            _ => Err(ParseRegistrationModeError),
49        }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use strum::IntoEnumIterator;
57
58    // ── Serde round-trip ─────────────────────────────────────────────
59
60    #[test]
61    fn serde_round_trip_all_variants() {
62        for mode in RegistrationMode::iter() {
63            let json = serde_json::to_string(&mode).expect("serialization should succeed");
64            let deserialized: RegistrationMode =
65                serde_json::from_str(&json).expect("deserialization should succeed");
66            assert_eq!(deserialized, mode);
67        }
68    }
69
70    #[test]
71    fn serde_serializes_as_snake_case_strings() {
72        assert_eq!(
73            serde_json::to_string(&RegistrationMode::Open).expect("serialization should succeed"),
74            r#""open""#
75        );
76        assert_eq!(
77            serde_json::to_string(&RegistrationMode::Invite).expect("serialization should succeed"),
78            r#""invite""#
79        );
80        assert_eq!(
81            serde_json::to_string(&RegistrationMode::Closed).expect("serialization should succeed"),
82            r#""closed""#
83        );
84    }
85
86    #[test]
87    fn serde_rejects_unknown_variant() {
88        let result = serde_json::from_str::<RegistrationMode>(r#""disabled""#);
89        assert!(result.is_err());
90    }
91
92    #[test]
93    fn serde_rejects_uppercase_variant() {
94        let result = serde_json::from_str::<RegistrationMode>(r#""Open""#);
95        assert!(result.is_err());
96    }
97
98    // ── Display output ───────────────────────────────────────────────
99
100    #[test]
101    fn display_open() {
102        assert_eq!(format!("{}", RegistrationMode::Open), "open");
103    }
104
105    #[test]
106    fn display_invite() {
107        assert_eq!(format!("{}", RegistrationMode::Invite), "invite");
108    }
109
110    #[test]
111    fn display_closed() {
112        assert_eq!(format!("{}", RegistrationMode::Closed), "closed");
113    }
114
115    #[test]
116    fn display_matches_as_str_for_all_variants() {
117        for mode in RegistrationMode::iter() {
118            assert_eq!(format!("{mode}"), mode.as_str());
119        }
120    }
121
122    // ── FromStr valid inputs ─────────────────────────────────────────
123
124    #[test]
125    fn from_str_open() {
126        let parsed: RegistrationMode = "open".parse().expect("should parse 'open'");
127        assert_eq!(parsed, RegistrationMode::Open);
128    }
129
130    #[test]
131    fn from_str_invite() {
132        let parsed: RegistrationMode = "invite".parse().expect("should parse 'invite'");
133        assert_eq!(parsed, RegistrationMode::Invite);
134    }
135
136    #[test]
137    fn from_str_closed() {
138        let parsed: RegistrationMode = "closed".parse().expect("should parse 'closed'");
139        assert_eq!(parsed, RegistrationMode::Closed);
140    }
141
142    #[test]
143    fn from_str_round_trips_through_as_str() {
144        for mode in RegistrationMode::iter() {
145            let s = mode.as_str();
146            let parsed: RegistrationMode = s
147                .parse()
148                .expect("from_str should succeed for as_str output");
149            assert_eq!(parsed, mode);
150        }
151    }
152
153    // ── FromStr invalid inputs ───────────────────────────────────────
154
155    #[test]
156    fn from_str_empty_string_fails() {
157        assert!("".parse::<RegistrationMode>().is_err());
158    }
159
160    #[test]
161    fn from_str_uppercase_fails() {
162        assert!("OPEN".parse::<RegistrationMode>().is_err());
163        assert!("Open".parse::<RegistrationMode>().is_err());
164    }
165
166    #[test]
167    fn from_str_unknown_value_fails() {
168        assert!("disabled".parse::<RegistrationMode>().is_err());
169        assert!("public".parse::<RegistrationMode>().is_err());
170    }
171
172    #[test]
173    fn from_str_whitespace_fails() {
174        assert!(" open".parse::<RegistrationMode>().is_err());
175        assert!("open ".parse::<RegistrationMode>().is_err());
176    }
177
178    // ── ParseRegistrationModeError ───────────────────────────────────
179
180    #[test]
181    fn parse_error_display_message() {
182        let err = ParseRegistrationModeError;
183        assert_eq!(err.to_string(), "invalid registration mode value");
184    }
185}