ci_manager/ci_provider/
util.rs

1use time::{format_description::well_known, OffsetDateTime};
2
3use crate::*;
4
5/// Type representing a date in the format `YYYY-MM-DD`.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Date {
8    pub year: u16,
9    pub month: u8,
10    pub day: u8,
11}
12
13impl fmt::Display for Date {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        let Date { year, month, day } = self;
16        write!(f, "{year}-{month:02}-{day:02}")
17    }
18}
19
20/// Filter an element by its creation or update date.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum DateFilter {
23    Created(Date),
24    Updated(Date),
25    None,
26}
27
28impl fmt::Display for DateFilter {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            DateFilter::Created(date) => write!(f, "created:{date}"),
32            DateFilter::Updated(date) => write!(f, "updated:{date}"),
33            DateFilter::None => f.write_str(""), // No date filter
34        }
35    }
36}
37
38/// Filter an element by its labels. This is a type-safe way to create a filter string for the GitHub API.
39///
40/// # Example
41///
42/// ```
43/// # use ci_manager::ci_provider::util::LabelFilter;
44///
45/// // Get elements with the label "bug"
46/// let label_filter = LabelFilter::Any(["bug"]);
47/// assert_eq!(label_filter.to_string(), r#"label:"bug""#);
48/// ```
49/// ```
50/// # use ci_manager::ci_provider::util::LabelFilter;
51/// // Only get elements with the labels "bug" and "help wanted"
52/// let label_filter = LabelFilter::All(["bug", "help wanted"]);
53/// assert_eq!(label_filter.to_string(), r#"label:"bug" label:"help wanted""#);
54/// ```
55/// ```
56/// # use ci_manager::ci_provider::util::LabelFilter;
57/// // Do not filter by labels
58/// let label_filter = LabelFilter::none();
59/// assert_eq!(label_filter.to_string(), "");
60/// ```
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum LabelFilter<I, S>
63where
64    I: IntoIterator<Item = S> + Clone,
65    S: AsRef<str> + fmt::Display + fmt::Debug,
66{
67    /// Any of the labels must be present.
68    Any(I),
69    /// All labels must be present.
70    All(I),
71    /// No label filter.
72    ///
73    /// # Note: Use the `none()` method to create this variant.
74    /// Or deal with the type complexity manually :).
75    None(PhantomData<I>),
76}
77
78impl LabelFilter<Vec<String>, String> {
79    /// Default label filter, does not filter by labels
80    pub fn none() -> Self {
81        LabelFilter::None(PhantomData)
82    }
83}
84
85impl<I, S> fmt::Display for LabelFilter<I, S>
86where
87    I: IntoIterator<Item = S> + Clone,
88    S: AsRef<str> + fmt::Display + fmt::Debug,
89{
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            LabelFilter::Any(labels) => write!(
93                f,
94                "label:{}",
95                labels
96                    .clone()
97                    .into_iter()
98                    .map(|l| format!("\"{l}\""))
99                    .collect::<Vec<String>>()
100                    .join(",")
101            ),
102            LabelFilter::All(labels) => write!(
103                f,
104                "{}",
105                labels
106                    .clone()
107                    .into_iter()
108                    .map(|l| format!("label:\"{l}\""))
109                    .collect::<Vec<String>>()
110                    .join(" ")
111            ),
112            LabelFilter::None(_) => f.write_str(""), // No label filter
113        }
114    }
115}
116
117/// Extract the timestamp from a log string.
118///
119/// # Example
120///
121/// ```
122/// # use ci_manager::ci_provider::util::timestamp_from_log;
123/// # use pretty_assertions::assert_eq;
124///
125/// let log = "2024-01-17T11:23:18.0396058Z This is a log message";
126/// let timestamp = timestamp_from_log(log).unwrap();
127/// assert_eq!(timestamp.to_string(), "2024-01-17 11:23:18.0396058 +00:00:00");
128/// ```
129///
130/// # Errors
131/// - If the timestamp could not be extracted from the log.
132/// - If the timestamp could not be parsed.
133pub fn timestamp_from_log(log: &str) -> Result<OffsetDateTime> {
134    static RE: Lazy<Regex> =
135        Lazy::new(|| Regex::new(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z").unwrap());
136    let captures = RE.captures(log);
137    if let Some(captures) = captures {
138        let timestamp = captures
139            .get(0)
140            .context("Could not extract timestamp")?
141            .as_str();
142        OffsetDateTime::parse(timestamp, &well_known::Iso8601::DEFAULT)
143            .with_context(|| format!("Could not parse timestamp: {timestamp}"))
144    } else {
145        bail!("Could not extract timestamp from log: {log}")
146    }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct JobLog {
151    pub name: String,
152    pub content: String,
153}
154
155impl JobLog {
156    pub fn new(name: String, content: String) -> Self {
157        Self { name, content }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use pretty_assertions::{assert_eq, assert_ne};
165
166    #[test]
167    fn test_date_display() {
168        let date = Date {
169            year: 2021,
170            month: 6,
171            day: 2,
172        };
173        assert_eq!(date.to_string(), "2021-06-02");
174    }
175
176    #[test]
177    fn test_date_filter_display() {
178        let date = Date {
179            year: 2021,
180            month: 6,
181            day: 2,
182        };
183        let date_filter = DateFilter::Created(date);
184        assert_eq!(date_filter.to_string(), "created:2021-06-02");
185    }
186
187    #[test]
188    fn test_label_filter_any_display() {
189        let label_filter = LabelFilter::Any(["kind/bug", "area/bake"]);
190        assert_eq!(label_filter.to_string(), r#"label:"kind/bug","area/bake""#);
191    }
192
193    #[test]
194    fn test_label_filter_all_display() {
195        let label_filter = LabelFilter::All(["kind/bug", "area/bake"]);
196        assert_eq!(
197            label_filter.to_string(),
198            r#"label:"kind/bug" label:"area/bake""#
199        );
200    }
201
202    #[test]
203    fn test_label_filter_all_1_display() {
204        let label_filter = LabelFilter::All(["kind/bug"]);
205        assert_eq!(label_filter.to_string(), r#"label:"kind/bug""#);
206    }
207}