use serde::Deserialize;
use super::chain_ref::ChainRef;
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct BranchChainConfig {
pub name: String,
pub chains: Vec<ChainRef>,
#[serde(default)]
pub max_iterations: Option<u32>,
#[serde(default)]
pub on_result: Option<BranchCondition>,
#[serde(default = "default_rejoin")]
pub rejoin: String,
}
fn default_rejoin() -> String {
"next".to_owned()
}
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct BranchCondition {
pub filter: String,
#[serde(default = "default_result_key")]
pub key: String,
#[serde(rename = "result")]
pub value: String,
}
fn default_result_key() -> String {
"status".to_owned()
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::needless_raw_strings,
clippy::needless_raw_string_hashes,
reason = "tests use unwrap/expect/indexing/raw strings for brevity"
)]
mod tests {
use super::*;
#[test]
fn parse_branch_with_on_result() {
let yaml = r#"
name: cache_hit
on_result:
filter: cache
result: hit
rejoin: terminal
chains:
- serve_cached
"#;
let branch: BranchChainConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(branch.name, "cache_hit", "branch name mismatch");
assert_eq!(branch.rejoin, "terminal", "rejoin mismatch");
assert!(branch.on_result.is_some(), "on_result should be present");
let cond = branch.on_result.unwrap();
assert_eq!(cond.filter, "cache", "condition filter mismatch");
assert_eq!(cond.key, "status", "condition key should default to 'status'");
assert_eq!(cond.value, "hit", "condition value mismatch");
}
#[test]
fn parse_unconditional_branch() {
let yaml = r#"
name: always_run
chains:
- utility_chain
"#;
let branch: BranchChainConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(branch.name, "always_run", "branch name mismatch");
assert!(
branch.on_result.is_none(),
"unconditional branch should have no on_result"
);
assert_eq!(branch.rejoin, "next", "default rejoin should be 'next'");
assert!(branch.max_iterations.is_none(), "max_iterations should default to None");
}
#[test]
fn parse_branch_with_max_iterations() {
let yaml = r#"
name: retry
on_result:
filter: auth
key: action
result: retry
rejoin: auth
max_iterations: 3
chains:
- name: refresh
filters:
- filter: headers
"#;
let branch: BranchChainConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(branch.max_iterations, Some(3), "max_iterations should be 3");
assert_eq!(branch.rejoin, "auth", "rejoin should be 'auth'");
}
#[test]
fn parse_branch_condition_custom_key() {
let yaml = r#"
filter: classifier
key: tier
result: premium
"#;
let cond: BranchCondition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cond.filter, "classifier", "filter mismatch");
assert_eq!(cond.key, "tier", "custom key mismatch");
assert_eq!(cond.value, "premium", "value mismatch");
}
#[test]
fn parse_branch_condition_default_key() {
let yaml = r#"
filter: cache
result: miss
"#;
let cond: BranchCondition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cond.key, "status", "key should default to 'status'");
}
#[test]
fn parse_branch_with_multiple_chains() {
let yaml = r#"
name: multi
chains:
- chain_a
- chain_b
- name: inline_chain
filters:
- filter: headers
"#;
let branch: BranchChainConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(branch.chains.len(), 3, "should have 3 chain refs");
}
#[test]
fn parse_branch_with_named_rejoin() {
let yaml = r#"
name: skip_to_routing
rejoin: routing
chains:
- guardrails
"#;
let branch: BranchChainConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(branch.rejoin, "routing", "rejoin should be 'routing'");
}
#[test]
fn parse_branch_with_cross_chain_rejoin() {
let yaml = r#"
name: cross
rejoin: "main:routing"
chains:
- utility
"#;
let branch: BranchChainConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(branch.rejoin, "main:routing", "cross-chain rejoin should be preserved");
}
}