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    #![expect(
56        clippy::assertions_on_result_states,
57        reason = "test assertions — is_ok/is_err provides readable failure messages"
58    )]
59    use super::*;
60    use strum::IntoEnumIterator;
61
62    // ── Serde round-trip ─────────────────────────────────────────────
63
64    #[test]
65    fn serde_round_trip_all_variants() {
66        for mode in RegistrationMode::iter() {
67            let json = serde_json::to_string(&mode).expect("serialization should succeed");
68            let deserialized: RegistrationMode =
69                serde_json::from_str(&json).expect("deserialization should succeed");
70            assert_eq!(deserialized, mode);
71        }
72    }
73
74    #[test]
75    fn serde_serializes_as_snake_case_strings() {
76        assert_eq!(
77            serde_json::to_string(&RegistrationMode::Open).expect("serialization should succeed"),
78            r#""open""#
79        );
80        assert_eq!(
81            serde_json::to_string(&RegistrationMode::Invite).expect("serialization should succeed"),
82            r#""invite""#
83        );
84        assert_eq!(
85            serde_json::to_string(&RegistrationMode::Closed).expect("serialization should succeed"),
86            r#""closed""#
87        );
88    }
89
90    #[test]
91    fn serde_rejects_unknown_variant() {
92        let result = serde_json::from_str::<RegistrationMode>(r#""disabled""#);
93        assert!(result.is_err());
94    }
95
96    #[test]
97    fn serde_rejects_uppercase_variant() {
98        let result = serde_json::from_str::<RegistrationMode>(r#""Open""#);
99        assert!(result.is_err());
100    }
101
102    // ── Display output ───────────────────────────────────────────────
103
104    #[test]
105    fn display_open() {
106        assert_eq!(format!("{}", RegistrationMode::Open), "open");
107    }
108
109    #[test]
110    fn display_invite() {
111        assert_eq!(format!("{}", RegistrationMode::Invite), "invite");
112    }
113
114    #[test]
115    fn display_closed() {
116        assert_eq!(format!("{}", RegistrationMode::Closed), "closed");
117    }
118
119    #[test]
120    fn display_matches_as_str_for_all_variants() {
121        for mode in RegistrationMode::iter() {
122            assert_eq!(format!("{mode}"), mode.as_str());
123        }
124    }
125
126    // ── FromStr valid inputs ─────────────────────────────────────────
127
128    #[test]
129    fn from_str_open() {
130        let parsed: RegistrationMode = "open".parse().expect("should parse 'open'");
131        assert_eq!(parsed, RegistrationMode::Open);
132    }
133
134    #[test]
135    fn from_str_invite() {
136        let parsed: RegistrationMode = "invite".parse().expect("should parse 'invite'");
137        assert_eq!(parsed, RegistrationMode::Invite);
138    }
139
140    #[test]
141    fn from_str_closed() {
142        let parsed: RegistrationMode = "closed".parse().expect("should parse 'closed'");
143        assert_eq!(parsed, RegistrationMode::Closed);
144    }
145
146    #[test]
147    fn from_str_round_trips_through_as_str() {
148        for mode in RegistrationMode::iter() {
149            let s = mode.as_str();
150            let parsed: RegistrationMode = s
151                .parse()
152                .expect("from_str should succeed for as_str output");
153            assert_eq!(parsed, mode);
154        }
155    }
156
157    // ── FromStr invalid inputs ───────────────────────────────────────
158
159    #[test]
160    fn from_str_empty_string_fails() {
161        assert!("".parse::<RegistrationMode>().is_err());
162    }
163
164    #[test]
165    fn from_str_uppercase_fails() {
166        assert!("OPEN".parse::<RegistrationMode>().is_err());
167        assert!("Open".parse::<RegistrationMode>().is_err());
168    }
169
170    #[test]
171    fn from_str_unknown_value_fails() {
172        assert!("disabled".parse::<RegistrationMode>().is_err());
173        assert!("public".parse::<RegistrationMode>().is_err());
174    }
175
176    #[test]
177    fn from_str_whitespace_fails() {
178        assert!(" open".parse::<RegistrationMode>().is_err());
179        assert!("open ".parse::<RegistrationMode>().is_err());
180    }
181
182    // ── ParseRegistrationModeError ───────────────────────────────────
183
184    #[test]
185    fn parse_error_display_message() {
186        let err = ParseRegistrationModeError;
187        assert_eq!(err.to_string(), "invalid registration mode value");
188    }
189}