1use chrono::{DateTime, Utc};
6use perfgate_types::RunReceipt;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct BaselineRecord {
17 pub schema: String,
19 pub id: String,
21 pub project: String,
23 pub benchmark: String,
25 pub version: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub git_ref: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub git_sha: Option<String>,
33 pub receipt: RunReceipt,
35 #[serde(default)]
37 pub metadata: BTreeMap<String, String>,
38 #[serde(default)]
40 pub tags: Vec<String>,
41 pub created_at: DateTime<Utc>,
43 pub updated_at: DateTime<Utc>,
45 pub content_hash: String,
47 pub source: BaselineSource,
49 #[serde(default)]
51 pub deleted: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
56#[serde(rename_all = "snake_case")]
57pub enum BaselineSource {
58 #[default]
60 Upload,
61 Promote,
63 Migrate,
65 Rollback,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct UploadBaselineRequest {
76 pub benchmark: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub version: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub git_ref: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub git_sha: Option<String>,
87 pub receipt: RunReceipt,
89 #[serde(default)]
91 pub metadata: BTreeMap<String, String>,
92 #[serde(default)]
94 pub tags: Vec<String>,
95 #[serde(default)]
97 pub normalize: bool,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PromoteBaselineRequest {
103 pub from_version: String,
105 pub to_version: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub git_ref: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub git_sha: Option<String>,
113 #[serde(default)]
115 pub normalize: bool,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ListBaselinesQuery {
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub benchmark: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub benchmark_prefix: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub git_ref: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub git_sha: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub tags: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub since: Option<DateTime<Utc>>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub until: Option<DateTime<Utc>>,
142 #[serde(default = "default_limit")]
144 pub limit: u32,
145 #[serde(default)]
147 pub offset: u64,
148 #[serde(default)]
150 pub include_receipt: bool,
151}
152
153impl Default for ListBaselinesQuery {
154 fn default() -> Self {
155 Self {
156 benchmark: None,
157 benchmark_prefix: None,
158 git_ref: None,
159 git_sha: None,
160 tags: None,
161 since: None,
162 until: None,
163 limit: default_limit(),
164 offset: 0,
165 include_receipt: false,
166 }
167 }
168}
169
170fn default_limit() -> u32 {
171 50
172}
173
174impl ListBaselinesQuery {
175 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn with_benchmark(mut self, benchmark: impl Into<String>) -> Self {
182 self.benchmark = Some(benchmark.into());
183 self
184 }
185
186 pub fn with_benchmark_prefix(mut self, prefix: impl Into<String>) -> Self {
188 self.benchmark_prefix = Some(prefix.into());
189 self
190 }
191
192 pub fn with_git_ref(mut self, git_ref: impl Into<String>) -> Self {
194 self.git_ref = Some(git_ref.into());
195 self
196 }
197
198 pub fn with_tags(mut self, tags: impl Into<String>) -> Self {
200 self.tags = Some(tags.into());
201 self
202 }
203
204 pub fn with_limit(mut self, limit: u32) -> Self {
206 self.limit = limit.min(200);
207 self
208 }
209
210 pub fn with_offset(mut self, offset: u64) -> Self {
212 self.offset = offset;
213 self
214 }
215
216 pub fn with_receipts(mut self) -> Self {
218 self.include_receipt = true;
219 self
220 }
221
222 pub fn to_query_params(&self) -> Vec<(String, String)> {
224 let mut params = Vec::new();
225
226 if let Some(ref v) = self.benchmark {
227 params.push(("benchmark".to_string(), v.clone()));
228 }
229 if let Some(ref v) = self.benchmark_prefix {
230 params.push(("benchmark_prefix".to_string(), v.clone()));
231 }
232 if let Some(ref v) = self.git_ref {
233 params.push(("git_ref".to_string(), v.clone()));
234 }
235 if let Some(ref v) = self.git_sha {
236 params.push(("git_sha".to_string(), v.clone()));
237 }
238 if let Some(ref v) = self.tags {
239 params.push(("tags".to_string(), v.clone()));
240 }
241 if let Some(ref v) = self.since {
242 params.push(("since".to_string(), v.to_rfc3339()));
243 }
244 if let Some(ref v) = self.until {
245 params.push(("until".to_string(), v.to_rfc3339()));
246 }
247 params.push(("limit".to_string(), self.limit.to_string()));
248 params.push(("offset".to_string(), self.offset.to_string()));
249 if self.include_receipt {
250 params.push(("include_receipt".to_string(), "true".to_string()));
251 }
252
253 params
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct UploadBaselineResponse {
264 pub id: String,
266 pub benchmark: String,
268 pub version: String,
270 pub created_at: DateTime<Utc>,
272 pub etag: String,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ListBaselinesResponse {
279 pub baselines: Vec<BaselineSummary>,
281 pub pagination: PaginationInfo,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct BaselineSummary {
288 pub id: String,
290 pub benchmark: String,
292 pub version: String,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub git_ref: Option<String>,
297 pub created_at: DateTime<Utc>,
299 #[serde(default)]
301 pub tags: Vec<String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub receipt: Option<RunReceipt>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct PaginationInfo {
310 pub total: u64,
312 pub limit: u32,
314 pub offset: u64,
316 pub has_more: bool,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct DeleteBaselineResponse {
323 pub deleted: bool,
325 pub id: String,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct PromoteBaselineResponse {
332 pub id: String,
334 pub benchmark: String,
336 pub version: String,
338 pub promoted_from: String,
340 pub created_at: DateTime<Utc>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct HealthResponse {
351 pub status: String,
353 pub version: String,
355 pub storage: StorageHealth,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct StorageHealth {
362 pub backend: String,
364 pub status: String,
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_list_baselines_query_params() {
374 let query = ListBaselinesQuery::new()
375 .with_benchmark("my-bench")
376 .with_limit(100)
377 .with_offset(50)
378 .with_receipts();
379
380 let params = query.to_query_params();
381 assert!(
382 params
383 .iter()
384 .any(|(k, v)| k == "benchmark" && v == "my-bench")
385 );
386 assert!(params.iter().any(|(k, v)| k == "limit" && v == "100"));
387 assert!(params.iter().any(|(k, v)| k == "offset" && v == "50"));
388 assert!(
389 params
390 .iter()
391 .any(|(k, v)| k == "include_receipt" && v == "true")
392 );
393 }
394
395 #[test]
396 fn test_list_baselines_query_limit_capped() {
397 let query = ListBaselinesQuery::new().with_limit(500);
398 assert_eq!(query.limit, 200); }
400
401 #[test]
402 fn test_baseline_source_serde() {
403 let source = BaselineSource::Promote;
404 let json = serde_json::to_string(&source).unwrap();
405 assert_eq!(json, "\"promote\"");
406 let parsed: BaselineSource = serde_json::from_str(&json).unwrap();
407 assert_eq!(parsed, source);
408 }
409}