use serde::{Deserialize, Serialize};
use super::validation::{EntityId, Namespace, ValidationError};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Group {
id: EntityId,
#[serde(default)]
namespace: Namespace,
}
impl Group {
pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
let group = Self {
id: EntityId::new(id),
namespace: Namespace::default(),
};
group.validate()?;
Ok(group)
}
pub fn with_namespace(mut self, namespace: Vec<String>) -> Result<Self, ValidationError> {
self.namespace = Namespace::new(namespace);
self.validate()?;
Ok(self)
}
pub fn id(&self) -> &str {
self.id.as_str()
}
pub fn namespace(&self) -> &[String] {
self.namespace.as_slice()
}
pub fn validate(&self) -> Result<(), ValidationError> {
self.id.validate("group.id")?;
self.namespace.validate("group.namespace")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct User {
id: EntityId,
#[serde(default)]
namespace: Namespace,
#[serde(default)]
groups: Vec<Group>,
}
impl User {
pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
let user = Self {
id: EntityId::new(id),
namespace: Namespace::default(),
groups: Vec::new(),
};
user.validate()?;
Ok(user)
}
pub fn with_namespace(mut self, namespace: Vec<String>) -> Result<Self, ValidationError> {
self.namespace = Namespace::new(namespace);
self.validate()?;
Ok(self)
}
pub fn with_groups(mut self, groups: Vec<Group>) -> Self {
self.groups = groups;
self
}
pub fn with_group_names(mut self, names: &[&str]) -> Result<Self, ValidationError> {
self.groups = names
.iter()
.map(|name| Group::new(*name))
.collect::<Result<_, _>>()?;
Ok(self)
}
pub fn id(&self) -> &str {
self.id.as_str()
}
pub fn namespace(&self) -> &[String] {
self.namespace.as_slice()
}
pub fn groups(&self) -> &[Group] {
&self.groups
}
pub fn validate(&self) -> Result<(), ValidationError> {
self.id.validate("user.id")?;
self.namespace.validate("user.namespace")?;
for group in &self.groups {
group.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Principal {
User(User),
Group(Group),
}
impl From<User> for Principal {
fn from(user: User) -> Self {
Principal::User(user)
}
}
impl From<Group> for Principal {
fn from(group: Group) -> Self {
Principal::Group(group)
}
}
impl Principal {
pub fn validate(&self) -> Result<(), ValidationError> {
match self {
Self::User(user) => user.validate(),
Self::Group(group) => group.validate(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_serialization_without_groups() {
let user = User::new("alice").unwrap();
let json = serde_json::to_value(&user).unwrap();
assert_eq!(json["id"], "alice");
assert_eq!(json["namespace"], serde_json::json!([]));
assert_eq!(json["groups"], serde_json::json!([]));
}
#[test]
fn user_serialization_with_groups_and_namespace() {
let user = User::new("alice")
.unwrap()
.with_namespace(vec!["App".to_string()])
.unwrap()
.with_group_names(&["admins", "users"])
.unwrap();
let json = serde_json::to_value(&user).unwrap();
assert_eq!(json["id"], "alice");
assert_eq!(json["namespace"], serde_json::json!(["App"]));
assert_eq!(json["groups"][0]["id"], "admins");
assert_eq!(json["groups"][1]["id"], "users");
}
#[test]
fn principal_user_serialization() {
let principal = Principal::User(User::new("alice").unwrap());
let json = serde_json::to_value(&principal).unwrap();
assert!(json["User"].is_object());
assert_eq!(json["User"]["id"], "alice");
}
#[test]
fn principal_group_serialization() {
let principal = Principal::Group(Group::new("admins").unwrap());
let json = serde_json::to_value(&principal).unwrap();
assert!(json["Group"].is_object());
assert_eq!(json["Group"]["id"], "admins");
}
#[test]
fn user_roundtrip() {
let user = User::new("bob")
.unwrap()
.with_namespace(vec!["Infra".to_string()])
.unwrap()
.with_groups(vec![
Group::new("ops")
.unwrap()
.with_namespace(vec!["Infra".to_string()])
.unwrap(),
]);
let json = serde_json::to_value(&user).unwrap();
let deserialized: User = serde_json::from_value(json).unwrap();
assert_eq!(user, deserialized);
}
}