use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OwnerKind {
Runtime,
Agent,
User,
}
impl OwnerKind {
pub fn parse(value: &str) -> Option<Self> {
match value {
"runtime" => Some(Self::Runtime),
"agent" => Some(Self::Agent),
"user" => Some(Self::User),
_ => None,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Runtime => "runtime",
Self::Agent => "agent",
Self::User => "user",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct OwnerRef {
pub kind: OwnerKind,
pub id: String,
}
impl OwnerRef {
pub fn runtime(id: impl Into<String>) -> Self {
Self {
kind: OwnerKind::Runtime,
id: id.into(),
}
}
pub fn agent(id: impl Into<String>) -> Self {
Self {
kind: OwnerKind::Agent,
id: id.into(),
}
}
pub fn user(id: impl Into<String>) -> Self {
Self {
kind: OwnerKind::User,
id: id.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OwnerValidationError {
Empty,
Duplicate,
Invalid,
}
pub fn normalize_owners(owners: Vec<OwnerRef>) -> Result<Vec<OwnerRef>, OwnerValidationError> {
if owners.is_empty() {
return Err(OwnerValidationError::Empty);
}
let mut set = BTreeSet::new();
for owner in owners {
let id = owner.id.trim();
if id.is_empty() || id.len() > 255 {
return Err(OwnerValidationError::Invalid);
}
if !set.insert(OwnerRef {
kind: owner.kind,
id: id.to_string(),
}) {
return Err(OwnerValidationError::Duplicate);
}
}
Ok(set.into_iter().collect())
}
pub fn owners_intersect(caller: &[OwnerRef], resource: &[OwnerRef]) -> bool {
let resource_set: BTreeSet<_> = resource.iter().collect();
caller.iter().any(|owner| resource_set.contains(owner))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_rejects_empty_and_duplicates() {
assert!(matches!(
normalize_owners(vec![]),
Err(OwnerValidationError::Empty)
));
assert!(matches!(
normalize_owners(vec![OwnerRef::user("u1"), OwnerRef::user("u1")]),
Err(OwnerValidationError::Duplicate)
));
}
#[test]
fn intersect_requires_shared_identity() {
assert!(owners_intersect(
&[OwnerRef::user("u1")],
&[OwnerRef::user("u1"), OwnerRef::agent("agt_1")]
));
assert!(!owners_intersect(
&[OwnerRef::runtime("rt-2")],
&[OwnerRef::runtime("rt-1")]
));
}
}