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    use super::*;
61
62    fn make_request(name: &str) -> CreateSoftwareIgnoreRequest {
63        CreateSoftwareIgnoreRequest {
64            name: name.to_string(),
65            host_id: None,
66        }
67    }
68
69    #[test]
70    fn validate_accepts_non_empty_name() {
71        assert!(make_request("FreshRSS").validate().is_ok());
72    }
73
74    #[test]
75    fn validate_rejects_empty_name() {
76        let err = make_request("").validate().unwrap_err();
77        assert_eq!(err.field, "name");
78    }
79
80    #[test]
81    fn validate_rejects_whitespace_only_name() {
82        assert!(make_request("   ").validate().is_err());
83    }
84
85    #[test]
86    fn software_ignore_response_round_trip_tenant_wide() {
87        use time::macros::datetime;
88        let resp = SoftwareIgnoreResponse {
89            id: Uuid::nil(),
90            name: "FreshRSS".to_string(),
91            host_id: None,
92            created_at: datetime!(2025-01-01 0:00:00 UTC),
93        };
94        let json = serde_json::to_string(&resp).expect("serialization should succeed");
95        let deserialized: SoftwareIgnoreResponse =
96            serde_json::from_str(&json).expect("deserialization should succeed");
97        assert_eq!(deserialized.name, "FreshRSS");
98        assert!(deserialized.host_id.is_none());
99        // host_id should be omitted when None
100        assert!(!json.contains("host_id"));
101    }
102
103    #[test]
104    fn software_ignore_response_round_trip_per_host() {
105        use time::macros::datetime;
106        let host = Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6").expect("valid uuid");
107        let resp = SoftwareIgnoreResponse {
108            id: Uuid::nil(),
109            name: "FreshRSS".to_string(),
110            host_id: Some(host),
111            created_at: datetime!(2025-01-01 0:00:00 UTC),
112        };
113        let json = serde_json::to_string(&resp).expect("serialization should succeed");
114        let deserialized: SoftwareIgnoreResponse =
115            serde_json::from_str(&json).expect("deserialization should succeed");
116        assert_eq!(deserialized.host_id, Some(host));
117    }
118
119    #[test]
120    fn create_request_with_host_id() {
121        let host = Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6").expect("valid uuid");
122        let req = CreateSoftwareIgnoreRequest {
123            name: "FreshRSS".to_string(),
124            host_id: Some(host),
125        };
126        assert!(req.validate().is_ok());
127        let json = serde_json::to_string(&req).expect("serialization should succeed");
128        let deserialized: CreateSoftwareIgnoreRequest =
129            serde_json::from_str(&json).expect("deserialization should succeed");
130        assert_eq!(deserialized.host_id, Some(host));
131    }
132}