use onlyne_config::Drive;
use onlyne_proto::{RoleInfo, Welcome};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoleSlice {
pub drive: Drive,
pub command: Vec<String>,
pub max_sessions: u32,
pub required_targets: Vec<String>,
}
impl RoleSlice {
pub fn from_welcome(welcome: &Welcome) -> Self {
let runtime = welcome.runtime.clone().unwrap_or_default();
Self {
drive: drive_of(runtime.drive),
command: runtime.command,
max_sessions: welcome.max_sessions,
required_targets: welcome.owes_targets.clone(),
}
}
pub fn from_role_info(info: &RoleInfo, _current: &RoleSlice) -> Self {
Self {
drive: drive_of(info.runtime.drive),
command: info.runtime.command.clone(),
max_sessions: info.max_sessions,
required_targets: info.owes_targets.clone(),
}
}
}
pub fn drive_of(wire: onlyne_proto::Drive) -> Drive {
match wire {
onlyne_proto::Drive::Plugin => Drive::Plugin,
onlyne_proto::Drive::Acp => Drive::Acp,
onlyne_proto::Drive::Exec => Drive::Exec,
}
}
pub fn slice_diff(current: &RoleSlice, next: &RoleSlice) -> Vec<&'static str> {
let mut fields = Vec::new();
if current.drive != next.drive || current.command != next.command {
fields.push("runtime");
}
if current.max_sessions != next.max_sessions {
fields.push("max_sessions");
}
if current.required_targets != next.required_targets {
fields.push("owes_targets");
}
fields
}
pub fn apply_if_changed(
current: &RoleSlice,
next: RoleSlice,
) -> Option<(RoleSlice, Vec<&'static str>)> {
let fields = slice_diff(current, &next);
if fields.is_empty() {
None
} else {
Some((next, fields))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn welcome(edges: &[&str], owes: &[&str]) -> Welcome {
Welcome {
cluster: "cluster-a".to_string(),
server: "srv".to_string(),
role: "planner".to_string(),
admin: false,
max_sessions: 3,
prose: String::new(),
spec_hash: "hash".to_string(),
aggregate: None,
allowed_targets: edges.iter().map(|name| name.to_string()).collect(),
owes_targets: owes.iter().map(|name| name.to_string()).collect(),
allowed_senders: Vec::new(),
runtime: None,
timeout_ready_ms: None,
timeout_idle_ms: None,
intent_attempts: None,
intent_backoff_ms: None,
seq: 1,
}
}
#[test]
fn the_slice_owes_owes_targets_not_the_reach() {
let slice = RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &["writer"]));
assert_eq!(slice.required_targets, ["writer"], "{slice:?}");
let reach_only = RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &[]));
assert!(
reach_only.required_targets.is_empty(),
"a role that declares no obligation owes nothing: {reach_only:?}"
);
}
#[test]
fn the_slice_diff_reports_the_obligation_alone() {
let base = RoleSlice::from_welcome(&welcome(&["writer"], &["writer"]));
let moved_obligation =
RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &["auditor"]));
assert_eq!(
slice_diff(&base, &moved_obligation),
vec!["owes_targets"],
"the reach moved too, and it is the server's to read"
);
let moved_reach = RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &["writer"]));
assert!(
slice_diff(&base, &moved_reach).is_empty(),
"a reach-only change reconfigures nothing on the client"
);
}
}