Skip to main content

greentic_deploy_spec/
traffic_split.rs

1//! `greentic.traffic-split.v1` (`§5.3`).
2//!
3//! One TrafficSplit per `deployment_id`. Entries sum to exactly 10,000 bps.
4
5use crate::error::SpecError;
6use crate::ids::{BundleId, DeploymentId, RevisionId};
7use crate::version::SchemaVersion;
8use chrono::{DateTime, Utc};
9use greentic_types::EnvId;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13const BASIS_POINTS_TOTAL: u32 = 10_000;
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct TrafficSplitEntry {
17    pub revision_id: RevisionId,
18    pub weight_bps: u32,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22pub struct TrafficSplit {
23    pub schema: SchemaVersion,
24    pub env_id: EnvId,
25    pub deployment_id: DeploymentId,
26    pub bundle_id: BundleId,
27    pub generation: u64,
28    pub entries: Vec<TrafficSplitEntry>,
29    pub updated_at: DateTime<Utc>,
30    pub updated_by: String,
31    pub idempotency_key: String,
32    pub authorization_ref: PathBuf,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub previous_split_ref: Option<PathBuf>,
35}
36
37impl TrafficSplit {
38    pub fn schema_str() -> &'static str {
39        SchemaVersion::TRAFFIC_SPLIT_V1
40    }
41
42    /// `§5.3`: schema discriminator equals `greentic.traffic-split.v1` and
43    /// the sum of `weight_bps` MUST equal 10,000.
44    ///
45    /// Sum widens into `u64` and rejects any per-entry value above 10,000 so a
46    /// crafted document like `[u32::MAX, 10001]` cannot wrap to exactly 10,000
47    /// in release builds.
48    pub fn validate(&self) -> Result<(), SpecError> {
49        if self.schema.as_str() != SchemaVersion::TRAFFIC_SPLIT_V1 {
50            return Err(SpecError::SchemaMismatch {
51                expected: SchemaVersion::TRAFFIC_SPLIT_V1,
52                actual: self.schema.as_str().to_string(),
53            });
54        }
55        let mut sum: u64 = 0;
56        for entry in &self.entries {
57            if entry.weight_bps > BASIS_POINTS_TOTAL {
58                return Err(SpecError::BasisPointsEntryTooLarge {
59                    value: entry.weight_bps,
60                });
61            }
62            sum += u64::from(entry.weight_bps);
63        }
64        if sum != u64::from(BASIS_POINTS_TOTAL) {
65            return Err(SpecError::BasisPointsSum { sum });
66        }
67        Ok(())
68    }
69}