Skip to main content

awaken_server_contract/contract/
scope.rs

1use 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}