Skip to main content

uptrakit_openapi_client/
software_items.rs

1use crate::Result;
2use crate::UptrakitClient;
3use crate::types_impl::batch_actions::{BatchActionRequest, BatchActionResponse};
4use crate::types_impl::pagination::{PaginatedResponse, PaginationParams};
5use crate::types_impl::software_items::{
6    AssignHostsRequest, CreateSoftwareItemRequest, ListSoftwareItemsParams,
7    MergeSoftwareItemsExecuteRequest, MergeSoftwareItemsExecuteResponse,
8    MergeSoftwareItemsPreviewRequest, MergeSoftwareItemsPreviewResponse,
9    SoftwareItemDetailResponse, SoftwareItemResponse, TriggerUpdateRequest, TriggerUpdateResponse,
10    TriggerVersionCheckResponse, UpdateHostAssignmentRequest, UpdateSoftwareItemRequest,
11};
12use uuid::Uuid;
13
14impl UptrakitClient {
15    /// List software items with pagination and optional discovery state filter.
16    pub async fn list_software_items(
17        &self,
18        params: &ListSoftwareItemsParams,
19    ) -> Result<PaginatedResponse<SoftwareItemResponse>> {
20        self.get_with_query(crate::paths::software_items::BASE, params)
21            .await
22    }
23
24    /// Preview a manual merge of software items.
25    pub async fn preview_software_item_merge(
26        &self,
27        req: &MergeSoftwareItemsPreviewRequest,
28    ) -> Result<MergeSoftwareItemsPreviewResponse> {
29        self.post_json(crate::paths::software_items::MERGE_PREVIEW, req)
30            .await
31    }
32
33    /// Execute a manual merge of software items.
34    pub async fn execute_software_item_merge(
35        &self,
36        req: &MergeSoftwareItemsExecuteRequest,
37    ) -> Result<MergeSoftwareItemsExecuteResponse> {
38        self.post_json(crate::paths::software_items::MERGE_EXECUTE, req)
39            .await
40    }
41
42    /// Fetch all software items across all pages.
43    ///
44    /// Automatically iterates through every page at [`MAX_PER_PAGE`] items per
45    /// request. Use [`list_software_items`] for manual pagination control.
46    ///
47    /// [`MAX_PER_PAGE`]: uptrakit_web_api_types::pagination::MAX_PER_PAGE
48    /// [`list_software_items`]: Self::list_software_items
49    pub async fn list_all_software_items(&self) -> Result<Vec<SoftwareItemResponse>> {
50        let base = PaginationParams {
51            page: None,
52            per_page: None,
53        };
54        self.fetch_all_pages(crate::paths::software_items::BASE, &base)
55            .await
56    }
57
58    /// Get a single software item by ID (detailed view with host info).
59    pub async fn get_software_item(&self, id: &Uuid) -> Result<SoftwareItemDetailResponse> {
60        self.get(&crate::paths::software_items::by_id(id)).await
61    }
62
63    /// Create a new software item (catalog entry — name and enabled flag only).
64    pub async fn create_software_item(
65        &self,
66        req: &CreateSoftwareItemRequest,
67    ) -> Result<SoftwareItemResponse> {
68        self.post_json(crate::paths::software_items::BASE, req)
69            .await
70    }
71
72    /// Update an existing software item (name and/or enabled flag).
73    pub async fn update_software_item(
74        &self,
75        id: &Uuid,
76        req: &UpdateSoftwareItemRequest,
77    ) -> Result<SoftwareItemResponse> {
78        self.put_json(&crate::paths::software_items::by_id(id), req)
79            .await
80    }
81
82    /// Delete a software item.
83    pub async fn delete_software_item(&self, id: &Uuid) -> Result<()> {
84        self.delete(&crate::paths::software_items::by_id(id)).await
85    }
86
87    /// Assign hosts to a software item.
88    ///
89    /// Each host assignment carries its own `plugin_config_id`, `package_identifier`,
90    /// and optional `config_override`.
91    pub async fn assign_hosts(
92        &self,
93        id: &Uuid,
94        req: &AssignHostsRequest,
95    ) -> Result<SoftwareItemDetailResponse> {
96        self.post_json(&crate::paths::software_items::hosts(id), req)
97            .await
98    }
99
100    /// Unassign a host from a software item.
101    pub async fn unassign_host(&self, item_id: &Uuid, host_id: &Uuid) -> Result<()> {
102        self.delete(&crate::paths::software_items::host(item_id, host_id))
103            .await
104    }
105
106    /// Unassign a host from a software item and create an autodiscovery ignore rule.
107    ///
108    /// Equivalent to `DELETE .../hosts/{host_id}?ignore=true`. The ignore rule is created for
109    /// the `(plugin_config_id, package_identifier)` pair stored on the host assignment,
110    /// preventing re-discovery of that package on any host in the future.
111    pub async fn unassign_host_with_ignore(&self, item_id: &Uuid, host_id: &Uuid) -> Result<()> {
112        #[derive(serde::Serialize)]
113        struct IgnoreQuery {
114            ignore: bool,
115        }
116        self.delete_with_query(
117            &crate::paths::software_items::host(item_id, host_id),
118            &IgnoreQuery { ignore: true },
119        )
120        .await
121    }
122
123    /// Update the plugin assignment for a specific host–software-item link.
124    pub async fn update_host_assignment(
125        &self,
126        item_id: &Uuid,
127        host_id: &Uuid,
128        req: &UpdateHostAssignmentRequest,
129    ) -> Result<SoftwareItemDetailResponse> {
130        self.put_json(&crate::paths::software_items::host(item_id, host_id), req)
131            .await
132    }
133
134    /// Remove a specific plugin assignment by role and ordinal.
135    pub async fn delete_plugin_assignment(
136        &self,
137        item_id: &Uuid,
138        host_id: &Uuid,
139        role: &str,
140        ordinal: i32,
141    ) -> Result<SoftwareItemDetailResponse> {
142        self.delete_json(&crate::paths::software_items::host_plugin_assignment(
143            item_id, host_id, role, ordinal,
144        ))
145        .await
146    }
147
148    /// Trigger a version check for a software item across all assigned hosts.
149    pub async fn check_versions(&self, item_id: &Uuid) -> Result<TriggerVersionCheckResponse> {
150        self.post_empty(&crate::paths::software_items::check_versions(item_id))
151            .await
152    }
153
154    /// Trigger a version check for a software item on a specific host.
155    pub async fn check_versions_host(
156        &self,
157        item_id: &Uuid,
158        host_id: &Uuid,
159    ) -> Result<TriggerVersionCheckResponse> {
160        self.post_empty(&crate::paths::software_items::host_check_versions(
161            item_id, host_id,
162        ))
163        .await
164    }
165
166    /// Trigger an update for a software item on a specific host.
167    pub async fn trigger_update(
168        &self,
169        item_id: &Uuid,
170        host_id: &Uuid,
171        req: &TriggerUpdateRequest,
172    ) -> Result<TriggerUpdateResponse> {
173        self.post_json(
174            &crate::paths::software_items::host_update(item_id, host_id),
175            req,
176        )
177        .await
178    }
179
180    /// Perform a batch action on multiple software items.
181    ///
182    /// Supported actions: `approve`, `delete`.
183    pub async fn batch_software_items(
184        &self,
185        req: &BatchActionRequest,
186    ) -> Result<BatchActionResponse> {
187        self.post_json(crate::paths::software_items::BATCH, req)
188            .await
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use crate::shared_types_impl::PluginRole;
195    use crate::types_impl::software_items::{
196        AssignHostsRequest, CreateSoftwareItemRequest, HostPluginRoleAssignment,
197        HostSoftwareAssignment, ReleaseInfoRequest, TriggerUpdateRequest,
198        UpdateHostAssignmentRequest, UpdateSoftwareItemRequest,
199    };
200    use uuid::Uuid;
201
202    #[test]
203    fn create_software_item_request_serialization() {
204        let req = CreateSoftwareItemRequest {
205            name: "Node.js".to_string(),
206            featured: true,
207            icon_url: None,
208        };
209        let json = serde_json::to_value(&req).expect("serialize");
210        assert_eq!(json["name"], "Node.js");
211        assert_eq!(json["featured"], true);
212        // plugin fields must NOT appear in the serialized form
213        assert!(json.get("provider_config_id").is_none());
214        assert!(json.get("package_identifier").is_none());
215    }
216
217    #[test]
218    fn update_software_item_request_serialization() {
219        use crate::types_impl::software_items::IconUrlPatch;
220        let req = UpdateSoftwareItemRequest {
221            name: Some("Node.js LTS".to_string()),
222            featured: Some(false),
223            icon_url: IconUrlPatch::Keep,
224        };
225        let json = serde_json::to_value(&req).expect("serialize");
226        assert_eq!(json["name"], "Node.js LTS");
227        assert_eq!(json["featured"], false);
228        // plugin fields must NOT appear
229        assert!(json.get("package_identifier").is_none());
230        assert!(json.get("config_override").is_none());
231    }
232
233    #[test]
234    fn assign_hosts_request_serialization() {
235        let pc_id = Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6").expect("valid uuid");
236        let host1 = Uuid::parse_str("11111111-1111-1111-1111-111111111111").expect("valid uuid");
237        let host2 = Uuid::parse_str("22222222-2222-2222-2222-222222222222").expect("valid uuid");
238
239        let req = AssignHostsRequest {
240            host_assignments: vec![
241                HostSoftwareAssignment {
242                    host_id: host1,
243                    plugins: vec![HostPluginRoleAssignment {
244                        role: PluginRole::DetectVersion,
245                        ordinal: 0,
246                        plugin_config_id: Some(pc_id),
247                        plugin_config: None,
248                        package_identifier: "nodejs/node".to_string(),
249                        config_override: None,
250                        execution_site: "auto".to_string(),
251                    }],
252                },
253                HostSoftwareAssignment {
254                    host_id: host2,
255                    plugins: vec![HostPluginRoleAssignment {
256                        role: PluginRole::DetectVersion,
257                        ordinal: 0,
258                        plugin_config_id: Some(pc_id),
259                        plugin_config: None,
260                        package_identifier: "nodejs/node".to_string(),
261                        config_override: None,
262                        execution_site: "auto".to_string(),
263                    }],
264                },
265            ],
266        };
267        let json = serde_json::to_value(&req).expect("serialize");
268        let assignments = json["host_assignments"].as_array().expect("array");
269        assert_eq!(assignments.len(), 2);
270        assert_eq!(
271            assignments[0]["host_id"],
272            "11111111-1111-1111-1111-111111111111"
273        );
274        let plugins = assignments[0]["plugins"].as_array().expect("plugins array");
275        assert_eq!(plugins.len(), 1);
276        assert_eq!(
277            plugins[0]["plugin_config_id"],
278            "a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6"
279        );
280        assert_eq!(plugins[0]["package_identifier"], "nodejs/node");
281        assert_eq!(plugins[0]["role"], "detect_version");
282    }
283
284    #[test]
285    fn update_host_assignment_request_serialization() {
286        let pc_id = Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6").expect("valid uuid");
287        use crate::types_impl::software_items::JsonObjectMapPatch;
288        let req = UpdateHostAssignmentRequest {
289            role: PluginRole::ExecuteUpdate,
290            ordinal: 0,
291            plugin_config_id: Some(pc_id),
292            plugin_config: None,
293            plugin_type: None,
294            package_identifier: Some("homebrew/cask/firefox".to_string()),
295            config_override: JsonObjectMapPatch::Keep,
296            execution_site: None,
297        };
298        let json = serde_json::to_value(&req).expect("serialize");
299        assert_eq!(
300            json["plugin_config_id"],
301            "a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6"
302        );
303        assert_eq!(json["package_identifier"], "homebrew/cask/firefox");
304        assert_eq!(json["role"], "execute_update");
305    }
306
307    #[test]
308    fn trigger_update_request_without_release_info() {
309        let req = TriggerUpdateRequest {
310            to_version: "2.0.0".to_string(),
311            release_info: None,
312            interactive: false,
313        };
314        let json = serde_json::to_value(&req).expect("serialize");
315        assert_eq!(json["to_version"], "2.0.0");
316        assert!(json["release_info"].is_null());
317    }
318
319    #[test]
320    fn trigger_update_request_with_release_info() {
321        let req = TriggerUpdateRequest {
322            to_version: "2.0.0".to_string(),
323            release_info: Some(ReleaseInfoRequest {
324                tag: "v2.0.0".to_string(),
325                release_url: "https://example.com/releases/v2.0.0".to_string(),
326                assets: vec![],
327            }),
328            interactive: false,
329        };
330        let json = serde_json::to_value(&req).expect("serialize");
331        assert_eq!(json["to_version"], "2.0.0");
332        assert_eq!(json["release_info"]["tag"], "v2.0.0");
333    }
334}