use chrono::{DateTime, Utc};
use perfgate_types::{RunReceipt, VerdictCounts, VerdictStatus};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const BASELINE_SCHEMA_V1: &str = "perfgate.baseline.v1";
pub const PROJECT_SCHEMA_V1: &str = "perfgate.project.v1";
pub const VERDICT_SCHEMA_V1: &str = "perfgate.verdict.v1";
pub const AUDIT_SCHEMA_V1: &str = "perfgate.audit.v1";
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum BaselineSource {
#[default]
Upload,
Promote,
Migrate,
Rollback,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, 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,
}
impl BaselineRecord {
pub fn etag(&self) -> String {
format!("\"sha256:{}\"", self.content_hash)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct VerdictRecord {
pub schema: String,
pub id: String,
pub project: String,
pub benchmark: String,
pub run_id: String,
pub status: VerdictStatus,
pub counts: VerdictCounts,
pub reasons: Vec<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 created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SubmitVerdictRequest {
pub benchmark: String,
pub run_id: String,
pub status: VerdictStatus,
pub counts: VerdictCounts,
pub reasons: Vec<String>,
pub git_ref: Option<String>,
pub git_sha: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListVerdictsQuery {
pub benchmark: Option<String>,
pub status: Option<VerdictStatus>,
pub since: Option<DateTime<Utc>>,
pub until: Option<DateTime<Utc>>,
#[serde(default = "default_limit")]
pub limit: u32,
#[serde(default)]
pub offset: u64,
}
impl Default for ListVerdictsQuery {
fn default() -> Self {
Self {
benchmark: None,
status: None,
since: None,
until: None,
limit: default_limit(),
offset: 0,
}
}
}
impl ListVerdictsQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_benchmark(mut self, b: impl Into<String>) -> Self {
self.benchmark = Some(b.into());
self
}
pub fn with_status(mut self, s: VerdictStatus) -> Self {
self.status = Some(s);
self
}
pub fn with_limit(mut self, l: u32) -> Self {
self.limit = l;
self
}
pub fn with_offset(mut self, o: u64) -> Self {
self.offset = o;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListVerdictsResponse {
pub verdicts: Vec<VerdictRecord>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct BaselineVersion {
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 created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by: Option<String>,
pub is_current: bool,
pub source: BaselineSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct RetentionPolicy {
pub max_versions: Option<u32>,
pub max_age_days: Option<u32>,
pub preserve_tags: Vec<String>,
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self {
max_versions: Some(50),
max_age_days: Some(365),
preserve_tags: vec!["production".to_string(), "stable".to_string()],
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum VersioningStrategy {
#[default]
RunId,
Timestamp,
GitSha,
Manual,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Project {
pub schema: String,
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub retention: RetentionPolicy,
pub versioning: VersioningStrategy,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListBaselinesQuery {
pub benchmark: Option<String>,
pub benchmark_prefix: Option<String>,
pub git_ref: Option<String>,
pub git_sha: Option<String>,
pub tags: Option<String>,
pub since: Option<DateTime<Utc>>,
pub until: Option<DateTime<Utc>>,
#[serde(default)]
pub include_receipt: bool,
#[serde(default = "default_limit")]
pub limit: u32,
#[serde(default)]
pub offset: u64,
}
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,
include_receipt: false,
limit: default_limit(),
offset: 0,
}
}
}
fn default_limit() -> u32 {
50
}
impl ListBaselinesQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_benchmark(mut self, b: impl Into<String>) -> Self {
self.benchmark = Some(b.into());
self
}
pub fn with_benchmark_prefix(mut self, p: impl Into<String>) -> Self {
self.benchmark_prefix = Some(p.into());
self
}
pub fn with_offset(mut self, o: u64) -> Self {
self.offset = o;
self
}
pub fn with_limit(mut self, l: u32) -> Self {
self.limit = l;
self
}
pub fn with_receipts(mut self) -> Self {
self.include_receipt = true;
self
}
pub fn parsed_tags(&self) -> Vec<String> {
self.tags
.as_ref()
.map(|t| {
t.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
pub fn to_query_params(&self) -> Vec<(String, String)> {
let mut params = Vec::new();
if let Some(b) = &self.benchmark {
params.push(("benchmark".to_string(), b.clone()));
}
if let Some(p) = &self.benchmark_prefix {
params.push(("benchmark_prefix".to_string(), p.clone()));
}
if let Some(r) = &self.git_ref {
params.push(("git_ref".to_string(), r.clone()));
}
if let Some(s) = &self.git_sha {
params.push(("git_sha".to_string(), s.clone()));
}
if let Some(t) = &self.tags {
params.push(("tags".to_string(), t.clone()));
}
if let Some(s) = &self.since {
params.push(("since".to_string(), s.to_rfc3339()));
}
if let Some(u) = &self.until {
params.push(("until".to_string(), u.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, JsonSchema)]
pub struct PaginationInfo {
pub total: u64,
pub offset: u64,
pub limit: u32,
pub has_more: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListBaselinesResponse {
pub baselines: Vec<BaselineSummary>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct BaselineSummary {
pub id: String,
pub benchmark: String,
pub version: String,
pub created_at: DateTime<Utc>,
pub git_ref: Option<String>,
pub git_sha: Option<String>,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receipt: Option<RunReceipt>,
}
impl From<BaselineRecord> for BaselineSummary {
fn from(record: BaselineRecord) -> Self {
Self {
id: record.id,
benchmark: record.benchmark,
version: record.version,
created_at: record.created_at,
git_ref: record.git_ref,
git_sha: record.git_sha,
tags: record.tags,
receipt: Some(record.receipt),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct UploadBaselineRequest {
pub benchmark: String,
pub version: Option<String>,
pub git_ref: Option<String>,
pub git_sha: Option<String>,
pub receipt: RunReceipt,
pub metadata: BTreeMap<String, String>,
pub tags: Vec<String>,
pub normalize: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
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, JsonSchema)]
pub struct PromoteBaselineRequest {
pub from_version: String,
pub to_version: String,
pub git_ref: Option<String>,
pub git_sha: Option<String>,
pub tags: Vec<String>,
#[serde(default)]
pub normalize: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PromoteBaselineResponse {
pub id: String,
pub benchmark: String,
pub version: String,
pub promoted_from: String,
pub promoted_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DeleteBaselineResponse {
pub deleted: bool,
pub id: String,
pub benchmark: String,
pub version: String,
pub deleted_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuditAction {
Create,
Update,
Delete,
Promote,
}
impl std::fmt::Display for AuditAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuditAction::Create => write!(f, "create"),
AuditAction::Update => write!(f, "update"),
AuditAction::Delete => write!(f, "delete"),
AuditAction::Promote => write!(f, "promote"),
}
}
}
impl std::str::FromStr for AuditAction {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"create" => Ok(AuditAction::Create),
"update" => Ok(AuditAction::Update),
"delete" => Ok(AuditAction::Delete),
"promote" => Ok(AuditAction::Promote),
other => Err(format!("Unknown audit action: {}", other)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuditResourceType {
Baseline,
Key,
Verdict,
}
impl std::fmt::Display for AuditResourceType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuditResourceType::Baseline => write!(f, "baseline"),
AuditResourceType::Key => write!(f, "key"),
AuditResourceType::Verdict => write!(f, "verdict"),
}
}
}
impl std::str::FromStr for AuditResourceType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"baseline" => Ok(AuditResourceType::Baseline),
"key" => Ok(AuditResourceType::Key),
"verdict" => Ok(AuditResourceType::Verdict),
other => Err(format!("Unknown resource type: {}", other)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AuditEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub actor: String,
pub action: AuditAction,
pub resource_type: AuditResourceType,
pub resource_id: String,
pub project: String,
#[serde(default)]
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListAuditEventsQuery {
pub project: Option<String>,
pub action: Option<String>,
pub resource_type: Option<String>,
pub actor: Option<String>,
pub since: Option<DateTime<Utc>>,
pub until: Option<DateTime<Utc>>,
#[serde(default = "default_limit")]
pub limit: u32,
#[serde(default)]
pub offset: u64,
}
impl Default for ListAuditEventsQuery {
fn default() -> Self {
Self {
project: None,
action: None,
resource_type: None,
actor: None,
since: None,
until: None,
limit: default_limit(),
offset: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListAuditEventsResponse {
pub events: Vec<AuditEvent>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct StorageHealth {
pub backend: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct PoolMetrics {
pub idle: u32,
pub active: u32,
pub max: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub storage: StorageHealth,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool: Option<PoolMetrics>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ApiError {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl ApiError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
details: None,
}
}
pub fn unauthorized(msg: &str) -> Self {
Self::new("unauthorized", msg)
}
pub fn forbidden(msg: &str) -> Self {
Self::new("forbidden", msg)
}
pub fn not_found(msg: &str) -> Self {
Self::new("not_found", msg)
}
pub fn bad_request(msg: &str) -> Self {
Self::new("bad_request", msg)
}
pub fn conflict(msg: &str) -> Self {
Self::new("conflict", msg)
}
pub fn internal_error(msg: &str) -> Self {
Self::new("internal_error", msg)
}
pub fn internal(msg: &str) -> Self {
Self::internal_error(msg)
}
pub fn validation(msg: &str) -> Self {
Self::new("invalid_input", msg)
}
pub fn already_exists(msg: &str) -> Self {
Self::new("conflict", msg)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CreateKeyRequest {
pub description: String,
pub role: perfgate_auth::Role,
pub project: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CreateKeyResponse {
pub id: String,
pub key: String,
pub description: String,
pub role: perfgate_auth::Role,
pub project: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct KeyEntry {
pub id: String,
pub key_prefix: String,
pub description: String,
pub role: perfgate_auth::Role,
pub project: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revoked_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListKeysResponse {
pub keys: Vec<KeyEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RevokeKeyResponse {
pub id: String,
pub revoked_at: DateTime<Utc>,
}
pub const DEPENDENCY_EVENT_SCHEMA_V1: &str = "perfgate.dependency_event.v1";
pub const FLEET_ALERT_SCHEMA_V1: &str = "perfgate.fleet_alert.v1";
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct DependencyChange {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct DependencyEvent {
pub schema: String,
pub id: String,
pub project: String,
pub benchmark: String,
pub dep_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_version: Option<String>,
pub metric: String,
pub delta_pct: f64,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RecordDependencyEventRequest {
pub project: String,
pub benchmark: String,
pub dependency_changes: Vec<DependencyChange>,
pub metric: String,
pub delta_pct: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RecordDependencyEventResponse {
pub recorded: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AffectedProject {
pub project: String,
pub benchmark: String,
pub metric: String,
pub delta_pct: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct FleetAlert {
pub schema: String,
pub id: String,
pub dependency: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_version: Option<String>,
pub affected_projects: Vec<AffectedProject>,
pub confidence: f64,
pub avg_delta_pct: f64,
pub first_seen: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListFleetAlertsQuery {
#[serde(default = "default_min_affected")]
pub min_affected: usize,
pub since: Option<DateTime<Utc>>,
#[serde(default = "default_limit")]
pub limit: u32,
}
impl Default for ListFleetAlertsQuery {
fn default() -> Self {
Self {
min_affected: default_min_affected(),
since: None,
limit: default_limit(),
}
}
}
fn default_min_affected() -> usize {
2
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListFleetAlertsResponse {
pub alerts: Vec<FleetAlert>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DependencyImpactQuery {
pub since: Option<DateTime<Utc>>,
#[serde(default = "default_limit")]
pub limit: u32,
}
impl Default for DependencyImpactQuery {
fn default() -> Self {
Self {
since: None,
limit: default_limit(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DependencyImpactResponse {
pub dependency: String,
pub events: Vec<DependencyEvent>,
pub project_count: usize,
pub avg_delta_pct: f64,
}
#[cfg(feature = "server")]
impl axum::response::IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let status = match self.code.as_str() {
"bad_request" | "invalid_input" => http::StatusCode::BAD_REQUEST,
"unauthorized" => http::StatusCode::UNAUTHORIZED,
"forbidden" => http::StatusCode::FORBIDDEN,
"not_found" => http::StatusCode::NOT_FOUND,
"conflict" => http::StatusCode::CONFLICT,
_ => http::StatusCode::INTERNAL_SERVER_ERROR,
};
(status, axum::Json(self)).into_response()
}
}