dinio_sdk/models/
client_identity.rs1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ClientIdentity {
13 pub uuid: Uuid,
15 pub id_type: String,
18 pub identify: String,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub name: Option<String>,
26 pub user_id: Uuid,
28 #[serde(default = "default_true")]
30 pub enabled: bool,
31 #[serde(default)]
34 pub suspended: bool,
35 #[serde(default)]
37 pub owner_suspended: bool,
38 pub created_at: i64,
40 pub updated_at: i64,
42}
43
44fn default_true() -> bool {
45 true
46}
47
48impl ClientIdentity {
49 pub fn kind(&self) -> Option<ClientIdentityType> {
51 self.id_type.parse().ok()
52 }
53
54 pub fn is_suspended(&self) -> bool {
57 self.suspended || self.owner_suspended
58 }
59
60 pub fn label(&self) -> &str {
62 self.name.as_deref().unwrap_or(&self.identify)
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub enum ClientIdentityType {
70 #[serde(rename = "IP")]
72 Ip,
73 Hostname,
76 Path,
79 None,
81}
82
83impl ClientIdentityType {
84 pub fn generates_identify(self) -> bool {
87 matches!(self, Self::Hostname | Self::Path)
88 }
89
90 pub fn as_wire(self) -> &'static str {
92 match self {
93 Self::Ip => "IP",
94 Self::Hostname => "Hostname",
95 Self::Path => "Path",
96 Self::None => "None",
97 }
98 }
99}
100
101impl fmt::Display for ClientIdentityType {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.write_str(self.as_wire())
104 }
105}
106
107impl FromStr for ClientIdentityType {
108 type Err = Error;
109
110 fn from_str(text: &str) -> Result<Self> {
112 match text.trim().to_ascii_lowercase().as_str() {
113 "ip" => Ok(Self::Ip),
114 "hostname" => Ok(Self::Hostname),
115 "path" => Ok(Self::Path),
116 "none" => Ok(Self::None),
117 _ => Err(Error::InvalidInput(format!(
118 "unknown client identity type {text:?} (IP, Hostname or Path)"
119 ))),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize)]
126pub struct NewClientIdentity {
127 #[serde(skip_serializing_if = "Option::is_none")]
131 pub identify: Option<String>,
132 pub id_type: ClientIdentityType,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub name: Option<String>,
138}
139
140#[derive(Debug, Clone, Default, Serialize)]
147pub struct ClientIdentityPatch {
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub identify: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub id_type: Option<ClientIdentityType>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub name: Option<Option<String>>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub enabled: Option<bool>,
160}
161
162impl ClientIdentityPatch {
163 pub fn is_empty(&self) -> bool {
165 self.identify.is_none()
166 && self.id_type.is_none()
167 && self.name.is_none()
168 && self.enabled.is_none()
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn wire_labels() {
178 let new = NewClientIdentity {
179 identify: Some("203.0.113.10".into()),
180 id_type: ClientIdentityType::Ip,
181 name: None,
182 };
183 assert_eq!(
184 serde_json::to_value(&new).unwrap(),
185 serde_json::json!({ "identify": "203.0.113.10", "id_type": "IP" })
186 );
187 let generated = NewClientIdentity {
188 identify: None,
189 id_type: ClientIdentityType::Path,
190 name: Some("phone".into()),
191 };
192 assert_eq!(
193 serde_json::to_value(&generated).unwrap(),
194 serde_json::json!({ "id_type": "Path", "name": "phone" })
195 );
196 assert_eq!(
197 "ip".parse::<ClientIdentityType>().unwrap(),
198 ClientIdentityType::Ip
199 );
200 assert_eq!(
201 "PATH".parse::<ClientIdentityType>().unwrap(),
202 ClientIdentityType::Path
203 );
204 assert!("dot".parse::<ClientIdentityType>().is_err());
205 }
206
207 #[test]
208 fn unknown_type_deserializes() {
209 let ci: ClientIdentity = serde_json::from_value(serde_json::json!({
210 "uuid": "00000000-0000-0000-0000-000000000000",
211 "id_type": "Mac", "identify": "aa:bb",
212 "user_id": "00000000-0000-0000-0000-000000000001",
213 "enabled": true, "suspended": false, "owner_suspended": true,
214 "created_at": 1, "updated_at": 1
215 }))
216 .unwrap();
217 assert_eq!(ci.kind(), None);
218 assert!(ci.is_suspended());
219 assert_eq!(ci.label(), "aa:bb");
220 }
221
222 #[test]
223 fn patch_clears_name_with_null() {
224 let patch = ClientIdentityPatch {
225 name: Some(None),
226 enabled: Some(false),
227 ..Default::default()
228 };
229 assert!(!patch.is_empty());
230 assert_eq!(
231 serde_json::to_value(&patch).unwrap(),
232 serde_json::json!({ "name": null, "enabled": false })
233 );
234 }
235}