Skip to main content

uptrakit_web_api_types/
autodiscovery.rs

1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5use crate::validation::{Validate, ValidationError};
6
7/// Response for trigger-discovery endpoints.
8#[derive(Serialize, Deserialize)]
9#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
10pub struct TriggerDiscoveryResponse {
11    /// Number of plugin assignments queued for discovery.
12    pub plugins_queued: u32,
13    /// Human-readable summary message.
14    pub message: String,
15}
16
17/// A single entry in the software ignore list.
18#[derive(Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
20pub struct SoftwareIgnoreResponse {
21    /// Ignore rule UUID.
22    pub id: Uuid,
23    /// Software item display name to suppress.
24    pub name: String,
25    /// When set, this ignore rule applies only to the given host.
26    /// `None` means the rule is tenant-wide.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub host_id: Option<Uuid>,
29    #[serde(with = "time::serde::rfc3339")]
30    #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime))]
31    pub created_at: OffsetDateTime,
32}
33
34/// Request body for creating a software ignore rule.
35#[derive(Serialize, Deserialize)]
36#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
37pub struct CreateSoftwareIgnoreRequest {
38    /// Software item display name to permanently suppress from future discoveries.
39    pub name: String,
40    /// Optionally scope the ignore rule to a specific host.
41    /// `None` means the rule applies tenant-wide.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub host_id: Option<Uuid>,
44}
45
46impl Validate for CreateSoftwareIgnoreRequest {
47    fn validate(&self) -> Result<(), ValidationError> {
48        if self.name.trim().is_empty() {
49            return Err(ValidationError {
50                field: "name",
51                message: "name must not be empty".to_string(),
52            });
53        }
54        Ok(())
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    #![expect(
61        clippy::assertions_on_result_states,
62        reason = "test assertions — is_ok/is_err provides readable failure messages"
63    )]
64    use super::*;
65
66    fn make_request(name: &str) -> CreateSoftwareIgnoreRequest {
67        CreateSoftwareIgnoreRequest {
68            name: name.to_string(),
69            host_id: None,
70        }
71    }
72
73    #[test]
74    fn validate_accepts_non_empty_name() {
75        assert!(make_request("FreshRSS").validate().is_ok());
76    }
77
78    #[test]
79    fn validate_rejects_empty_name() {
80        let err = make_request("").validate().unwrap_err();
81        assert_eq!(err.field, "name");
82    }
83
84    #[test]
85    fn validate_rejects_whitespace_only_name() {
86        assert!(make_request("   ").validate().is_err());
87    }
88
89    #[test]
90    fn software_ignore_response_round_trip_tenant_wide() {
91        use time::macros::datetime;
92        let resp = SoftwareIgnoreResponse {
93            id: Uuid::nil(),
94            name: "FreshRSS".to_string(),
95            host_id: None,
96            created_at: datetime!(2025-01-01 0:00:00 UTC),
97        };
98        let json = serde_json::to_string(&resp).expect("serialization should succeed");
99        let deserialized: SoftwareIgnoreResponse =
100            serde_json::from_str(&json).expect("deserialization should succeed");
101        assert_eq!(deserialized.name, "FreshRSS");
102        assert!(deserialized.host_id.is_none());
103        // host_id should be omitted when None
104        assert!(!json.contains("host_id"));
105    }
106
107    #[test]
108    fn software_ignore_response_round_trip_per_host() {
109        use time::macros::datetime;
110        let host = Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6").expect("valid uuid");
111        let resp = SoftwareIgnoreResponse {
112            id: Uuid::nil(),
113            name: "FreshRSS".to_string(),
114            host_id: Some(host),
115            created_at: datetime!(2025-01-01 0:00:00 UTC),
116        };
117        let json = serde_json::to_string(&resp).expect("serialization should succeed");
118        let deserialized: SoftwareIgnoreResponse =
119            serde_json::from_str(&json).expect("deserialization should succeed");
120        assert_eq!(deserialized.host_id, Some(host));
121    }
122
123    #[test]
124    fn create_request_with_host_id() {
125        let host = Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6").expect("valid uuid");
126        let req = CreateSoftwareIgnoreRequest {
127            name: "FreshRSS".to_string(),
128            host_id: Some(host),
129        };
130        assert!(req.validate().is_ok());
131        let json = serde_json::to_string(&req).expect("serialization should succeed");
132        let deserialized: CreateSoftwareIgnoreRequest =
133            serde_json::from_str(&json).expect("deserialization should succeed");
134        assert_eq!(deserialized.host_id, Some(host));
135    }
136}