use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Relationship<'a> {
#[serde(borrow)]
resource: Cow<'a, str>,
#[serde(borrow)]
relation: Cow<'a, str>,
#[serde(borrow)]
subject: Cow<'a, str>,
}
impl<'a> Relationship<'a> {
pub fn new(
resource: impl Into<Cow<'a, str>>,
relation: impl Into<Cow<'a, str>>,
subject: impl Into<Cow<'a, str>>,
) -> Self {
Self {
resource: resource.into(),
relation: relation.into(),
subject: subject.into(),
}
}
#[inline]
pub fn resource(&self) -> &str {
&self.resource
}
#[inline]
pub fn relation(&self) -> &str {
&self.relation
}
#[inline]
pub fn subject(&self) -> &str {
&self.subject
}
pub fn resource_type(&self) -> Option<&str> {
self.resource.split(':').next()
}
pub fn resource_id(&self) -> Option<&str> {
self.resource.split(':').nth(1)
}
pub fn subject_type(&self) -> Option<&str> {
self.subject.split([':', '#']).next()
}
pub fn subject_id(&self) -> Option<&str> {
let after_colon = self.subject.split(':').nth(1)?;
Some(after_colon.split('#').next().unwrap_or(after_colon))
}
pub fn subject_relation(&self) -> Option<&str> {
self.subject.split('#').nth(1)
}
pub fn is_subject_set(&self) -> bool {
self.subject.contains('#')
}
pub fn into_owned(self) -> Relationship<'static> {
Relationship {
resource: Cow::Owned(self.resource.into_owned()),
relation: Cow::Owned(self.relation.into_owned()),
subject: Cow::Owned(self.subject.into_owned()),
}
}
pub fn as_borrowed(&self) -> Relationship<'_> {
Relationship {
resource: Cow::Borrowed(&self.resource),
relation: Cow::Borrowed(&self.relation),
subject: Cow::Borrowed(&self.subject),
}
}
}
impl fmt::Display for Relationship<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}#{}@{}", self.resource, self.relation, self.subject)
}
}
impl FromStr for Relationship<'static> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (resource, rest) = s.split_once('#').ok_or_else(|| {
Error::invalid_argument(format!(
"invalid relationship format: missing '#' separator in '{}'",
s
))
})?;
let (relation, subject) = rest.split_once('@').ok_or_else(|| {
Error::invalid_argument(format!(
"invalid relationship format: missing '@' separator in '{}'",
s
))
})?;
if resource.is_empty() {
return Err(Error::invalid_argument(
"relationship resource cannot be empty",
));
}
if relation.is_empty() {
return Err(Error::invalid_argument(
"relationship relation cannot be empty",
));
}
if subject.is_empty() {
return Err(Error::invalid_argument(
"relationship subject cannot be empty",
));
}
Ok(Relationship::new(
resource.to_owned(),
relation.to_owned(),
subject.to_owned(),
))
}
}
impl<'a, R, L, S> From<(R, L, S)> for Relationship<'a>
where
R: Into<Cow<'a, str>>,
L: Into<Cow<'a, str>>,
S: Into<Cow<'a, str>>,
{
fn from((resource, relation, subject): (R, L, S)) -> Self {
Self::new(resource, relation, subject)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relationship_new() {
let rel = Relationship::new("document:readme", "viewer", "user:alice");
assert_eq!(rel.resource(), "document:readme");
assert_eq!(rel.relation(), "viewer");
assert_eq!(rel.subject(), "user:alice");
}
#[test]
fn test_relationship_with_owned_strings() {
let resource = String::from("document:123");
let relation = String::from("editor");
let subject = String::from("user:bob");
let rel = Relationship::new(resource, relation, subject);
assert_eq!(rel.resource(), "document:123");
assert_eq!(rel.relation(), "editor");
assert_eq!(rel.subject(), "user:bob");
}
#[test]
fn test_relationship_parts() {
let rel = Relationship::new("document:readme", "viewer", "user:alice");
assert_eq!(rel.resource_type(), Some("document"));
assert_eq!(rel.resource_id(), Some("readme"));
assert_eq!(rel.subject_type(), Some("user"));
assert_eq!(rel.subject_id(), Some("alice"));
assert_eq!(rel.subject_relation(), None);
assert!(!rel.is_subject_set());
}
#[test]
fn test_subject_set() {
let rel = Relationship::new("folder:reports", "viewer", "team:engineering#member");
assert_eq!(rel.subject_type(), Some("team"));
assert_eq!(rel.subject_id(), Some("engineering"));
assert_eq!(rel.subject_relation(), Some("member"));
assert!(rel.is_subject_set());
}
#[test]
fn test_display() {
let rel = Relationship::new("document:readme", "viewer", "user:alice");
assert_eq!(rel.to_string(), "document:readme#viewer@user:alice");
}
#[test]
fn test_from_str() {
let rel: Relationship = "document:readme#viewer@user:alice".parse().unwrap();
assert_eq!(rel.resource(), "document:readme");
assert_eq!(rel.relation(), "viewer");
assert_eq!(rel.subject(), "user:alice");
}
#[test]
fn test_from_str_subject_set() {
let rel: Relationship = "folder:reports#viewer@team:eng#member".parse().unwrap();
assert_eq!(rel.resource(), "folder:reports");
assert_eq!(rel.relation(), "viewer");
assert_eq!(rel.subject(), "team:eng#member");
assert!(rel.is_subject_set());
}
#[test]
fn test_from_str_invalid() {
assert!("document:readme".parse::<Relationship>().is_err());
assert!("document:readme#viewer".parse::<Relationship>().is_err());
assert!("#viewer@user:alice".parse::<Relationship>().is_err());
assert!("doc:1#@user:alice".parse::<Relationship>().is_err());
assert!("doc:1#viewer@".parse::<Relationship>().is_err());
}
#[test]
fn test_from_tuple() {
let rel: Relationship = ("doc:1", "viewer", "user:alice").into();
assert_eq!(rel.resource(), "doc:1");
assert_eq!(rel.relation(), "viewer");
assert_eq!(rel.subject(), "user:alice");
}
#[test]
fn test_into_owned() {
let rel = Relationship::new("doc:1", "viewer", "user:alice");
let owned: Relationship<'static> = rel.into_owned();
assert_eq!(owned.resource(), "doc:1");
}
#[test]
fn test_equality() {
let rel1 = Relationship::new("doc:1", "viewer", "user:alice");
let rel2 = Relationship::new("doc:1", "viewer", "user:alice");
let rel3 = Relationship::new("doc:1", "editor", "user:alice");
assert_eq!(rel1, rel2);
assert_ne!(rel1, rel3);
}
#[test]
fn test_serialization() {
let rel = Relationship::new("doc:1", "viewer", "user:alice");
let json = serde_json::to_string(&rel).unwrap();
let parsed: Relationship = serde_json::from_str(&json).unwrap();
assert_eq!(rel, parsed);
}
#[test]
fn test_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Relationship::new("doc:1", "viewer", "user:alice").into_owned());
set.insert(Relationship::new("doc:1", "viewer", "user:alice").into_owned());
set.insert(Relationship::new("doc:2", "viewer", "user:alice").into_owned());
assert_eq!(set.len(), 2);
}
}