Skip to main content

keygen_rs/
group.rs

1#[cfg(feature = "token")]
2use crate::client::Client;
3#[cfg(feature = "token")]
4use crate::errors::Error;
5#[cfg(feature = "token")]
6use crate::insert_optional;
7#[cfg(feature = "token")]
8use crate::license::{License, LicenseAttributes, PaginationOptions};
9#[cfg(feature = "token")]
10use crate::machine::{Machine, MachineAttributes};
11#[cfg(feature = "token")]
12use crate::user::{User, UserAttributes};
13use crate::KeygenResponseData;
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GroupAttributes {
20    pub name: String,
21    #[serde(rename = "maxUsers")]
22    pub max_users: Option<i32>,
23    #[serde(rename = "maxLicenses")]
24    pub max_licenses: Option<i32>,
25    #[serde(rename = "maxMachines")]
26    pub max_machines: Option<i32>,
27    pub metadata: Option<HashMap<String, serde_json::Value>>,
28    pub created: DateTime<Utc>,
29    pub updated: DateTime<Utc>,
30}
31
32#[cfg(feature = "token")]
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub(crate) struct GroupResponse {
35    pub data: KeygenResponseData<GroupAttributes>,
36}
37
38#[cfg(feature = "token")]
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub(crate) struct GroupsResponse {
41    pub data: Vec<KeygenResponseData<GroupAttributes>>,
42}
43
44#[cfg(feature = "token")]
45#[derive(Debug, Clone, Serialize, Deserialize)]
46struct GroupUsersResponse {
47    data: Vec<KeygenResponseData<UserAttributes>>,
48}
49
50#[cfg(feature = "token")]
51#[derive(Debug, Clone, Serialize, Deserialize)]
52struct GroupLicensesResponse {
53    data: Vec<KeygenResponseData<LicenseAttributes>>,
54}
55
56#[cfg(feature = "token")]
57#[derive(Debug, Clone, Serialize, Deserialize)]
58struct GroupMachinesResponse {
59    data: Vec<KeygenResponseData<MachineAttributes>>,
60}
61
62#[cfg(feature = "token")]
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CreateGroupRequest {
65    pub name: String,
66    #[serde(rename = "maxUsers")]
67    pub max_users: Option<i32>,
68    #[serde(rename = "maxLicenses")]
69    pub max_licenses: Option<i32>,
70    #[serde(rename = "maxMachines")]
71    pub max_machines: Option<i32>,
72    pub metadata: Option<HashMap<String, serde_json::Value>>,
73}
74
75#[cfg(feature = "token")]
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct ListGroupsOptions {
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub limit: Option<u32>,
80    #[serde(rename = "page[size]", skip_serializing_if = "Option::is_none")]
81    pub page_size: Option<u32>,
82    #[serde(rename = "page[number]", skip_serializing_if = "Option::is_none")]
83    pub page_number: Option<u32>,
84}
85
86#[cfg(feature = "token")]
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct UpdateGroupRequest {
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub name: Option<String>,
91    #[serde(rename = "maxUsers", skip_serializing_if = "Option::is_none")]
92    pub max_users: Option<i32>,
93    #[serde(rename = "maxLicenses", skip_serializing_if = "Option::is_none")]
94    pub max_licenses: Option<i32>,
95    #[serde(rename = "maxMachines", skip_serializing_if = "Option::is_none")]
96    pub max_machines: Option<i32>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub metadata: Option<HashMap<String, serde_json::Value>>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct Group {
103    pub id: String,
104    pub name: String,
105    pub max_users: Option<i32>,
106    pub max_licenses: Option<i32>,
107    pub max_machines: Option<i32>,
108    pub metadata: Option<HashMap<String, serde_json::Value>>,
109    pub created: DateTime<Utc>,
110    pub updated: DateTime<Utc>,
111    pub account_id: Option<String>,
112    pub owner_id: Option<String>,
113}
114
115impl Default for Group {
116    fn default() -> Self {
117        Self {
118            id: String::new(),
119            name: String::new(),
120            max_users: None,
121            max_licenses: None,
122            max_machines: None,
123            metadata: None,
124            created: Utc::now(),
125            updated: Utc::now(),
126            account_id: None,
127            owner_id: None,
128        }
129    }
130}
131
132impl Group {
133    #[allow(dead_code)]
134    pub(crate) fn from(data: KeygenResponseData<GroupAttributes>) -> Group {
135        Group {
136            id: data.id,
137            name: data.attributes.name,
138            max_users: data.attributes.max_users,
139            max_licenses: data.attributes.max_licenses,
140            max_machines: data.attributes.max_machines,
141            metadata: data.attributes.metadata,
142            created: data.attributes.created,
143            updated: data.attributes.updated,
144            account_id: data
145                .relationships
146                .account
147                .as_ref()
148                .and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
149            owner_id: data
150                .relationships
151                .owner
152                .as_ref()
153                .and_then(|o| o.data.as_ref().map(|d| d.id.clone())),
154        }
155    }
156
157    /// Create a new group
158    #[cfg(feature = "token")]
159    pub async fn create(request: CreateGroupRequest) -> Result<Group, Error> {
160        let client = Client::from_global_config()?;
161
162        let mut attributes = serde_json::Map::new();
163        attributes.insert("name".to_string(), serde_json::Value::String(request.name));
164
165        insert_optional(&mut attributes, "maxUsers", request.max_users)?;
166        insert_optional(&mut attributes, "maxLicenses", request.max_licenses)?;
167        insert_optional(&mut attributes, "maxMachines", request.max_machines)?;
168        insert_optional(&mut attributes, "metadata", request.metadata)?;
169
170        let body = serde_json::json!({
171            "data": {
172                "type": "groups",
173                "attributes": attributes
174            }
175        });
176
177        let response = client.post("groups", Some(&body), None::<&()>).await?;
178        let group_response: GroupResponse = serde_json::from_value(response.body)?;
179        Ok(Group::from(group_response.data))
180    }
181
182    /// List groups with optional pagination and filtering
183    #[cfg(feature = "token")]
184    pub async fn list(options: Option<ListGroupsOptions>) -> Result<Vec<Group>, Error> {
185        let client = Client::from_global_config()?;
186        let response = client.get("groups", options.as_ref()).await?;
187        let groups_response: GroupsResponse = serde_json::from_value(response.body)?;
188        Ok(groups_response.data.into_iter().map(Group::from).collect())
189    }
190
191    /// Get a group by ID
192    #[cfg(feature = "token")]
193    pub async fn get(id: &str) -> Result<Group, Error> {
194        let client = Client::from_global_config()?;
195        let endpoint = format!("groups/{id}");
196        let response = client.get(&endpoint, None::<&()>).await?;
197        let group_response: GroupResponse = serde_json::from_value(response.body)?;
198        Ok(Group::from(group_response.data))
199    }
200
201    /// Update a group
202    #[cfg(feature = "token")]
203    pub async fn update(&self, request: UpdateGroupRequest) -> Result<Group, Error> {
204        let client = Client::from_global_config()?;
205        let endpoint = format!("groups/{}", self.id);
206
207        let mut attributes = serde_json::Map::new();
208        insert_optional(&mut attributes, "name", request.name)?;
209        insert_optional(&mut attributes, "maxUsers", request.max_users)?;
210        insert_optional(&mut attributes, "maxLicenses", request.max_licenses)?;
211        insert_optional(&mut attributes, "maxMachines", request.max_machines)?;
212        insert_optional(&mut attributes, "metadata", request.metadata)?;
213
214        let body = serde_json::json!({
215            "data": {
216                "type": "groups",
217                "attributes": attributes
218            }
219        });
220
221        let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
222        let group_response: GroupResponse = serde_json::from_value(response.body)?;
223        Ok(Group::from(group_response.data))
224    }
225
226    /// Delete a group
227    #[cfg(feature = "token")]
228    pub async fn delete(&self) -> Result<(), Error> {
229        let client = Client::from_global_config()?;
230        let endpoint = format!("groups/{}", self.id);
231        client.delete::<(), ()>(&endpoint, None::<&()>).await?;
232        Ok(())
233    }
234
235    #[cfg(feature = "token")]
236    async fn list_related_users(
237        &self,
238        path: &str,
239        options: Option<&PaginationOptions>,
240    ) -> Result<Vec<User>, Error> {
241        let client = Client::from_global_config()?;
242        let response = client.get(path, options).await?;
243        let users_response: GroupUsersResponse = serde_json::from_value(response.body)?;
244        Ok(users_response.data.into_iter().map(User::from).collect())
245    }
246
247    #[cfg(feature = "token")]
248    async fn list_related_licenses(
249        &self,
250        path: &str,
251        options: Option<&PaginationOptions>,
252    ) -> Result<Vec<License>, Error> {
253        let client = Client::from_global_config()?;
254        let response = client.get(path, options).await?;
255        let licenses_response: GroupLicensesResponse = serde_json::from_value(response.body)?;
256        Ok(licenses_response
257            .data
258            .into_iter()
259            .map(License::from)
260            .collect())
261    }
262
263    #[cfg(feature = "token")]
264    async fn list_related_machines(
265        &self,
266        path: &str,
267        options: Option<&PaginationOptions>,
268    ) -> Result<Vec<Machine>, Error> {
269        let client = Client::from_global_config()?;
270        let response = client.get(path, options).await?;
271        let machines_response: GroupMachinesResponse = serde_json::from_value(response.body)?;
272        Ok(machines_response
273            .data
274            .into_iter()
275            .map(Machine::from)
276            .collect())
277    }
278
279    /// List group owners.
280    #[cfg(feature = "token")]
281    pub async fn owners(&self, options: Option<&PaginationOptions>) -> Result<Vec<User>, Error> {
282        self.list_related_users(&format!("groups/{}/owners", self.id), options)
283            .await
284    }
285
286    /// List group users.
287    #[cfg(feature = "token")]
288    pub async fn users(&self, options: Option<&PaginationOptions>) -> Result<Vec<User>, Error> {
289        self.list_related_users(&format!("groups/{}/users", self.id), options)
290            .await
291    }
292
293    /// List group licenses.
294    #[cfg(feature = "token")]
295    pub async fn licenses(
296        &self,
297        options: Option<&PaginationOptions>,
298    ) -> Result<Vec<License>, Error> {
299        self.list_related_licenses(&format!("groups/{}/licenses", self.id), options)
300            .await
301    }
302
303    /// List group machines.
304    #[cfg(feature = "token")]
305    pub async fn machines(
306        &self,
307        options: Option<&PaginationOptions>,
308    ) -> Result<Vec<Machine>, Error> {
309        self.list_related_machines(&format!("groups/{}/machines", self.id), options)
310            .await
311    }
312}
313
314#[cfg(all(test, feature = "token"))]
315mod tests {
316    use super::*;
317    use crate::{
318        KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
319    };
320
321    #[test]
322    fn test_group_relationships() {
323        let group_data = KeygenResponseData {
324            id: "test-group-id".to_string(),
325            r#type: "groups".to_string(),
326            attributes: GroupAttributes {
327                name: "Premium Team".to_string(),
328                max_users: Some(10),
329                max_licenses: Some(50),
330                max_machines: Some(100),
331                metadata: Some({
332                    let mut map = HashMap::new();
333                    map.insert(
334                        "tier".to_string(),
335                        serde_json::Value::String("premium".to_string()),
336                    );
337                    map
338                }),
339                created: "2023-01-01T00:00:00Z".parse().unwrap(),
340                updated: "2023-01-01T00:00:00Z".parse().unwrap(),
341            },
342            relationships: KeygenRelationships {
343                account: Some(KeygenRelationship {
344                    data: Some(KeygenRelationshipData {
345                        r#type: "accounts".to_string(),
346                        id: "test-account-id".to_string(),
347                    }),
348                    links: None,
349                }),
350                owner: Some(KeygenRelationship {
351                    data: Some(KeygenRelationshipData {
352                        r#type: "users".to_string(),
353                        id: "test-owner-id".to_string(),
354                    }),
355                    links: None,
356                }),
357                ..Default::default()
358            },
359        };
360
361        let group = Group::from(group_data);
362
363        assert_eq!(group.account_id, Some("test-account-id".to_string()));
364        assert_eq!(group.owner_id, Some("test-owner-id".to_string()));
365        assert_eq!(group.id, "test-group-id");
366        assert_eq!(group.name, "Premium Team");
367        assert_eq!(group.max_users, Some(10));
368        assert_eq!(group.max_licenses, Some(50));
369        assert_eq!(group.max_machines, Some(100));
370        assert!(group.metadata.is_some());
371    }
372
373    #[test]
374    fn test_group_without_relationships() {
375        let group_data = KeygenResponseData {
376            id: "test-group-id".to_string(),
377            r#type: "groups".to_string(),
378            attributes: GroupAttributes {
379                name: "Basic Group".to_string(),
380                max_users: None,
381                max_licenses: None,
382                max_machines: None,
383                metadata: None,
384                created: "2023-01-01T00:00:00Z".parse().unwrap(),
385                updated: "2023-01-01T00:00:00Z".parse().unwrap(),
386            },
387            relationships: KeygenRelationships::default(),
388        };
389
390        let group = Group::from(group_data);
391
392        assert_eq!(group.account_id, None);
393        assert_eq!(group.owner_id, None);
394        assert_eq!(group.name, "Basic Group");
395        assert_eq!(group.max_users, None);
396        assert_eq!(group.max_licenses, None);
397        assert_eq!(group.max_machines, None);
398        assert_eq!(group.metadata, None);
399    }
400
401    #[test]
402    fn test_create_group_request_serialization() {
403        let mut metadata = HashMap::new();
404        metadata.insert(
405            "department".to_string(),
406            serde_json::Value::String("engineering".to_string()),
407        );
408
409        let request = CreateGroupRequest {
410            name: "Engineering Team".to_string(),
411            max_users: Some(25),
412            max_licenses: Some(100),
413            max_machines: Some(200),
414            metadata: Some(metadata),
415        };
416
417        let serialized = serde_json::to_string(&request).unwrap();
418        assert!(serialized.contains("\"name\":\"Engineering Team\""));
419        assert!(serialized.contains("\"maxUsers\":25"));
420        assert!(serialized.contains("\"maxLicenses\":100"));
421        assert!(serialized.contains("\"maxMachines\":200"));
422        assert!(serialized.contains("\"metadata\""));
423    }
424
425    #[test]
426    fn test_update_group_request_serialization() {
427        let request = UpdateGroupRequest {
428            name: Some("Updated Team Name".to_string()),
429            max_users: Some(30),
430            max_licenses: None,
431            max_machines: Some(250),
432            metadata: None,
433        };
434
435        let serialized = serde_json::to_string(&request).unwrap();
436        assert!(serialized.contains("\"name\":\"Updated Team Name\""));
437        assert!(serialized.contains("\"maxUsers\":30"));
438        assert!(serialized.contains("\"maxMachines\":250"));
439        // Should not contain null fields
440        assert!(!serialized.contains("\"maxLicenses\":null"));
441        assert!(!serialized.contains("\"metadata\":null"));
442    }
443
444    #[test]
445    fn test_list_groups_options_serialization() {
446        let options = ListGroupsOptions {
447            limit: Some(20),
448            page_size: Some(10),
449            page_number: Some(3),
450        };
451
452        let serialized = serde_json::to_string(&options).unwrap();
453        assert!(serialized.contains("\"limit\":20"));
454        assert!(serialized.contains("\"page[size]\":10"));
455        assert!(serialized.contains("\"page[number]\":3"));
456    }
457
458    #[test]
459    fn test_group_attributes_serde() {
460        // Test serialization/deserialization of GroupAttributes
461        let attributes = GroupAttributes {
462            name: "Test Group".to_string(),
463            max_users: Some(15),
464            max_licenses: Some(75),
465            max_machines: Some(150),
466            metadata: Some({
467                let mut map = HashMap::new();
468                map.insert(
469                    "region".to_string(),
470                    serde_json::Value::String("us-east".to_string()),
471                );
472                map
473            }),
474            created: "2023-01-01T00:00:00Z".parse().unwrap(),
475            updated: "2023-01-02T00:00:00Z".parse().unwrap(),
476        };
477
478        let serialized = serde_json::to_string(&attributes).unwrap();
479        let deserialized: GroupAttributes = serde_json::from_str(&serialized).unwrap();
480
481        assert_eq!(deserialized.name, attributes.name);
482        assert_eq!(deserialized.max_users, attributes.max_users);
483        assert_eq!(deserialized.max_licenses, attributes.max_licenses);
484        assert_eq!(deserialized.max_machines, attributes.max_machines);
485        assert_eq!(deserialized.metadata, attributes.metadata);
486    }
487
488    #[test]
489    fn test_group_default() {
490        let default_group = Group::default();
491
492        assert_eq!(default_group.id, "");
493        assert_eq!(default_group.name, "");
494        assert_eq!(default_group.max_users, None);
495        assert_eq!(default_group.max_licenses, None);
496        assert_eq!(default_group.max_machines, None);
497        assert_eq!(default_group.metadata, None);
498        assert_eq!(default_group.account_id, None);
499        assert_eq!(default_group.owner_id, None);
500        // created and updated should be set to current time
501        assert!(default_group.created <= Utc::now());
502        assert!(default_group.updated <= Utc::now());
503    }
504
505    #[test]
506    fn test_group_default_in_struct_syntax() {
507        // Test that Default works with struct update syntax (useful for license_file.rs)
508        let group = Group {
509            id: "test-id".to_string(),
510            name: "Test Group".to_string(),
511            max_users: Some(5),
512            ..Default::default()
513        };
514
515        assert_eq!(group.id, "test-id");
516        assert_eq!(group.name, "Test Group");
517        assert_eq!(group.max_users, Some(5));
518        assert_eq!(group.max_licenses, None);
519        assert_eq!(group.max_machines, None);
520        assert_eq!(group.metadata, None);
521        assert_eq!(group.account_id, None);
522        assert_eq!(group.owner_id, None);
523    }
524}