1use std::collections::HashMap;
2
3use chrono::{
4 DateTime,
5 Utc,
6};
7use serde::{
8 Deserialize,
9 Serialize,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum PipelineStatus {
15 Success,
16 Failed,
17 Degraded,
18 Running,
19 Pending,
20 Cancelled,
21 Skipped,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum ReviewDecision {
27 Approve,
28 Reject,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AvailablePipeline {
33 pub id: String,
34 pub name: String,
35 pub description: Option<String>,
36 pub organization: Option<String>,
37 pub repository: Option<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ResourceState {
42 pub label: String,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub message: Option<String>,
45}
46
47impl ResourceState {
48 pub fn new(label: impl Into<String>, message: Option<&str>) -> Self {
49 Self {
50 label: label.into(),
51 message: message
52 .map(str::trim)
53 .filter(|message| !message.is_empty())
54 .map(str::to_string),
55 }
56 }
57}
58
59pub const READ_ONLY_METADATA_KEY: &str = "read_only";
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Pipeline {
63 pub id: String,
64 pub name: String,
65 pub status: PipelineStatus,
66 pub last_run: Option<DateTime<Utc>>,
67 pub last_updated: DateTime<Utc>,
68 pub repository: String,
69 pub branch: Option<String>,
70 pub workflow_file: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub resource_state: Option<ResourceState>,
73 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
74 pub metadata: HashMap<String, serde_json::Value>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PipelineRun {
79 pub id: String,
80 pub pipeline_id: String,
81 pub run_number: i64,
82 pub status: PipelineStatus,
83 pub started_at: DateTime<Utc>,
84 pub concluded_at: Option<DateTime<Utc>>,
85 pub duration_seconds: Option<i64>,
86 pub logs_url: String,
87 pub commit_sha: Option<String>,
88 pub commit_message: Option<String>,
89 pub branch: Option<String>,
90 pub actor: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub inputs: Option<serde_json::Value>,
93 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
94 pub metadata: HashMap<String, serde_json::Value>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct TriggerParams {
99 pub workflow_id: String,
100 pub inputs: Option<serde_json::Value>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Organization {
105 pub id: String,
106 pub name: String,
107 pub description: Option<String>,
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct PipelineCatalog {
112 pub organizations: Vec<Organization>,
113 pub pipelines: Vec<AvailablePipeline>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(tag = "type", rename_all = "lowercase")]
118pub enum WorkflowParameterType {
119 String {
120 #[serde(default)]
121 default: Option<String>,
122 },
123 Boolean {
124 #[serde(default)]
125 default: bool,
126 },
127 Choice {
128 options: Vec<String>,
129 #[serde(default)]
130 default: Option<String>,
131 },
132 Number {
133 #[serde(default)]
134 default: Option<f64>,
135 },
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct WorkflowParameter {
140 pub name: String,
141 pub label: Option<String>,
142 pub description: Option<String>,
143 #[serde(flatten)]
144 pub param_type: WorkflowParameterType,
145 #[serde(default)]
146 pub required: bool,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct PaginationParams {
151 pub page: usize,
152 pub page_size: usize,
153}
154
155impl PaginationParams {
156 pub fn validate(&self) -> Result<(), String> {
157 if self.page == 0 {
158 return Err("Page must be >= 1".to_string());
159 }
160 if self.page_size == 0 {
161 return Err("Page size must be >= 1".to_string());
162 }
163 if self.page_size > 200 {
164 return Err("Page size must be <= 200".to_string());
165 }
166 Ok(())
167 }
168
169 pub fn calculate_offset(&self) -> Result<usize, String> {
170 self.page
171 .checked_sub(1)
172 .and_then(|p| p.checked_mul(self.page_size))
173 .ok_or_else(|| "Pagination offset overflow".to_string())
174 }
175}
176
177impl Default for PaginationParams {
178 fn default() -> Self {
179 Self {
180 page: 1,
181 page_size: 100,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct PaginatedResponse<T> {
188 pub items: Vec<T>,
189 pub page: usize,
190 pub page_size: usize,
191 pub total_count: usize,
192 pub total_pages: usize,
193 pub has_more: bool,
194}
195
196impl<T> PaginatedResponse<T> {
197 pub fn new(items: Vec<T>, page: usize, page_size: usize, total_count: usize) -> Self {
198 let items_count = items.len();
199 let total_pages = if page_size > 0 {
200 total_count.div_ceil(page_size)
201 } else {
202 1
203 };
204 let has_more = items_count == page_size;
205
206 Self {
207 items,
208 page,
209 page_size,
210 total_count,
211 total_pages,
212 has_more,
213 }
214 }
215
216 pub fn empty() -> Self {
217 Self {
218 items: Vec::new(),
219 page: 1,
220 page_size: 100,
221 total_count: 0,
222 total_pages: 0,
223 has_more: false,
224 }
225 }
226}
227
228pub type PaginatedAvailablePipelines = PaginatedResponse<AvailablePipeline>;
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
231pub struct Permission {
232 pub name: String,
233 pub description: String,
234 pub required: bool,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct PermissionCheck {
239 pub permission: Permission,
240 pub granted: bool,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct PermissionStatus {
245 pub permissions: Vec<PermissionCheck>,
246 pub all_granted: bool,
247 pub checked_at: DateTime<Utc>,
248 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
249 pub metadata: HashMap<String, String>,
250 #[serde(default)]
251 pub credential_label: Option<String>,
252}
253
254impl PermissionStatus {
255 pub fn evaluate(
256 permissions: &[Permission], token_type: &str, mut granted: impl FnMut(&Permission) -> bool,
257 ) -> Self {
258 let permissions: Vec<_> = permissions
259 .iter()
260 .map(|permission| PermissionCheck {
261 granted: granted(permission),
262 permission: permission.clone(),
263 })
264 .collect();
265 let all_granted = permissions
266 .iter()
267 .filter(|check| check.permission.required)
268 .all(|check| check.granted);
269
270 Self {
271 permissions,
272 all_granted,
273 checked_at: Utc::now(),
274 metadata: HashMap::from([("token_type".to_string(), token_type.to_string())]),
275 credential_label: None,
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct RateLimit {
282 pub limit: u64,
283 pub remaining: u64,
284 pub reset_at: DateTime<Utc>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct Feature {
289 pub id: String,
290 pub name: String,
291 pub description: String,
292 pub required_permissions: Vec<String>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct FeatureAvailability {
297 pub feature: Feature,
298 pub available: bool,
299 pub missing_permissions: Vec<String>,
300}
301
302#[cfg(test)]
303mod tests;