Skip to main content

keygen_rs/
component.rs

1#[cfg(feature = "token")]
2use crate::client::Client;
3#[cfg(feature = "token")]
4use crate::errors::Error;
5use crate::KeygenResponseData;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ComponentAttributes {
13    pub fingerprint: String,
14    pub name: String,
15    pub metadata: Option<HashMap<String, Value>>,
16    pub created: DateTime<Utc>,
17    pub updated: DateTime<Utc>,
18}
19
20#[cfg(feature = "token")]
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub(crate) struct ComponentResponse {
23    pub data: KeygenResponseData<ComponentAttributes>,
24}
25
26#[cfg(feature = "token")]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub(crate) struct ComponentsResponse {
29    pub data: Vec<KeygenResponseData<ComponentAttributes>>,
30}
31
32#[cfg(feature = "token")]
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct CreateComponentRequest {
35    pub fingerprint: String,
36    pub name: String,
37    pub metadata: Option<HashMap<String, Value>>,
38    pub machine_id: String, // Required according to API docs
39}
40
41#[cfg(feature = "token")]
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct ListComponentsOptions {
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub limit: Option<u32>,
46    #[serde(rename = "page[size]", skip_serializing_if = "Option::is_none")]
47    pub page_size: Option<u32>,
48    #[serde(rename = "page[number]", skip_serializing_if = "Option::is_none")]
49    pub page_number: Option<u32>,
50    // Filters as per API documentation
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub machine: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub license: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub owner: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub user: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub product: Option<String>,
61}
62
63#[cfg(feature = "token")]
64#[derive(Debug, Clone, Serialize, Deserialize, Default)]
65pub struct UpdateComponentRequest {
66    pub name: Option<String>, // Only name and metadata are updatable
67    pub metadata: Option<HashMap<String, Value>>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Component {
72    pub id: String,
73    pub fingerprint: String,
74    pub name: String,
75    pub metadata: Option<HashMap<String, Value>>,
76    pub created: DateTime<Utc>,
77    pub updated: DateTime<Utc>,
78    // Relationships as per API documentation
79    pub account_id: Option<String>,
80    pub environment_id: Option<String>,
81    pub product_id: Option<String>,
82    pub license_id: Option<String>,
83    pub machine_id: Option<String>,
84}
85
86impl Default for Component {
87    fn default() -> Self {
88        let now = Utc::now();
89        Self {
90            id: String::new(),
91            fingerprint: String::new(),
92            name: String::new(),
93            metadata: None,
94            created: now,
95            updated: now,
96            account_id: None,
97            environment_id: None,
98            product_id: None,
99            license_id: None,
100            machine_id: None,
101        }
102    }
103}
104
105impl Component {
106    #[allow(dead_code)]
107    pub(crate) fn from(data: KeygenResponseData<ComponentAttributes>) -> Component {
108        Component {
109            id: data.id,
110            fingerprint: data.attributes.fingerprint,
111            name: data.attributes.name,
112            metadata: data.attributes.metadata,
113            created: data.attributes.created,
114            updated: data.attributes.updated,
115            account_id: data
116                .relationships
117                .account
118                .as_ref()
119                .and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
120            environment_id: data
121                .relationships
122                .environment
123                .as_ref()
124                .and_then(|e| e.data.as_ref().map(|d| d.id.clone())),
125            product_id: data
126                .relationships
127                .product
128                .as_ref()
129                .and_then(|p| p.data.as_ref().map(|d| d.id.clone())),
130            license_id: data
131                .relationships
132                .license
133                .as_ref()
134                .and_then(|l| l.data.as_ref().map(|d| d.id.clone())),
135            machine_id: data
136                .relationships
137                .machines
138                .as_ref()
139                .and_then(|m| m.data.as_ref().map(|d| d.id.clone())),
140        }
141    }
142
143    /// Create a simple component object for legacy compatibility
144    pub fn create_object(component: &Component) -> serde_json::Value {
145        json!({
146          "data": {
147            "id": component.id,
148            "type": "components",
149            "attributes": {
150                "fingerprint": component.fingerprint,
151                "name": component.name,
152                "metadata": component.metadata
153            }
154          }
155        })
156    }
157
158    /// Create a new component
159    #[cfg(feature = "token")]
160    pub async fn create(request: CreateComponentRequest) -> Result<Component, Error> {
161        let client = Client::from_global_config()?;
162
163        let mut attributes = serde_json::Map::new();
164        attributes.insert(
165            "fingerprint".to_string(),
166            Value::String(request.fingerprint),
167        );
168        attributes.insert("name".to_string(), Value::String(request.name));
169
170        if let Some(metadata) = request.metadata {
171            attributes.insert("metadata".to_string(), serde_json::to_value(metadata)?);
172        }
173
174        let body = json!({
175            "data": {
176                "type": "components",
177                "attributes": attributes,
178                "relationships": {
179                    "machine": {
180                        "data": {
181                            "type": "machines",
182                            "id": request.machine_id
183                        }
184                    }
185                }
186            }
187        });
188
189        let response = client.post("components", Some(&body), None::<&()>).await?;
190        let component_response: ComponentResponse = serde_json::from_value(response.body)?;
191        Ok(Component::from(component_response.data))
192    }
193
194    /// List all components with optional filtering
195    #[cfg(feature = "token")]
196    pub async fn list(options: Option<ListComponentsOptions>) -> Result<Vec<Component>, Error> {
197        let client = Client::from_global_config()?;
198        let mut query_params = HashMap::new();
199
200        if let Some(opts) = options {
201            if let Some(limit) = opts.limit {
202                query_params.insert("limit".to_string(), limit.to_string());
203            }
204            if let Some(page_size) = opts.page_size {
205                query_params.insert("page[size]".to_string(), page_size.to_string());
206            }
207            if let Some(page_number) = opts.page_number {
208                query_params.insert("page[number]".to_string(), page_number.to_string());
209            }
210            // API documented filters
211            if let Some(machine) = opts.machine {
212                query_params.insert("machine".to_string(), machine);
213            }
214            if let Some(license) = opts.license {
215                query_params.insert("license".to_string(), license);
216            }
217            if let Some(owner) = opts.owner {
218                query_params.insert("owner".to_string(), owner);
219            }
220            if let Some(user) = opts.user {
221                query_params.insert("user".to_string(), user);
222            }
223            if let Some(product) = opts.product {
224                query_params.insert("product".to_string(), product);
225            }
226        }
227
228        let query = if query_params.is_empty() {
229            None
230        } else {
231            Some(query_params)
232        };
233
234        let response = client.get("components", query.as_ref()).await?;
235        let components_response: ComponentsResponse = serde_json::from_value(response.body)?;
236        Ok(components_response
237            .data
238            .into_iter()
239            .map(Component::from)
240            .collect())
241    }
242
243    /// Get a component by ID
244    #[cfg(feature = "token")]
245    pub async fn get(id: &str) -> Result<Component, Error> {
246        let client = Client::from_global_config()?;
247        let endpoint = format!("components/{id}");
248        let response = client.get(&endpoint, None::<&()>).await?;
249        let component_response: ComponentResponse = serde_json::from_value(response.body)?;
250        Ok(Component::from(component_response.data))
251    }
252
253    /// Update a component (only name and metadata are updatable per API docs)
254    #[cfg(feature = "token")]
255    pub async fn update(&self, request: UpdateComponentRequest) -> Result<Component, Error> {
256        let client = Client::from_global_config()?;
257        let endpoint = format!("components/{}", self.id);
258
259        let mut attributes = serde_json::Map::new();
260        if let Some(name) = request.name {
261            attributes.insert("name".to_string(), Value::String(name));
262        }
263        if let Some(metadata) = request.metadata {
264            attributes.insert("metadata".to_string(), serde_json::to_value(metadata)?);
265        }
266
267        let body = json!({
268            "data": {
269                "type": "components",
270                "attributes": attributes
271            }
272        });
273
274        let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
275        let component_response: ComponentResponse = serde_json::from_value(response.body)?;
276        Ok(Component::from(component_response.data))
277    }
278
279    /// Delete a component
280    #[cfg(feature = "token")]
281    pub async fn delete(&self) -> Result<(), Error> {
282        let client = Client::from_global_config()?;
283        let endpoint = format!("components/{}", self.id);
284        client.delete::<(), ()>(&endpoint, None::<&()>).await?;
285        Ok(())
286    }
287}
288
289// Convenience implementations for request builders
290#[cfg(feature = "token")]
291impl CreateComponentRequest {
292    /// Create a new component creation request (machine_id is required)
293    pub fn new(fingerprint: String, name: String, machine_id: String) -> Self {
294        Self {
295            fingerprint,
296            name,
297            metadata: None,
298            machine_id,
299        }
300    }
301
302    /// Set metadata for the component
303    pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
304        self.metadata = Some(metadata);
305        self
306    }
307}
308
309#[cfg(feature = "token")]
310impl UpdateComponentRequest {
311    /// Create a new empty component update request
312    pub fn new() -> Self {
313        Self {
314            name: None,
315            metadata: None,
316        }
317    }
318
319    /// Set the component name
320    pub fn with_name(mut self, name: String) -> Self {
321        self.name = Some(name);
322        self
323    }
324
325    /// Set the component metadata
326    pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
327        self.metadata = Some(metadata);
328        self
329    }
330}
331
332#[cfg(feature = "token")]
333impl ListComponentsOptions {
334    /// Create new list options
335    pub fn new() -> Self {
336        Self::default()
337    }
338
339    /// Set the limit for number of results (1-100)
340    pub fn with_limit(mut self, limit: u32) -> Self {
341        self.limit = Some(limit);
342        self
343    }
344
345    /// Set pagination options
346    pub fn with_pagination(mut self, page_number: u32, page_size: u32) -> Self {
347        self.page_number = Some(page_number);
348        self.page_size = Some(page_size);
349        self
350    }
351
352    /// Filter by machine ID
353    pub fn with_machine(mut self, machine: String) -> Self {
354        self.machine = Some(machine);
355        self
356    }
357
358    /// Filter by license ID
359    pub fn with_license(mut self, license: String) -> Self {
360        self.license = Some(license);
361        self
362    }
363
364    /// Filter by owner ID
365    pub fn with_owner(mut self, owner: String) -> Self {
366        self.owner = Some(owner);
367        self
368    }
369
370    /// Filter by user ID
371    pub fn with_user(mut self, user: String) -> Self {
372        self.user = Some(user);
373        self
374    }
375
376    /// Filter by product ID
377    pub fn with_product(mut self, product: String) -> Self {
378        self.product = Some(product);
379        self
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use crate::{
387        KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
388    };
389    use chrono::Utc;
390
391    #[test]
392    fn test_component_from_data() {
393        let component_data = KeygenResponseData {
394            id: "test-component-id".to_string(),
395            r#type: "components".to_string(),
396            attributes: ComponentAttributes {
397                fingerprint: "test-fingerprint".to_string(),
398                name: "Test Component".to_string(),
399                metadata: Some({
400                    let mut map = HashMap::new();
401                    map.insert("version".to_string(), Value::String("1.0.0".to_string()));
402                    map
403                }),
404                created: Utc::now(),
405                updated: Utc::now(),
406            },
407            relationships: KeygenRelationships {
408                account: Some(KeygenRelationship {
409                    data: Some(KeygenRelationshipData {
410                        r#type: "accounts".to_string(),
411                        id: "test-account-id".to_string(),
412                    }),
413                    links: None,
414                }),
415                environment: Some(KeygenRelationship {
416                    data: Some(KeygenRelationshipData {
417                        r#type: "environments".to_string(),
418                        id: "test-environment-id".to_string(),
419                    }),
420                    links: None,
421                }),
422                product: Some(KeygenRelationship {
423                    data: Some(KeygenRelationshipData {
424                        r#type: "products".to_string(),
425                        id: "test-product-id".to_string(),
426                    }),
427                    links: None,
428                }),
429                license: Some(KeygenRelationship {
430                    data: Some(KeygenRelationshipData {
431                        r#type: "licenses".to_string(),
432                        id: "test-license-id".to_string(),
433                    }),
434                    links: None,
435                }),
436                machines: Some(KeygenRelationship {
437                    data: Some(KeygenRelationshipData {
438                        r#type: "machines".to_string(),
439                        id: "test-machine-id".to_string(),
440                    }),
441                    links: None,
442                }),
443                ..Default::default()
444            },
445        };
446
447        let component = Component::from(component_data);
448
449        assert_eq!(component.id, "test-component-id");
450        assert_eq!(component.fingerprint, "test-fingerprint");
451        assert_eq!(component.name, "Test Component");
452        assert_eq!(component.account_id, Some("test-account-id".to_string()));
453        assert_eq!(
454            component.environment_id,
455            Some("test-environment-id".to_string())
456        );
457        assert_eq!(component.product_id, Some("test-product-id".to_string()));
458        assert_eq!(component.license_id, Some("test-license-id".to_string()));
459        assert_eq!(component.machine_id, Some("test-machine-id".to_string()));
460        assert!(component.metadata.is_some());
461        let metadata = component.metadata.unwrap();
462        assert_eq!(metadata.get("version").unwrap().as_str().unwrap(), "1.0.0");
463    }
464
465    #[test]
466    fn test_component_without_relationships() {
467        let component_data = KeygenResponseData {
468            id: "test-component-id".to_string(),
469            r#type: "components".to_string(),
470            attributes: ComponentAttributes {
471                fingerprint: "test-fingerprint".to_string(),
472                name: "Test Component".to_string(),
473                metadata: None,
474                created: Utc::now(),
475                updated: Utc::now(),
476            },
477            relationships: KeygenRelationships::default(),
478        };
479
480        let component = Component::from(component_data);
481
482        assert_eq!(component.account_id, None);
483        assert_eq!(component.environment_id, None);
484        assert_eq!(component.product_id, None);
485        assert_eq!(component.license_id, None);
486        assert_eq!(component.machine_id, None);
487        assert!(component.metadata.is_none());
488    }
489
490    #[cfg(feature = "token")]
491    #[test]
492    fn test_create_component_request_builder() {
493        let mut metadata = HashMap::new();
494        metadata.insert("version".to_string(), Value::String("1.0.0".to_string()));
495
496        let request = CreateComponentRequest::new(
497            "test-fingerprint".to_string(),
498            "Test Component".to_string(),
499            "test-machine-id".to_string(),
500        )
501        .with_metadata(metadata.clone());
502
503        assert_eq!(request.fingerprint, "test-fingerprint");
504        assert_eq!(request.name, "Test Component");
505        assert_eq!(request.machine_id, "test-machine-id");
506        assert_eq!(request.metadata, Some(metadata));
507    }
508
509    #[cfg(feature = "token")]
510    #[test]
511    fn test_update_component_request_builder() {
512        let mut metadata = HashMap::new();
513        metadata.insert("version".to_string(), Value::String("2.0.0".to_string()));
514
515        let request = UpdateComponentRequest::new()
516            .with_name("Updated Component".to_string())
517            .with_metadata(metadata.clone());
518
519        assert_eq!(request.name, Some("Updated Component".to_string()));
520        assert_eq!(request.metadata, Some(metadata));
521    }
522
523    #[cfg(feature = "token")]
524    #[test]
525    fn test_list_components_options_builder() {
526        let options = ListComponentsOptions::new()
527            .with_limit(50)
528            .with_pagination(2, 25)
529            .with_machine("test-machine-id".to_string())
530            .with_license("test-license-id".to_string())
531            .with_owner("test-owner-id".to_string())
532            .with_user("test-user-id".to_string())
533            .with_product("test-product-id".to_string());
534
535        assert_eq!(options.limit, Some(50));
536        assert_eq!(options.page_number, Some(2));
537        assert_eq!(options.page_size, Some(25));
538        assert_eq!(options.machine, Some("test-machine-id".to_string()));
539        assert_eq!(options.license, Some("test-license-id".to_string()));
540        assert_eq!(options.owner, Some("test-owner-id".to_string()));
541        assert_eq!(options.user, Some("test-user-id".to_string()));
542        assert_eq!(options.product, Some("test-product-id".to_string()));
543    }
544
545    #[test]
546    fn test_component_default() {
547        let component = Component::default();
548
549        assert!(component.id.is_empty());
550        assert!(component.fingerprint.is_empty());
551        assert!(component.name.is_empty());
552        assert!(component.metadata.is_none());
553        assert!(component.account_id.is_none());
554        assert!(component.environment_id.is_none());
555        assert!(component.product_id.is_none());
556        assert!(component.license_id.is_none());
557        assert!(component.machine_id.is_none());
558        // created and updated should be set to now
559        assert!(component.created <= Utc::now());
560        assert!(component.updated <= Utc::now());
561    }
562
563    #[test]
564    fn test_create_object_legacy_compatibility() {
565        let component = Component {
566            id: "test-id".to_string(),
567            fingerprint: "test-fingerprint".to_string(),
568            name: "Test Component".to_string(),
569            metadata: Some({
570                let mut map = HashMap::new();
571                map.insert("version".to_string(), Value::String("1.0.0".to_string()));
572                map
573            }),
574            ..Default::default()
575        };
576
577        let object = Component::create_object(&component);
578
579        assert!(object["data"]["id"].is_string());
580        assert_eq!(object["data"]["type"], "components");
581        assert_eq!(
582            object["data"]["attributes"]["fingerprint"],
583            "test-fingerprint"
584        );
585        assert_eq!(object["data"]["attributes"]["name"], "Test Component");
586    }
587}