1use std::fmt;
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13pub const MAX_HANDLE_BYTES: usize = 256;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct HandleError {
19 pub len: usize,
20}
21
22impl fmt::Display for HandleError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 write!(
25 f,
26 "a scope handle is 1 to {MAX_HANDLE_BYTES} bytes, not {}",
27 self.len
28 )
29 }
30}
31
32impl std::error::Error for HandleError {}
33
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct Handle(String);
39
40impl Handle {
41 pub fn new(s: impl Into<String>) -> Result<Self, HandleError> {
42 let s = s.into();
43 if s.is_empty() || s.len() > MAX_HANDLE_BYTES {
44 return Err(HandleError { len: s.len() });
45 }
46 Ok(Self(s))
47 }
48
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54impl Serialize for Handle {
55 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
56 s.serialize_str(&self.0)
57 }
58}
59
60impl schemars::JsonSchema for Handle {
61 fn schema_name() -> std::borrow::Cow<'static, str> {
62 "Handle".into()
63 }
64
65 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
68 schemars::json_schema!({
69 "type": "string",
70 "minLength": 1,
71 "maxLength": MAX_HANDLE_BYTES
72 })
73 }
74}
75
76impl<'de> Deserialize<'de> for Handle {
77 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
78 Handle::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
79 }
80}
81
82#[derive(
84 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
85)]
86#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
87pub enum Scope {
88 TenantOnly {
92 tenant: Handle,
93 context_revision: Handle,
94 },
95 Principal {
98 tenant: Handle,
99 context_revision: Handle,
100 principal_scope: Handle,
101 },
102}
103
104impl Scope {
105 pub fn principal(tenant: Handle, context_revision: Handle, principal_scope: Handle) -> Self {
107 Scope::Principal {
108 tenant,
109 context_revision,
110 principal_scope,
111 }
112 }
113
114 pub fn tenant_only(tenant: Handle, context_revision: Handle) -> Self {
116 Scope::TenantOnly {
117 tenant,
118 context_revision,
119 }
120 }
121
122 pub fn mode(&self) -> &'static str {
123 match self {
124 Scope::TenantOnly { .. } => "tenant_only",
125 Scope::Principal { .. } => "principal",
126 }
127 }
128
129 pub fn tenant(&self) -> &Handle {
130 match self {
131 Scope::TenantOnly { tenant, .. } | Scope::Principal { tenant, .. } => tenant,
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn h(s: &str) -> Handle {
141 Handle::new(s).unwrap()
142 }
143
144 #[test]
145 fn handles_are_bounded_and_non_empty() {
146 assert!(Handle::new("").is_err());
147 assert!(Handle::new("x".repeat(MAX_HANDLE_BYTES)).is_ok());
148 assert!(Handle::new("x".repeat(MAX_HANDLE_BYTES + 1)).is_err());
149 assert!(serde_json::from_str::<Handle>("\"\"").is_err());
150 }
151
152 #[test]
153 fn both_modes_round_trip_and_state_their_mode() {
154 for s in [
155 Scope::tenant_only(h("t"), h("r1")),
156 Scope::principal(h("t"), h("r1"), h("p")),
157 ] {
158 let text = serde_json::to_string(&s).unwrap();
159 assert!(
160 text.contains(&format!("\"mode\":\"{}\"", s.mode())),
161 "{text}"
162 );
163 assert_eq!(serde_json::from_str::<Scope>(&text).unwrap(), s);
164 }
165 }
166
167 #[test]
168 fn omitted_unknown_or_mixed_modes_are_refused() {
169 for bad in [
170 r#"{"tenant":"t","context_revision":"r"}"#,
172 r#"{"mode":"public","tenant":"t","context_revision":"r"}"#,
173 r#"{"mode":"tenant_only","tenant":"t","context_revision":"r","principal_scope":"p"}"#,
175 r#"{"mode":"principal","tenant":"t","context_revision":"r"}"#,
177 r#"{"mode":"principal","tenant":"t","context_revision":"","principal_scope":"p"}"#,
178 ] {
179 assert!(serde_json::from_str::<Scope>(bad).is_err(), "{bad}");
180 }
181 assert!(
183 serde_json::from_str::<Scope>(
184 r#"{"mode":"principal","tenant":"t","context_revision":"r","principal_scope":"p"}"#
185 )
186 .is_ok()
187 );
188 }
189
190 #[test]
191 fn modes_never_compare_equal() {
192 assert_ne!(
193 Scope::tenant_only(h("t"), h("r")),
194 Scope::principal(h("t"), h("r"), h("p"))
195 );
196 }
197}