Skip to main content

remendo/models/
patch.rs

1//! Patch domain type — an individual diff within a patchset.
2
3use serde::{Deserialize, Deserializer};
4use std::fmt;
5
6/// Status of an individual patch within a patchset.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
8pub enum PatchStatus {
9    /// Awaiting review.
10    #[default]
11    Pending,
12    /// Review completed.
13    Reviewed,
14    /// Review failed.
15    Failed,
16    /// Patch could not be applied to the baseline.
17    ApplyError,
18    /// Unrecognized status from the API.
19    Unknown,
20}
21
22impl fmt::Display for PatchStatus {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Pending => write!(f, "Pending"),
26            Self::Reviewed => write!(f, "Reviewed"),
27            Self::Failed => write!(f, "Failed"),
28            Self::ApplyError => write!(f, "Apply Error"),
29            Self::Unknown => write!(f, "Unknown"),
30        }
31    }
32}
33
34impl<'de> Deserialize<'de> for PatchStatus {
35    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36    where
37        D: Deserializer<'de>,
38    {
39        let s = Option::<String>::deserialize(deserializer)?;
40        Ok(match s.as_deref().map(str::to_lowercase).as_deref() {
41            Some("reviewed") => Self::Reviewed,
42            Some("failed") => Self::Failed,
43            Some("apply error" | "applyerror" | "failedtoapply") => Self::ApplyError,
44            Some("pending") | None => Self::Pending,
45            Some(other) => {
46                tracing::warn!(status = other, "unknown patch status, using Unknown");
47                Self::Unknown
48            }
49        })
50    }
51}
52
53/// An individual patch (diff) within a patchset.
54#[derive(Debug, Clone, PartialEq, Deserialize)]
55pub struct Patch {
56    /// Database identifier.
57    #[serde(default)]
58    pub id: i64,
59    /// RFC 2822 `Message-ID` of the patch email.
60    #[serde(default)]
61    pub message_id: Option<String>,
62    /// Position in the series (e.g., 1 for `[PATCH 1/5]`).
63    #[serde(default)]
64    pub part_index: Option<u32>,
65    /// Subject line.
66    #[serde(default)]
67    pub subject: Option<String>,
68    /// Review status of this individual patch.
69    #[serde(default)]
70    pub status: Option<PatchStatus>,
71    /// Error message if the patch could not be applied.
72    #[serde(default)]
73    pub apply_error: Option<String>,
74    /// Database ID of the corresponding message record.
75    #[serde(default)]
76    pub msg_db_id: Option<i64>,
77}
78
79#[cfg(test)]
80#[allow(clippy::expect_used)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn patch_status_default() {
86        assert_eq!(PatchStatus::default(), PatchStatus::Pending);
87    }
88
89    #[test]
90    fn patch_status_display() {
91        assert_eq!(PatchStatus::ApplyError.to_string(), "Apply Error");
92        assert_eq!(PatchStatus::Unknown.to_string(), "Unknown");
93    }
94
95    #[test]
96    fn deserialize_patch() {
97        let json = r#"{"id": 42, "part_index": 3, "subject": "fix something"}"#;
98        let patch: Patch = serde_json::from_str(json).expect("deserialize patch");
99        assert_eq!(patch.id, 42);
100        assert_eq!(patch.part_index, Some(3));
101    }
102
103    #[test]
104    fn deserialize_patch_unknown_status() {
105        let json = r#"{"id": 1, "status": "NewThing"}"#;
106        let patch: Patch = serde_json::from_str(json).expect("deserialize patch unknown status");
107        assert_eq!(patch.status, Some(PatchStatus::Unknown));
108    }
109}