use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApprovalRuleEvaluation {
pub index: u32,
pub condition: String,
pub matched: bool,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApprovalRequirement {
pub rule_index: Option<u32>,
pub condition: Option<String>,
pub required_approvers: u32,
#[serde(default)]
pub approver_groups: Vec<String>,
#[serde(default)]
pub evaluated: Vec<ApprovalRuleEvaluation>,
}
impl Default for ApprovalRequirement {
fn default() -> Self {
Self {
rule_index: None,
condition: None,
required_approvers: 1,
approver_groups: Vec::new(),
evaluated: Vec::new(),
}
}
}
impl ApprovalRequirement {
pub fn is_satisfied_by(&self, received: usize) -> bool {
received >= self.required_approvers as usize
}
pub fn allows_everyone(&self) -> bool {
self.approver_groups.is_empty()
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StepApproval {
pub user_id: Uuid,
pub approved_by: String,
pub at: DateTime<Utc>,
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, from_value, json, to_string};
use super::*;
fn requirement(required: u32) -> ApprovalRequirement {
ApprovalRequirement {
rule_index: Some(1),
condition: Some("labels.env == \"production\"".to_string()),
required_approvers: required,
approver_groups: vec!["sre".to_string()],
evaluated: vec![
ApprovalRuleEvaluation {
index: 0,
condition: "payload.amount > 10000".to_string(),
matched: false,
},
ApprovalRuleEvaluation {
index: 1,
condition: "labels.env == \"production\"".to_string(),
matched: true,
},
],
}
}
#[test]
fn requirement_serde_roundtrip() {
let req = requirement(3);
let json = to_string(&req).expect("serialize");
let back: ApprovalRequirement = from_str(&json).expect("deserialize");
assert_eq!(back, req);
}
#[test]
fn requirement_defaults_missing_lists() {
let back: ApprovalRequirement = from_value(json!({
"rule_index": null,
"condition": null,
"required_approvers": 1
}))
.expect("deserialize");
assert_eq!(back, ApprovalRequirement::default());
}
#[test]
fn default_requires_one_approval_from_anyone() {
let req = ApprovalRequirement::default();
assert_eq!(req.rule_index, None);
assert_eq!(req.condition, None);
assert_eq!(req.required_approvers, 1);
assert!(req.approver_groups.is_empty());
assert!(req.evaluated.is_empty());
assert!(req.allows_everyone());
}
#[test]
fn is_satisfied_by_boundaries() {
let req = requirement(3);
assert!(!req.is_satisfied_by(0));
assert!(!req.is_satisfied_by(2));
assert!(req.is_satisfied_by(3));
assert!(req.is_satisfied_by(4));
assert!(!req.allows_everyone());
}
#[test]
fn step_approval_serde_roundtrip() {
let vote = StepApproval {
user_id: Uuid::now_v7(),
approved_by: "élodie".to_string(),
at: Utc::now(),
};
let json = to_string(&vote).expect("serialize");
let back: StepApproval = from_str(&json).expect("deserialize");
assert_eq!(back, vote);
}
}