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