1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
19pub struct Scope {
20 #[serde(default = "default_org")]
22 pub org_id: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub agent_id: Option<String>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub user_id: Option<String>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub session_id: Option<String>,
35}
36
37fn default_org() -> String {
38 "default".to_string()
39}
40
41impl Default for Scope {
42 fn default() -> Self {
43 Self {
44 org_id: default_org(),
45 agent_id: None,
46 user_id: None,
47 session_id: None,
48 }
49 }
50}
51
52impl Scope {
53 pub fn org(org_id: impl Into<String>) -> Self {
55 Self {
56 org_id: org_id.into(),
57 ..Default::default()
58 }
59 }
60
61 pub fn user(org_id: impl Into<String>, user_id: impl Into<String>) -> Self {
63 Self {
64 org_id: org_id.into(),
65 user_id: Some(user_id.into()),
66 ..Default::default()
67 }
68 }
69
70 pub fn session(
72 org_id: impl Into<String>,
73 user_id: impl Into<String>,
74 session_id: impl Into<String>,
75 ) -> Self {
76 Self {
77 org_id: org_id.into(),
78 user_id: Some(user_id.into()),
79 session_id: Some(session_id.into()),
80 ..Default::default()
81 }
82 }
83
84 pub fn full(
86 org_id: impl Into<String>,
87 agent_id: impl Into<String>,
88 user_id: impl Into<String>,
89 session_id: impl Into<String>,
90 ) -> Self {
91 Self {
92 org_id: org_id.into(),
93 agent_id: Some(agent_id.into()),
94 user_id: Some(user_id.into()),
95 session_id: Some(session_id.into()),
96 }
97 }
98
99 pub fn depth(&self) -> u8 {
104 match (&self.user_id, &self.session_id, &self.agent_id) {
105 (None, None, None) => 0,
106 (None, None, Some(_)) => 0, (Some(_), None, None) => 1,
108 (Some(_), None, Some(_)) => 1,
109 (Some(_), Some(_), None) => 2,
110 (Some(_), Some(_), Some(_)) => 3,
111 (None, Some(_), _) => 0, }
113 }
114
115 pub fn contains(&self, other: &Scope) -> bool {
122 if self.org_id != other.org_id {
123 return false;
124 }
125 if let Some(ref a) = self.agent_id {
126 if other.agent_id.as_deref() != Some(a.as_str()) {
127 return false;
128 }
129 }
130 if let Some(ref u) = self.user_id {
131 if other.user_id.as_deref() != Some(u.as_str()) {
132 return false;
133 }
134 }
135 if let Some(ref s) = self.session_id {
136 if other.session_id.as_deref() != Some(s.as_str()) {
137 return false;
138 }
139 }
140 true
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn depth_org_only() {
150 assert_eq!(Scope::org("acme").depth(), 0);
151 }
152
153 #[test]
154 fn depth_user() {
155 assert_eq!(Scope::user("acme", "alice").depth(), 1);
156 }
157
158 #[test]
159 fn depth_session() {
160 assert_eq!(Scope::session("acme", "alice", "s1").depth(), 2);
161 }
162
163 #[test]
164 fn depth_full() {
165 assert_eq!(Scope::full("acme", "agent-1", "alice", "s1").depth(), 3);
166 }
167
168 #[test]
169 fn contains_self() {
170 let s = Scope::session("acme", "alice", "s1");
171 assert!(s.contains(&s));
172 }
173
174 #[test]
175 fn org_contains_user() {
176 let org = Scope::org("acme");
177 let user = Scope::user("acme", "alice");
178 assert!(org.contains(&user));
179 assert!(!user.contains(&org));
180 }
181
182 #[test]
183 fn different_org_not_contained() {
184 let a = Scope::org("acme");
185 let b = Scope::org("globex");
186 assert!(!a.contains(&b));
187 }
188
189 #[test]
190 fn serialization_omits_none_fields() {
191 let s = Scope::user("acme", "alice");
192 let json = serde_json::to_string(&s).unwrap();
193 assert!(json.contains("\"org_id\""));
194 assert!(json.contains("\"user_id\""));
195 assert!(!json.contains("\"session_id\""));
196 assert!(!json.contains("\"agent_id\""));
197 }
198
199 #[test]
200 fn default_scope_org_is_default() {
201 let s = Scope::default();
202 assert_eq!(s.org_id, "default");
203 }
204}