1use serde::{Deserialize, Serialize};
15use utoipa::ToSchema;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
31pub struct Posture {
32 pub signed: bool,
34 pub encrypted: bool,
36}
37
38impl Posture {
39 pub const OPEN: Posture = Posture {
41 signed: false,
42 encrypted: false,
43 };
44
45 pub const fn new(signed: bool, encrypted: bool) -> Self {
47 Self { signed, encrypted }
48 }
49
50 pub const fn level(self) -> PostureLevel {
55 match (self.signed, self.encrypted) {
56 (false, _) => PostureLevel::Open,
57 (true, false) => PostureLevel::Authenticated,
58 (true, true) => PostureLevel::Confidential,
59 }
60 }
61
62 pub const fn is_secure(self) -> bool {
65 self.signed
66 }
67}
68
69#[derive(
75 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
76)]
77#[serde(rename_all = "snake_case")]
78pub enum PostureLevel {
79 Open,
81 Authenticated,
83 Confidential,
85}
86
87impl PostureLevel {
88 pub const fn as_wire(self) -> &'static str {
93 match self {
94 PostureLevel::Open => "open",
95 PostureLevel::Authenticated => "authenticated",
96 PostureLevel::Confidential => "confidential",
97 }
98 }
99
100 pub fn from_wire(s: &str) -> Option<Self> {
105 match s {
106 "open" => Some(PostureLevel::Open),
107 "authenticated" => Some(PostureLevel::Authenticated),
108 "confidential" => Some(PostureLevel::Confidential),
109 _ => None,
110 }
111 }
112
113 pub const fn to_posture(self) -> Posture {
116 match self {
117 PostureLevel::Open => Posture::OPEN,
118 PostureLevel::Authenticated => Posture::new(true, false),
119 PostureLevel::Confidential => Posture::new(true, true),
120 }
121 }
122}
123
124impl From<Posture> for PostureLevel {
125 fn from(p: Posture) -> Self {
126 p.level()
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn open_is_neither_and_not_secure() {
136 assert_eq!(Posture::OPEN, Posture::new(false, false));
137 assert_eq!(Posture::OPEN.level(), PostureLevel::Open);
138 assert!(!Posture::OPEN.is_secure());
139 }
140
141 #[test]
142 fn default_posture_is_open() {
143 assert_eq!(Posture::default(), Posture::OPEN);
144 }
145
146 #[test]
147 fn signed_only_is_authenticated_and_secure() {
148 let p = Posture::new(true, false);
149 assert_eq!(p.level(), PostureLevel::Authenticated);
150 assert!(p.is_secure());
151 }
152
153 #[test]
154 fn signed_and_encrypted_is_confidential() {
155 let p = Posture::new(true, true);
156 assert_eq!(p.level(), PostureLevel::Confidential);
157 assert!(p.is_secure());
158 }
159
160 #[test]
161 fn encrypted_without_signed_degrades_to_open() {
162 let p = Posture::new(false, true);
164 assert_eq!(p.level(), PostureLevel::Open);
165 assert!(!p.is_secure());
166 }
167
168 #[test]
169 fn level_ordering_is_a_graduated_ladder() {
170 assert!(PostureLevel::Open < PostureLevel::Authenticated);
171 assert!(PostureLevel::Authenticated < PostureLevel::Confidential);
172 }
173
174 #[test]
175 fn from_posture_for_level() {
176 assert_eq!(
177 PostureLevel::from(Posture::new(true, false)),
178 PostureLevel::Authenticated
179 );
180 }
181
182 #[test]
183 fn posture_serde_round_trip() {
184 let p = Posture::new(true, false);
185 let json = serde_json::to_string(&p).unwrap();
186 assert_eq!(json, r#"{"signed":true,"encrypted":false}"#);
187 let back: Posture = serde_json::from_str(&json).unwrap();
188 assert_eq!(back, p);
189 }
190
191 #[test]
192 fn posture_level_serializes_snake_case() {
193 assert_eq!(
194 serde_json::to_string(&PostureLevel::Authenticated).unwrap(),
195 r#""authenticated""#
196 );
197 assert_eq!(
198 serde_json::to_string(&PostureLevel::Confidential).unwrap(),
199 r#""confidential""#
200 );
201 let back: PostureLevel = serde_json::from_str(r#""open""#).unwrap();
202 assert_eq!(back, PostureLevel::Open);
203 }
204
205 #[test]
206 fn as_wire_matches_serde_snake_case() {
207 for level in [
210 PostureLevel::Open,
211 PostureLevel::Authenticated,
212 PostureLevel::Confidential,
213 ] {
214 let serde = serde_json::to_string(&level).unwrap();
215 assert_eq!(format!("\"{}\"", level.as_wire()), serde);
216 }
217 }
218
219 #[test]
220 fn from_wire_round_trips_as_wire() {
221 for level in [
222 PostureLevel::Open,
223 PostureLevel::Authenticated,
224 PostureLevel::Confidential,
225 ] {
226 assert_eq!(PostureLevel::from_wire(level.as_wire()), Some(level));
227 }
228 assert_eq!(PostureLevel::from_wire("bogus"), None);
229 assert_eq!(PostureLevel::from_wire("Authenticated"), None); }
231
232 #[test]
233 fn to_posture_is_inverse_of_level() {
234 for posture in [
235 Posture::OPEN,
236 Posture::new(true, false),
237 Posture::new(true, true),
238 ] {
239 assert_eq!(posture.level().to_posture(), posture);
240 }
241 }
242}