use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug)]
pub enum GateError {
IoError(std::io::Error),
TomlError(toml::de::Error),
InvalidPointer(String),
TypeMismatch { expected: String, actual: String },
InvalidOperator { op: String, value_type: String },
MissingField { name: String, field: String },
}
impl std::fmt::Display for GateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoError(e) => write!(f, "Failed to read policy file: {e}"),
Self::TomlError(e) => write!(f, "Failed to parse policy TOML: {e}"),
Self::InvalidPointer(p) => write!(f, "Invalid JSON pointer: {p}"),
Self::TypeMismatch { expected, actual } => {
write!(f, "Type mismatch: expected {expected}, got {actual}")
}
Self::InvalidOperator { op, value_type } => {
write!(f, "Invalid operator '{op}' for type '{value_type}'")
}
Self::MissingField { name, field } => {
write!(f, "Rule '{name}' missing required field: {field}")
}
}
}
}
impl std::error::Error for GateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(e) => Some(e),
Self::TomlError(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for GateError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<toml::de::Error> for GateError {
fn from(err: toml::de::Error) -> Self {
Self::TomlError(err)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct PolicyConfig {
pub rules: Vec<PolicyRule>,
#[serde(default)]
pub fail_fast: bool,
#[serde(default)]
pub allow_missing: bool,
}
impl PolicyConfig {
pub fn from_toml(s: &str) -> Result<Self, GateError> {
Ok(toml::from_str(s)?)
}
pub fn from_file(path: &Path) -> Result<Self, GateError> {
let content = std::fs::read_to_string(path)?;
Self::from_toml(&content)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
pub name: String,
pub pointer: String,
pub op: RuleOperator,
#[serde(default)]
pub value: Option<serde_json::Value>,
#[serde(default)]
pub values: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub negate: bool,
#[serde(default)]
pub level: RuleLevel,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RuleOperator {
Gt,
Gte,
Lt,
Lte,
#[default]
Eq,
Ne,
In,
Contains,
Exists,
}
impl std::fmt::Display for RuleOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuleOperator::Gt => write!(f, ">"),
RuleOperator::Gte => write!(f, ">="),
RuleOperator::Lt => write!(f, "<"),
RuleOperator::Lte => write!(f, "<="),
RuleOperator::Eq => write!(f, "=="),
RuleOperator::Ne => write!(f, "!="),
RuleOperator::In => write!(f, "in"),
RuleOperator::Contains => write!(f, "contains"),
RuleOperator::Exists => write!(f, "exists"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum RuleLevel {
Warn,
#[default]
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateResult {
pub passed: bool,
pub rule_results: Vec<RuleResult>,
pub errors: usize,
pub warnings: usize,
}
impl GateResult {
pub fn from_results(rule_results: Vec<RuleResult>) -> Self {
let errors = rule_results
.iter()
.filter(|r| !r.passed && r.level == RuleLevel::Error)
.count();
let warnings = rule_results
.iter()
.filter(|r| !r.passed && r.level == RuleLevel::Warn)
.count();
let passed = errors == 0;
Self {
passed,
rule_results,
errors,
warnings,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleResult {
pub name: String,
pub passed: bool,
pub level: RuleLevel,
pub actual: Option<serde_json::Value>,
pub expected: String,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatchetRule {
pub pointer: String,
#[serde(default)]
pub max_increase_pct: Option<f64>,
#[serde(default)]
pub max_value: Option<f64>,
#[serde(default)]
pub level: RuleLevel,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatchetResult {
pub rule: RatchetRule,
pub passed: bool,
pub baseline_value: Option<f64>,
pub current_value: f64,
pub change_pct: Option<f64>,
pub message: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct RatchetConfig {
pub rules: Vec<RatchetRule>,
#[serde(default)]
pub fail_fast: bool,
#[serde(default)]
pub allow_missing_baseline: bool,
#[serde(default)]
pub allow_missing_current: bool,
}
impl RatchetConfig {
pub fn from_toml(s: &str) -> Result<Self, GateError> {
Ok(toml::from_str(s)?)
}
pub fn from_file(path: &Path) -> Result<Self, GateError> {
let content = std::fs::read_to_string(path)?;
Self::from_toml(&content)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatchetGateResult {
pub passed: bool,
pub ratchet_results: Vec<RatchetResult>,
pub errors: usize,
pub warnings: usize,
}
impl RatchetGateResult {
pub fn from_results(ratchet_results: Vec<RatchetResult>) -> Self {
let errors = ratchet_results
.iter()
.filter(|r| !r.passed && r.rule.level == RuleLevel::Error)
.count();
let warnings = ratchet_results
.iter()
.filter(|r| !r.passed && r.rule.level == RuleLevel::Warn)
.count();
let passed = errors == 0;
Self {
passed,
ratchet_results,
errors,
warnings,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_policy() {
let toml = r#"
fail_fast = true
allow_missing = false
[[rules]]
name = "max_tokens"
pointer = "/derived/totals/tokens"
op = "lte"
value = 500000
level = "error"
message = "Too many tokens"
[[rules]]
name = "has_license"
pointer = "/license/effective"
op = "exists"
level = "warn"
"#;
let policy = PolicyConfig::from_toml(toml).unwrap();
assert!(policy.fail_fast);
assert!(!policy.allow_missing);
assert_eq!(policy.rules.len(), 2);
assert_eq!(policy.rules[0].name, "max_tokens");
assert_eq!(policy.rules[0].op, RuleOperator::Lte);
assert_eq!(policy.rules[1].op, RuleOperator::Exists);
}
#[test]
fn test_gate_result() {
let results = vec![
RuleResult {
name: "rule1".into(),
passed: true,
level: RuleLevel::Error,
actual: None,
expected: "test".into(),
message: None,
},
RuleResult {
name: "rule2".into(),
passed: false,
level: RuleLevel::Warn,
actual: None,
expected: "test".into(),
message: Some("Warning".into()),
},
];
let gate = GateResult::from_results(results);
assert!(gate.passed); assert_eq!(gate.errors, 0);
assert_eq!(gate.warnings, 1);
}
#[test]
fn test_policy_from_file() {
use std::time::{SystemTime, UNIX_EPOCH};
let toml = r#"
fail_fast = true
allow_missing = false
[[rules]]
name = "max_tokens"
pointer = "/derived/totals/tokens"
op = "lte"
value = 500000
level = "error"
"#;
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!("tokmd-gate-policy-{nanos}.toml"));
std::fs::write(&path, toml).unwrap();
let policy = PolicyConfig::from_file(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert!(policy.fail_fast);
assert_eq!(policy.rules.len(), 1);
assert_eq!(policy.rules[0].name, "max_tokens");
assert_eq!(policy.rules[0].op, RuleOperator::Lte);
}
#[test]
fn test_rule_operator_display() {
assert_eq!(RuleOperator::Gt.to_string(), ">");
assert_eq!(RuleOperator::Gte.to_string(), ">=");
assert_eq!(RuleOperator::Lt.to_string(), "<");
assert_eq!(RuleOperator::Lte.to_string(), "<=");
assert_eq!(RuleOperator::Eq.to_string(), "==");
assert_eq!(RuleOperator::Ne.to_string(), "!=");
assert_eq!(RuleOperator::In.to_string(), "in");
assert_eq!(RuleOperator::Contains.to_string(), "contains");
assert_eq!(RuleOperator::Exists.to_string(), "exists");
}
#[test]
fn test_gate_result_counts_only_failed_rules() {
let results = vec![
RuleResult {
name: "passed_warn".into(),
passed: true,
level: RuleLevel::Warn,
actual: None,
expected: "x".into(),
message: None,
},
RuleResult {
name: "failed_warn".into(),
passed: false,
level: RuleLevel::Warn,
actual: None,
expected: "x".into(),
message: Some("warn".into()),
},
];
let gate = GateResult::from_results(results);
assert!(gate.passed); assert_eq!(gate.errors, 0);
assert_eq!(gate.warnings, 1);
}
}