use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub const MAX_HANDLE_BYTES: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HandleError {
pub len: usize,
}
impl fmt::Display for HandleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"a scope handle is 1 to {MAX_HANDLE_BYTES} bytes, not {}",
self.len
)
}
}
impl std::error::Error for HandleError {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Handle(String);
impl Handle {
pub fn new(s: impl Into<String>) -> Result<Self, HandleError> {
let s = s.into();
if s.is_empty() || s.len() > MAX_HANDLE_BYTES {
return Err(HandleError { len: s.len() });
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Serialize for Handle {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
impl schemars::JsonSchema for Handle {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Handle".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"minLength": 1,
"maxLength": MAX_HANDLE_BYTES
})
}
}
impl<'de> Deserialize<'de> for Handle {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Handle::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
pub enum Scope {
TenantOnly {
tenant: Handle,
context_revision: Handle,
},
Principal {
tenant: Handle,
context_revision: Handle,
principal_scope: Handle,
},
}
impl Scope {
pub fn principal(tenant: Handle, context_revision: Handle, principal_scope: Handle) -> Self {
Scope::Principal {
tenant,
context_revision,
principal_scope,
}
}
pub fn tenant_only(tenant: Handle, context_revision: Handle) -> Self {
Scope::TenantOnly {
tenant,
context_revision,
}
}
pub fn mode(&self) -> &'static str {
match self {
Scope::TenantOnly { .. } => "tenant_only",
Scope::Principal { .. } => "principal",
}
}
pub fn tenant(&self) -> &Handle {
match self {
Scope::TenantOnly { tenant, .. } | Scope::Principal { tenant, .. } => tenant,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn h(s: &str) -> Handle {
Handle::new(s).unwrap()
}
#[test]
fn handles_are_bounded_and_non_empty() {
assert!(Handle::new("").is_err());
assert!(Handle::new("x".repeat(MAX_HANDLE_BYTES)).is_ok());
assert!(Handle::new("x".repeat(MAX_HANDLE_BYTES + 1)).is_err());
assert!(serde_json::from_str::<Handle>("\"\"").is_err());
}
#[test]
fn both_modes_round_trip_and_state_their_mode() {
for s in [
Scope::tenant_only(h("t"), h("r1")),
Scope::principal(h("t"), h("r1"), h("p")),
] {
let text = serde_json::to_string(&s).unwrap();
assert!(
text.contains(&format!("\"mode\":\"{}\"", s.mode())),
"{text}"
);
assert_eq!(serde_json::from_str::<Scope>(&text).unwrap(), s);
}
}
#[test]
fn omitted_unknown_or_mixed_modes_are_refused() {
for bad in [
r#"{"tenant":"t","context_revision":"r"}"#,
r#"{"mode":"public","tenant":"t","context_revision":"r"}"#,
r#"{"mode":"tenant_only","tenant":"t","context_revision":"r","principal_scope":"p"}"#,
r#"{"mode":"principal","tenant":"t","context_revision":"r"}"#,
r#"{"mode":"principal","tenant":"t","context_revision":"","principal_scope":"p"}"#,
] {
assert!(serde_json::from_str::<Scope>(bad).is_err(), "{bad}");
}
assert!(
serde_json::from_str::<Scope>(
r#"{"mode":"principal","tenant":"t","context_revision":"r","principal_scope":"p"}"#
)
.is_ok()
);
}
#[test]
fn modes_never_compare_equal() {
assert_ne!(
Scope::tenant_only(h("t"), h("r")),
Scope::principal(h("t"), h("r"), h("p"))
);
}
}