hara_native/runtime/
session_model.rs1use std::fmt;
2
3#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
5pub struct SessionId(String);
6
7impl SessionId {
8 pub fn parse(value: &str) -> Result<Self, String> {
9 if value.is_empty()
10 || !value
11 .bytes()
12 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
13 {
14 return Err("INVALID_SESSION_NAME".into());
15 }
16 Ok(Self(value.into()))
17 }
18
19 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22}
23
24impl AsRef<str> for SessionId {
25 fn as_ref(&self) -> &str {
26 self.as_str()
27 }
28}
29
30impl fmt::Display for SessionId {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 formatter.write_str(self.as_str())
33 }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct SessionMountId(u64);
39
40impl SessionMountId {
41 pub const fn new(value: u64) -> Self {
42 assert!(value > 0, "filesystem mount identifiers must be positive");
43 Self(value)
44 }
45
46 pub const fn get(self) -> u64 {
47 self.0
48 }
49}
50
51impl fmt::Display for SessionMountId {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 self.0.fmt(formatter)
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum SessionState {
60 New,
61 Active,
62 Closed,
63}
64
65impl SessionState {
66 pub const fn as_str(self) -> &'static str {
67 match self {
68 Self::New => "new",
69 Self::Active => "active",
70 Self::Closed => "closed",
71 }
72 }
73}
74
75impl fmt::Display for SessionState {
76 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77 formatter.write_str(self.as_str())
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct SessionAuthorityPolicy {
89 pub host_filesystem: bool,
90 pub host_network: bool,
91 pub host_process: bool,
92 pub reflection: bool,
93 pub packages: bool,
94 pub project: bool,
95}
96
97impl SessionAuthorityPolicy {
98 pub const ZERO: Self = Self {
99 host_filesystem: false,
100 host_network: false,
101 host_process: false,
102 reflection: false,
103 packages: false,
104 project: false,
105 };
106
107 pub const fn profile(self) -> &'static str {
108 if !self.host_filesystem
109 && !self.host_network
110 && !self.host_process
111 && !self.reflection
112 && !self.packages
113 && !self.project
114 {
115 "zero"
116 } else {
117 "explicit"
118 }
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct SessionSpec {
129 pub id: SessionId,
130 pub authority: SessionAuthorityPolicy,
131}
132
133impl SessionSpec {
134 pub fn new(id: SessionId, authority: SessionAuthorityPolicy) -> Self {
135 Self { id, authority }
136 }
137
138 pub fn zero_authority(name: &str) -> Result<Self, String> {
139 Ok(Self::new(
140 SessionId::parse(name)?,
141 SessionAuthorityPolicy::ZERO,
142 ))
143 }
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct SessionStatus {
149 pub name: SessionId,
150 pub namespace: String,
151 pub state: SessionState,
152 pub filesystem: Option<SessionMountId>,
153 pub authority: SessionAuthorityPolicy,
154}
155
156pub type SessionMetadata = SessionStatus;
158
159#[cfg(test)]
160mod session_model_tests {
161 use super::*;
162
163 #[test]
164 fn session_identity_and_mount_identifiers_are_distinct_types() {
165 let session = SessionId::parse("workspace.alpha").unwrap();
166 let mount = SessionMountId::new(7);
167 assert_eq!(session.as_str(), "workspace.alpha");
168 assert_eq!(mount.get(), 7);
169 assert_eq!(mount.to_string(), "7");
170 assert!(SessionId::parse("bad/name").is_err());
171 }
172
173 #[test]
174 #[should_panic(expected = "filesystem mount identifiers must be positive")]
175 fn mount_identifiers_reject_zero() {
176 SessionMountId::new(0);
177 }
178
179 #[test]
180 fn zero_authority_spec_is_explicit_and_immutable() {
181 let spec = SessionSpec::zero_authority("child").unwrap();
182 assert_eq!(spec.id.as_str(), "child");
183 assert_eq!(spec.authority, SessionAuthorityPolicy::ZERO);
184 assert_eq!(spec.authority.profile(), "zero");
185 }
186
187 #[test]
188 fn session_lifecycle_states_are_explicit() {
189 assert_eq!(SessionState::New.as_str(), "new");
190 assert_eq!(SessionState::Active.as_str(), "active");
191 assert_eq!(SessionState::Closed.as_str(), "closed");
192 }
193}