awaken_server_contract/contract/
scope.rs1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4pub const DEFAULT_SCOPE_ID: &str = "default";
5pub const MAX_SCOPE_ID_LEN: usize = 512;
6
7#[derive(Debug, Error, Clone, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ScopeError {
10 #[error("scope_id cannot be empty")]
11 Empty,
12 #[error("scope_id cannot exceed {max} bytes")]
13 TooLong { max: usize },
14 #[error("scope_id cannot contain control characters")]
15 ControlCharacter,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
19#[serde(try_from = "String", into = "String")]
20pub struct ScopeId(String);
21
22impl ScopeId {
23 pub fn new(value: impl Into<String>) -> Result<Self, ScopeError> {
24 let value = value.into();
25 if value.trim().is_empty() {
26 return Err(ScopeError::Empty);
27 }
28 if value.len() > MAX_SCOPE_ID_LEN {
29 return Err(ScopeError::TooLong {
30 max: MAX_SCOPE_ID_LEN,
31 });
32 }
33 if value.chars().any(char::is_control) {
34 return Err(ScopeError::ControlCharacter);
35 }
36 Ok(Self(value))
37 }
38
39 pub fn default_scope() -> Self {
40 Self(DEFAULT_SCOPE_ID.to_string())
41 }
42
43 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48impl Default for ScopeId {
49 fn default() -> Self {
50 Self::default_scope()
51 }
52}
53
54impl TryFrom<String> for ScopeId {
55 type Error = ScopeError;
56
57 fn try_from(value: String) -> Result<Self, Self::Error> {
58 Self::new(value)
59 }
60}
61
62impl TryFrom<&str> for ScopeId {
63 type Error = ScopeError;
64
65 fn try_from(value: &str) -> Result<Self, Self::Error> {
66 Self::new(value)
67 }
68}
69
70impl From<ScopeId> for String {
71 fn from(value: ScopeId) -> Self {
72 value.0
73 }
74}
75
76impl From<&ScopeId> for String {
77 fn from(value: &ScopeId) -> Self {
78 value.as_str().to_string()
79 }
80}
81
82impl AsRef<str> for ScopeId {
83 fn as_ref(&self) -> &str {
84 self.as_str()
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct ScopeContext {
90 pub scope_id: ScopeId,
91}
92
93pub fn scoped_key(scope_id: &ScopeId, value: &str) -> String {
94 let scope = scope_id.as_str();
95 format!("scope:{}:{}:{}", scope.len(), scope, value)
96}
97
98pub fn unscoped_key<'a>(scope_id: &ScopeId, value: &'a str) -> Option<&'a str> {
99 let scope = scope_id.as_str();
100 let prefix = format!("scope:{}:{}:", scope.len(), scope);
101 value.strip_prefix(&prefix)
102}
103
104impl ScopeContext {
105 pub fn new(scope_id: ScopeId) -> Self {
106 Self { scope_id }
107 }
108
109 pub fn default_scope() -> Self {
110 Self::new(ScopeId::default_scope())
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum RequestSurface {
117 Admin,
118 AgentInvoke,
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn scope_id_rejects_empty_values() {
127 assert_eq!(ScopeId::new(""), Err(ScopeError::Empty));
128 assert_eq!(ScopeId::new(" "), Err(ScopeError::Empty));
129 }
130
131 #[test]
132 fn scope_id_rejects_control_characters() {
133 assert_eq!(
134 ScopeId::new("workspace\n1"),
135 Err(ScopeError::ControlCharacter)
136 );
137 }
138
139 #[test]
140 fn scope_id_round_trips_as_string() {
141 let scope = ScopeId::new("workspace_123").unwrap();
142 let encoded = serde_json::to_string(&scope).unwrap();
143 assert_eq!(encoded, "\"workspace_123\"");
144 let decoded: ScopeId = serde_json::from_str(&encoded).unwrap();
145 assert_eq!(decoded, scope);
146 }
147}