1use std::path::Path;
5
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum ScanScheduleKind {
16 Webhook,
17 Poll,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum ScanScheduleProvider {
22 #[serde(rename = "github")]
23 GitHub,
24 #[serde(rename = "gitlab")]
25 GitLab,
26 #[serde(rename = "bitbucket")]
27 Bitbucket,
28 #[serde(rename = "any")]
29 Any,
30}
31
32impl ScanScheduleProvider {
33 #[must_use]
34 pub const fn display_name(&self) -> &'static str {
35 match self {
36 Self::GitHub => "GitHub",
37 Self::GitLab => "GitLab",
38 Self::Bitbucket => "Bitbucket",
39 Self::Any => "Any / Poll",
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ScanSchedule {
48 pub id: Uuid,
49 pub label: String,
50 pub repo_url: String,
51 pub branch: String,
52 pub kind: ScanScheduleKind,
53 pub provider: ScanScheduleProvider,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub webhook_secret: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub interval_secs: Option<u64>,
58 pub last_scan_sha: Option<String>,
59 pub last_scan_at: Option<DateTime<Utc>>,
60 pub last_run_id: Option<String>,
61 pub enabled: bool,
62}
63
64impl ScanSchedule {
65 #[must_use]
66 pub fn new_webhook(
67 repo_url: String,
68 branch: String,
69 provider: ScanScheduleProvider,
70 label: String,
71 webhook_secret: Option<String>,
72 ) -> Self {
73 Self {
74 id: Uuid::new_v4(),
75 label,
76 repo_url,
77 branch,
78 kind: ScanScheduleKind::Webhook,
79 provider,
80 webhook_secret: Some(webhook_secret.unwrap_or_else(generate_secret)),
81 interval_secs: None,
82 last_scan_sha: None,
83 last_scan_at: None,
84 last_run_id: None,
85 enabled: true,
86 }
87 }
88
89 #[must_use]
90 pub fn new_poll(repo_url: String, branch: String, interval_secs: u64, label: String) -> Self {
91 Self {
92 id: Uuid::new_v4(),
93 label,
94 repo_url,
95 branch,
96 kind: ScanScheduleKind::Poll,
97 provider: ScanScheduleProvider::Any,
98 webhook_secret: None,
99 interval_secs: Some(interval_secs),
100 last_scan_sha: None,
101 last_scan_at: None,
102 last_run_id: None,
103 enabled: true,
104 }
105 }
106}
107
108fn generate_secret() -> String {
109 format!("{}-{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
110}
111
112#[derive(Debug, Default, Serialize, Deserialize)]
115pub struct ScheduleStore {
116 pub schedules: Vec<ScanSchedule>,
117}
118
119impl ScheduleStore {
120 #[must_use]
121 pub fn load(path: &Path) -> Self {
122 std::fs::read_to_string(path)
123 .ok()
124 .and_then(|s| serde_json::from_str(&s).ok())
125 .unwrap_or_default()
126 }
127
128 pub fn save(&self, path: &Path) -> Result<()> {
131 let json = serde_json::to_string_pretty(self)?;
132 std::fs::write(path, json)?;
133 Ok(())
134 }
135
136 #[must_use]
137 pub fn find_matching<'a>(&'a self, repo_url: &str, branch: &str) -> Vec<&'a ScanSchedule> {
138 self.schedules
139 .iter()
140 .filter(|s| s.enabled && urls_match(&s.repo_url, repo_url) && s.branch == branch)
141 .collect()
142 }
143
144 pub fn by_id_mut(&mut self, id: Uuid) -> Option<&mut ScanSchedule> {
145 self.schedules.iter_mut().find(|s| s.id == id)
146 }
147
148 pub fn remove(&mut self, id: Uuid) {
149 self.schedules.retain(|s| s.id != id);
150 }
151}
152
153fn urls_match(a: &str, b: &str) -> bool {
154 normalize_url(a) == normalize_url(b)
155}
156
157fn normalize_url(url: &str) -> String {
158 url.trim_end_matches('/')
159 .trim_end_matches(".git")
160 .to_lowercase()
161}