use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub name: String,
#[serde(default)]
pub value: Value,
#[serde(default)]
pub overrides: Vec<Override>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Override {
pub name: String,
#[serde(default)]
pub conditions: Vec<Condition>,
#[serde(default)]
pub value: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "operator", rename_all = "snake_case")]
pub enum Condition {
Equals {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
In {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
NotIn {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
LessThan {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
LessThanOrEqual {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
GreaterThan {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
GreaterThanOrEqual {
property: String,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
value: Option<Value>,
},
#[serde(rename_all = "camelCase")]
Segmentation {
property: String,
from_percentage: f64,
to_percentage: f64,
seed: String,
},
And {
conditions: Vec<Condition>,
},
Or {
conditions: Vec<Condition>,
},
Not {
condition: Box<Condition>,
},
#[serde(other)]
Unsupported,
}
fn present<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Value>, D::Error> {
Value::deserialize(deserializer).map(Some)
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Snapshot {
pub configs: Vec<Config>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigChange {
pub name: String,
pub value: Value,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StartReplicationStreamBody<'a> {
pub current_configs: &'a [Config],
pub required_configs: &'a [String],
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ReplicationStreamRecord {
Init { configs: Vec<Config> },
ConfigChange { config: Config },
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_server_payload() {
let raw = json!({
"name": "rate-limit",
"value": 100,
"overrides": [{
"name": "vip",
"value": 1000,
"conditions": [
{"operator": "in", "property": "plan", "value": ["pro", "enterprise"]},
{"operator": "segmentation", "property": "userId", "fromPercentage": 0, "toPercentage": 50, "seed": "s"},
{"operator": "not", "__sentinel": null, "condition": {"operator": "equals", "property": "region"}},
{"operator": "regex_match", "property": "email", "value": ".*"}
]
}]
});
let config: Config = serde_json::from_value(raw).unwrap();
let conditions = &config.overrides[0].conditions;
assert!(matches!(
&conditions[0],
Condition::In { value: Some(_), .. }
));
assert!(matches!(
&conditions[1],
Condition::Segmentation { to_percentage, .. } if *to_percentage == 50.0
));
assert!(matches!(
&conditions[2],
Condition::Not { condition } if matches!(**condition, Condition::Equals { value: None, .. })
));
assert_eq!(conditions[3], Condition::Unsupported);
}
#[test]
fn distinguishes_null_from_missing_value() {
let null: Condition =
serde_json::from_value(json!({"operator": "equals", "property": "p", "value": null}))
.unwrap();
assert_eq!(
null,
Condition::Equals {
property: "p".into(),
value: Some(Value::Null)
}
);
let missing: Condition =
serde_json::from_value(json!({"operator": "equals", "property": "p"})).unwrap();
assert_eq!(
missing,
Condition::Equals {
property: "p".into(),
value: None
}
);
}
#[test]
fn snapshot_round_trips() {
let snapshot = Snapshot {
configs: vec![Config {
name: "flag".into(),
value: json!(true),
overrides: vec![Override {
name: "o".into(),
conditions: vec![Condition::Segmentation {
property: "userId".into(),
from_percentage: 0.0,
to_percentage: 10.0,
seed: "x".into(),
}],
value: json!(false),
}],
}],
};
let json = serde_json::to_value(&snapshot).unwrap();
assert_eq!(
json["configs"][0]["overrides"][0]["conditions"][0]["fromPercentage"],
json!(0.0)
);
let back: Snapshot = serde_json::from_value(json).unwrap();
assert_eq!(back, snapshot);
}
}