use chrono::{DateTime, Utc};
use perfgate_types::RunReceipt;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BaselineRecord {
pub schema: String,
pub id: String,
pub project: String,
pub benchmark: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_sha: Option<String>,
pub receipt: RunReceipt,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub tags: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub content_hash: String,
pub source: BaselineSource,
#[serde(default)]
pub deleted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum BaselineSource {
#[default]
Upload,
Promote,
Migrate,
Rollback,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadBaselineRequest {
pub benchmark: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_sha: Option<String>,
pub receipt: RunReceipt,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub normalize: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromoteBaselineRequest {
pub from_version: String,
pub to_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_sha: Option<String>,
#[serde(default)]
pub normalize: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListBaselinesQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub benchmark: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub benchmark_prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_sha: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub until: Option<DateTime<Utc>>,
#[serde(default = "default_limit")]
pub limit: u32,
#[serde(default)]
pub offset: u64,
#[serde(default)]
pub include_receipt: bool,
}
impl Default for ListBaselinesQuery {
fn default() -> Self {
Self {
benchmark: None,
benchmark_prefix: None,
git_ref: None,
git_sha: None,
tags: None,
since: None,
until: None,
limit: default_limit(),
offset: 0,
include_receipt: false,
}
}
}
fn default_limit() -> u32 {
50
}
impl ListBaselinesQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_benchmark(mut self, benchmark: impl Into<String>) -> Self {
self.benchmark = Some(benchmark.into());
self
}
pub fn with_benchmark_prefix(mut self, prefix: impl Into<String>) -> Self {
self.benchmark_prefix = Some(prefix.into());
self
}
pub fn with_git_ref(mut self, git_ref: impl Into<String>) -> Self {
self.git_ref = Some(git_ref.into());
self
}
pub fn with_tags(mut self, tags: impl Into<String>) -> Self {
self.tags = Some(tags.into());
self
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = limit.min(200);
self
}
pub fn with_offset(mut self, offset: u64) -> Self {
self.offset = offset;
self
}
pub fn with_receipts(mut self) -> Self {
self.include_receipt = true;
self
}
pub fn to_query_params(&self) -> Vec<(String, String)> {
let mut params = Vec::new();
if let Some(ref v) = self.benchmark {
params.push(("benchmark".to_string(), v.clone()));
}
if let Some(ref v) = self.benchmark_prefix {
params.push(("benchmark_prefix".to_string(), v.clone()));
}
if let Some(ref v) = self.git_ref {
params.push(("git_ref".to_string(), v.clone()));
}
if let Some(ref v) = self.git_sha {
params.push(("git_sha".to_string(), v.clone()));
}
if let Some(ref v) = self.tags {
params.push(("tags".to_string(), v.clone()));
}
if let Some(ref v) = self.since {
params.push(("since".to_string(), v.to_rfc3339()));
}
if let Some(ref v) = self.until {
params.push(("until".to_string(), v.to_rfc3339()));
}
params.push(("limit".to_string(), self.limit.to_string()));
params.push(("offset".to_string(), self.offset.to_string()));
if self.include_receipt {
params.push(("include_receipt".to_string(), "true".to_string()));
}
params
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadBaselineResponse {
pub id: String,
pub benchmark: String,
pub version: String,
pub created_at: DateTime<Utc>,
pub etag: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListBaselinesResponse {
pub baselines: Vec<BaselineSummary>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineSummary {
pub id: String,
pub benchmark: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_ref: Option<String>,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receipt: Option<RunReceipt>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
pub total: u64,
pub limit: u32,
pub offset: u64,
pub has_more: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteBaselineResponse {
pub deleted: bool,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromoteBaselineResponse {
pub id: String,
pub benchmark: String,
pub version: String,
pub promoted_from: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub storage: StorageHealth,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageHealth {
pub backend: String,
pub status: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_baselines_query_params() {
let query = ListBaselinesQuery::new()
.with_benchmark("my-bench")
.with_limit(100)
.with_offset(50)
.with_receipts();
let params = query.to_query_params();
assert!(
params
.iter()
.any(|(k, v)| k == "benchmark" && v == "my-bench")
);
assert!(params.iter().any(|(k, v)| k == "limit" && v == "100"));
assert!(params.iter().any(|(k, v)| k == "offset" && v == "50"));
assert!(
params
.iter()
.any(|(k, v)| k == "include_receipt" && v == "true")
);
}
#[test]
fn test_list_baselines_query_limit_capped() {
let query = ListBaselinesQuery::new().with_limit(500);
assert_eq!(query.limit, 200); }
#[test]
fn test_baseline_source_serde() {
let source = BaselineSource::Promote;
let json = serde_json::to_string(&source).unwrap();
assert_eq!(json, "\"promote\"");
let parsed: BaselineSource = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, source);
}
}