pipedash_plugin_api/
build_watch.rs1use serde::{
2 Deserialize,
3 Serialize,
4};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum BuildWatchRunStatus {
9 Pending,
10 Running,
11 Success,
12 Failed,
13 Cancelled,
14 Skipped,
15}
16
17impl BuildWatchRunStatus {
18 pub fn is_terminal(self) -> bool {
19 !matches!(self, Self::Pending | Self::Running)
20 }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum BuildWatchMonitoring {
26 Watching,
27 Completed,
28 Interrupted,
29 Stopped,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "lowercase")]
34pub enum BuildWatchStopReason {
35 Stopped,
36 Interrupted,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct BuildWatchSnapshot {
42 pub session_id: String,
43 pub pipeline_id: String,
44 pub pipeline_name: String,
45 pub provider_name: String,
46 pub provider_type: String,
47 pub run_number: i64,
48 pub branch: Option<String>,
49 pub status: BuildWatchRunStatus,
50 pub started_at: i64,
51 pub concluded_at: Option<i64>,
52 pub updated_at: i64,
53 pub monitoring: BuildWatchMonitoring,
54 pub url: String,
55}
56
57impl BuildWatchSnapshot {
58 pub fn run_url(pipeline_id: &str, run_number: i64) -> String {
59 const HEX: &[u8; 16] = b"0123456789ABCDEF";
60 let mut url = String::with_capacity(pipeline_id.len() + 64);
61 url.push_str("pipedash://pipelines/");
62 for byte in pipeline_id.bytes() {
63 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
64 url.push(char::from(byte));
65 } else {
66 url.push('%');
67 url.push(char::from(HEX[usize::from(byte >> 4)]));
68 url.push(char::from(HEX[usize::from(byte & 15)]));
69 }
70 }
71 url.push_str("/runs/");
72 url.push_str(&run_number.to_string());
73 url
74 }
75
76 pub fn validate(&self) -> Result<(), String> {
77 let valid_text = [
78 (&self.session_id, 128),
79 (&self.pipeline_id, 2048),
80 (&self.pipeline_name, 512),
81 (&self.provider_name, 512),
82 (&self.provider_type, 128),
83 ]
84 .into_iter()
85 .all(|(text, limit)| {
86 !text.trim().is_empty() && text.len() <= limit && !text.chars().any(char::is_control)
87 });
88 let valid_branch = self
89 .branch
90 .as_ref()
91 .is_none_or(|branch| branch.len() <= 1024 && !branch.chars().any(char::is_control));
92 let valid_lifecycle = match self.monitoring {
93 BuildWatchMonitoring::Watching => !self.status.is_terminal(),
94 BuildWatchMonitoring::Completed => self.status.is_terminal(),
95 BuildWatchMonitoring::Interrupted | BuildWatchMonitoring::Stopped => true,
96 };
97 if !valid_text
98 || !valid_branch
99 || !(1..=9_007_199_254_740_991).contains(&self.run_number)
100 || self.started_at < 0
101 || self.updated_at < 0
102 || self.concluded_at.is_some_and(|end| end < self.started_at)
103 || !valid_lifecycle
104 || self.url.len() > 8192
105 || self.url != Self::run_url(&self.pipeline_id, self.run_number)
106 {
107 return Err("Invalid build watch snapshot".to_string());
108 }
109 let size = serde_json::to_vec(self)
110 .map_err(|error| error.to_string())?
111 .len();
112 if size > 3500 {
113 return Err("The build details exceed the watch size limit".to_string());
114 }
115 Ok(())
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct BuildWatchStatus {
122 pub supported: bool,
123 pub available: bool,
124 pub reason: Option<String>,
125 pub watch: Option<BuildWatchSnapshot>,
126}
127
128#[cfg(test)]
129mod tests;