use crate::protocol_abstraction::Protocol;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum TransitionMode {
TimeBased,
#[default]
Manual,
Scheduled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum MergeStrategy {
#[default]
FieldLevel,
Weighted,
BodyBlend,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ContinuumConfig {
#[serde(default = "default_false")]
pub enabled: bool,
#[serde(default = "default_blend_ratio")]
pub default_ratio: f64,
#[serde(default)]
pub transition_mode: TransitionMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_schedule: Option<super::schedule::TimeSchedule>,
#[serde(default)]
pub merge_strategy: MergeStrategy,
#[serde(default)]
pub routes: Vec<ContinuumRule>,
#[serde(default)]
pub groups: HashMap<String, f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub field_mixing: Option<super::field_mixer::FieldRealityConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_protocol_state: Option<CrossProtocolStateConfig>,
}
fn default_false() -> bool {
false
}
fn default_blend_ratio() -> f64 {
0.0 }
impl Default for ContinuumConfig {
fn default() -> Self {
Self {
enabled: false,
default_ratio: 0.0,
transition_mode: TransitionMode::Manual,
time_schedule: None,
merge_strategy: MergeStrategy::FieldLevel,
routes: Vec::new(),
groups: HashMap::new(),
field_mixing: None,
cross_protocol_state: None,
}
}
}
impl ContinuumConfig {
pub fn new() -> Self {
Self::default()
}
pub fn enable(mut self) -> Self {
self.enabled = true;
self
}
pub fn with_default_ratio(mut self, ratio: f64) -> Self {
self.default_ratio = ratio.clamp(0.0, 1.0);
self
}
pub fn with_transition_mode(mut self, mode: TransitionMode) -> Self {
self.transition_mode = mode;
self
}
pub fn with_time_schedule(mut self, schedule: super::schedule::TimeSchedule) -> Self {
self.time_schedule = Some(schedule);
self
}
pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
self.merge_strategy = strategy;
self
}
pub fn add_route(mut self, rule: ContinuumRule) -> Self {
self.routes.push(rule);
self
}
pub fn set_group_ratio(mut self, group: String, ratio: f64) -> Self {
self.groups.insert(group, ratio.clamp(0.0, 1.0));
self
}
pub fn with_cross_protocol_state(mut self, config: CrossProtocolStateConfig) -> Self {
self.cross_protocol_state = Some(config);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CrossProtocolStateConfig {
pub state_model: String,
#[serde(default)]
pub share_state_across: Vec<Protocol>,
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Default for CrossProtocolStateConfig {
fn default() -> Self {
Self {
state_model: "default".to_string(),
share_state_across: vec![Protocol::Http, Protocol::WebSocket, Protocol::Grpc],
enabled: true,
}
}
}
impl CrossProtocolStateConfig {
pub fn new(state_model: String) -> Self {
Self {
state_model,
share_state_across: Vec::new(),
enabled: true,
}
}
pub fn add_protocol(mut self, protocol: Protocol) -> Self {
if !self.share_state_across.contains(&protocol) {
self.share_state_across.push(protocol);
}
self
}
pub fn with_protocols(mut self, protocols: Vec<Protocol>) -> Self {
self.share_state_across = protocols;
self
}
pub fn should_share_state(&self, protocol: &Protocol) -> bool {
self.enabled && self.share_state_across.contains(protocol)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ContinuumRule {
pub pattern: String,
pub ratio: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_true() -> bool {
true
}
impl ContinuumRule {
pub fn new(pattern: String, ratio: f64) -> Self {
Self {
pattern,
ratio: ratio.clamp(0.0, 1.0),
group: None,
enabled: true,
}
}
pub fn with_group(mut self, group: String) -> Self {
self.group = Some(group);
self
}
pub fn matches_path(&self, path: &str) -> bool {
if !self.enabled {
return false;
}
if self.pattern.ends_with("/*") {
let prefix = &self.pattern[..self.pattern.len() - 2];
if let Some(remaining) = path.strip_prefix(prefix) {
!remaining.is_empty() && remaining != "/"
} else {
false
}
} else {
path == self.pattern
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_continuum_config_default() {
let config = ContinuumConfig::default();
assert!(!config.enabled);
assert_eq!(config.default_ratio, 0.0);
assert_eq!(config.transition_mode, TransitionMode::Manual);
}
#[test]
fn test_continuum_config_builder() {
let config = ContinuumConfig::new()
.enable()
.with_default_ratio(0.5)
.with_transition_mode(TransitionMode::TimeBased);
assert!(config.enabled);
assert_eq!(config.default_ratio, 0.5);
assert_eq!(config.transition_mode, TransitionMode::TimeBased);
}
#[test]
fn test_continuum_rule_matching() {
let rule = ContinuumRule::new("/api/users/*".to_string(), 0.5);
assert!(rule.matches_path("/api/users/123"));
assert!(rule.matches_path("/api/users/456"));
assert!(!rule.matches_path("/api/orders/123"));
let exact_rule = ContinuumRule::new("/api/health".to_string(), 0.0);
assert!(exact_rule.matches_path("/api/health"));
assert!(!exact_rule.matches_path("/api/health/check"));
}
#[test]
fn test_ratio_clamping() {
let rule = ContinuumRule::new("/test".to_string(), 1.5);
assert_eq!(rule.ratio, 1.0);
let rule = ContinuumRule::new("/test".to_string(), -0.5);
assert_eq!(rule.ratio, 0.0);
}
}