onlyne_client/session/
slice.rs1use onlyne_config::Drive;
14use onlyne_proto::{RoleInfo, Welcome};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RoleSlice {
19 pub drive: Drive,
21 pub command: Vec<String>,
22 pub max_sessions: u32,
23 pub required_targets: Vec<String>,
27}
28
29impl RoleSlice {
30 pub fn from_welcome(welcome: &Welcome) -> Self {
31 let runtime = welcome.runtime.clone().unwrap_or_default();
32 Self {
33 drive: drive_of(runtime.drive),
34 command: runtime.command,
35 max_sessions: welcome.max_sessions,
36 required_targets: welcome.owes_targets.clone(),
37 }
38 }
39
40 pub fn from_role_info(info: &RoleInfo, _current: &RoleSlice) -> Self {
41 Self {
42 drive: drive_of(info.runtime.drive),
43 command: info.runtime.command.clone(),
44 max_sessions: info.max_sessions,
45 required_targets: info.owes_targets.clone(),
46 }
47 }
48}
49
50pub fn drive_of(wire: onlyne_proto::Drive) -> Drive {
57 match wire {
58 onlyne_proto::Drive::Plugin => Drive::Plugin,
59 onlyne_proto::Drive::Acp => Drive::Acp,
60 onlyne_proto::Drive::Exec => Drive::Exec,
61 }
62}
63
64pub fn slice_diff(current: &RoleSlice, next: &RoleSlice) -> Vec<&'static str> {
66 let mut fields = Vec::new();
67 if current.drive != next.drive || current.command != next.command {
68 fields.push("runtime");
69 }
70 if current.max_sessions != next.max_sessions {
71 fields.push("max_sessions");
72 }
73 if current.required_targets != next.required_targets {
74 fields.push("owes_targets");
75 }
76 fields
77}
78
79pub fn apply_if_changed(
81 current: &RoleSlice,
82 next: RoleSlice,
83) -> Option<(RoleSlice, Vec<&'static str>)> {
84 let fields = slice_diff(current, &next);
85 if fields.is_empty() {
86 None
87 } else {
88 Some((next, fields))
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn welcome(edges: &[&str], owes: &[&str]) -> Welcome {
97 Welcome {
98 cluster: "cluster-a".to_string(),
99 server: "srv".to_string(),
100 role: "planner".to_string(),
101 admin: false,
102 max_sessions: 3,
103 prose: String::new(),
104 spec_hash: "hash".to_string(),
105 aggregate: None,
106 allowed_targets: edges.iter().map(|name| name.to_string()).collect(),
107 owes_targets: owes.iter().map(|name| name.to_string()).collect(),
108 allowed_senders: Vec::new(),
109 runtime: None,
110 timeout_ready_ms: None,
111 timeout_idle_ms: None,
112 intent_attempts: None,
113 intent_backoff_ms: None,
114 seq: 1,
115 }
116 }
117
118 #[test]
122 fn the_slice_owes_owes_targets_not_the_reach() {
123 let slice = RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &["writer"]));
124 assert_eq!(slice.required_targets, ["writer"], "{slice:?}");
125
126 let reach_only = RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &[]));
127 assert!(
128 reach_only.required_targets.is_empty(),
129 "a role that declares no obligation owes nothing: {reach_only:?}"
130 );
131 }
132
133 #[test]
136 fn the_slice_diff_reports_the_obligation_alone() {
137 let base = RoleSlice::from_welcome(&welcome(&["writer"], &["writer"]));
138 let moved_obligation =
139 RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &["auditor"]));
140 assert_eq!(
141 slice_diff(&base, &moved_obligation),
142 vec!["owes_targets"],
143 "the reach moved too, and it is the server's to read"
144 );
145 let moved_reach = RoleSlice::from_welcome(&welcome(&["writer", "auditor"], &["writer"]));
146 assert!(
147 slice_diff(&base, &moved_reach).is_empty(),
148 "a reach-only change reconfigures nothing on the client"
149 );
150 }
151}