use std::{fmt, str::FromStr};
use crate::model::{Object, Relation, RelationTuple, User};
const MAX_TYPE_BYTES: usize = 64;
const MAX_RELATION_BYTES: usize = 64;
const MAX_ID_BYTES: usize = 256;
const MAX_RELATIONSHIP_BYTES: usize = 768;
const LEGACY_USER_SUBJECT_TYPE: &str = "user";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentifierKind {
ObjectType,
ObjectId,
RelationName,
SubjectType,
SubjectId,
}
impl fmt::Display for IdentifierKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ObjectType => formatter.write_str("object type"),
Self::ObjectId => formatter.write_str("object id"),
Self::RelationName => formatter.write_str("relation name"),
Self::SubjectType => formatter.write_str("subject type"),
Self::SubjectId => formatter.write_str("subject id"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DomainError {
#[error("identifier {kind} exceeds {max_bytes} bytes")]
IdentifierTooLong {
kind: IdentifierKind,
max_bytes: usize,
},
#[error("identifier {kind} must not be empty")]
EmptyIdentifier {
kind: IdentifierKind,
},
#[error("identifier {kind} contains invalid byte at offset {offset}")]
InvalidIdentifierByte {
kind: IdentifierKind,
offset: usize,
},
#[error("relationship is malformed: {reason}")]
MalformedRelationship {
reason: &'static str,
},
}
macro_rules! validated_identifier {
($name:ident, $kind:expr, $max:expr, $validator:ident) => {
#[doc = concat!("Validated ", stringify!($name), " domain primitive.")]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(String);
impl $name {
#[doc = concat!("Creates a validated ", stringify!($name), ".")]
pub fn new(value: impl Into<String>) -> Result<Self, DomainError> {
let value = value.into();
$validator($kind, &value, $max)?;
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for $name {
type Err = DomainError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for $name {
type Error = DomainError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for $name {
type Error = DomainError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
Self::try_from(value).map_err(serde::de::Error::custom)
}
}
};
}
validated_identifier!(
ObjectType,
IdentifierKind::ObjectType,
MAX_TYPE_BYTES,
validate_type_identifier
);
validated_identifier!(
ObjectId,
IdentifierKind::ObjectId,
MAX_ID_BYTES,
validate_id_identifier
);
validated_identifier!(
RelationName,
IdentifierKind::RelationName,
MAX_RELATION_BYTES,
validate_type_identifier
);
validated_identifier!(
SubjectType,
IdentifierKind::SubjectType,
MAX_TYPE_BYTES,
validate_type_identifier
);
validated_identifier!(
SubjectId,
IdentifierKind::SubjectId,
MAX_ID_BYTES,
validate_id_identifier
);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjectRef {
object_type: ObjectType,
object_id: ObjectId,
}
impl ObjectRef {
#[must_use]
pub fn new(object_type: ObjectType, object_id: ObjectId) -> Self {
Self {
object_type,
object_id,
}
}
#[must_use]
pub fn object_type(&self) -> &ObjectType {
&self.object_type
}
#[must_use]
pub fn object_id(&self) -> &ObjectId {
&self.object_id
}
}
impl fmt::Display for ObjectRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}", self.object_type, self.object_id)
}
}
impl FromStr for ObjectRef {
type Err = DomainError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (object_type, object_id) = split_once(value, ':', "object reference must contain ':'")?;
Ok(Self::new(
ObjectType::try_from(object_type)?,
ObjectId::try_from(object_id)?,
))
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ObjectRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ObjectRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
Self::from_str(&value).map_err(serde::de::Error::custom)
}
}
impl TryFrom<&Object> for ObjectRef {
type Error = DomainError;
fn try_from(value: &Object) -> Result<Self, Self::Error> {
Ok(Self::new(
ObjectType::try_from(value.namespace.as_str())?,
ObjectId::try_from(value.id.as_str())?,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SubjectRef {
Object(ObjectRef),
Userset {
object: ObjectRef,
relation: RelationName,
},
}
impl fmt::Display for SubjectRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Object(object) => write!(formatter, "{object}"),
Self::Userset { object, relation } => write!(formatter, "{object}#{relation}"),
}
}
}
impl FromStr for SubjectRef {
type Err = DomainError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.split_once('#') {
Some((object, relation)) => Ok(Self::Userset {
object: object.parse()?,
relation: RelationName::try_from(relation)?,
}),
None => Ok(Self::Object(parse_subject_object(value)?)),
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for SubjectRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for SubjectRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
Self::from_str(&value).map_err(serde::de::Error::custom)
}
}
impl TryFrom<&User> for SubjectRef {
type Error = DomainError;
fn try_from(value: &User) -> Result<Self, Self::Error> {
match value {
User::UserId(id) => Ok(Self::Object(ObjectRef::new(
ObjectType::try_from(LEGACY_USER_SUBJECT_TYPE)?,
ObjectId::try_from(id.as_str())?,
))),
User::Userset(object, relation) => Ok(Self::Userset {
object: ObjectRef::try_from(object)?,
relation: RelationName::try_from(relation.0.as_str())?,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Relationship {
resource: ObjectRef,
relation: RelationName,
subject: SubjectRef,
}
impl Relationship {
#[must_use]
pub fn new(resource: ObjectRef, relation: RelationName, subject: SubjectRef) -> Self {
Self {
resource,
relation,
subject,
}
}
#[must_use]
pub fn resource(&self) -> &ObjectRef {
&self.resource
}
#[must_use]
pub fn relation(&self) -> &RelationName {
&self.relation
}
#[must_use]
pub fn subject(&self) -> &SubjectRef {
&self.subject
}
}
impl fmt::Display for Relationship {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}#{}@{}",
self.resource, self.relation, self.subject
)
}
}
impl FromStr for Relationship {
type Err = DomainError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.len() > MAX_RELATIONSHIP_BYTES {
return Err(DomainError::IdentifierTooLong {
kind: IdentifierKind::ObjectId,
max_bytes: MAX_RELATIONSHIP_BYTES,
});
}
let (resource, subject) =
split_once(value, '@', "relationship must contain one '@' separator")?;
if subject.contains('@') {
return Err(DomainError::MalformedRelationship {
reason: "relationship must contain one '@' separator",
});
}
let (object, relation) =
split_once(resource, '#', "relationship resource must contain '#'")?;
if relation.contains('#') {
return Err(DomainError::MalformedRelationship {
reason: "relationship resource must contain one '#'",
});
}
Ok(Self::new(
object.parse()?,
RelationName::try_from(relation)?,
subject.parse()?,
))
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Relationship {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Relationship {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
Self::from_str(&value).map_err(serde::de::Error::custom)
}
}
impl TryFrom<&RelationTuple> for Relationship {
type Error = DomainError;
fn try_from(value: &RelationTuple) -> Result<Self, Self::Error> {
Ok(Self::new(
ObjectRef::try_from(&value.object)?,
RelationName::try_from(value.relation.0.as_str())?,
SubjectRef::try_from(&value.user)?,
))
}
}
fn parse_subject_object(value: &str) -> Result<ObjectRef, DomainError> {
let (subject_type, subject_id) = split_once(value, ':', "subject reference must contain ':'")?;
Ok(ObjectRef::new(
ObjectType::new(SubjectType::try_from(subject_type)?.as_str())?,
ObjectId::new(SubjectId::try_from(subject_id)?.as_str())?,
))
}
fn split_once<'a>(
value: &'a str,
delimiter: char,
missing_reason: &'static str,
) -> Result<(&'a str, &'a str), DomainError> {
let (left, right) = value
.split_once(delimiter)
.ok_or(DomainError::MalformedRelationship {
reason: missing_reason,
})?;
if left.is_empty() || right.is_empty() {
return Err(DomainError::MalformedRelationship {
reason: missing_reason,
});
}
Ok((left, right))
}
fn validate_type_identifier(
kind: IdentifierKind,
value: &str,
max_bytes: usize,
) -> Result<(), DomainError> {
if value.is_empty() {
return Err(DomainError::EmptyIdentifier { kind });
}
if value.len() > max_bytes {
return Err(DomainError::IdentifierTooLong { kind, max_bytes });
}
for (offset, byte) in value.bytes().enumerate() {
let valid = if offset == 0 {
byte.is_ascii_alphabetic()
} else {
byte.is_ascii_alphanumeric() || byte == b'_'
};
if !valid {
return Err(DomainError::InvalidIdentifierByte { kind, offset });
}
}
Ok(())
}
fn validate_id_identifier(
kind: IdentifierKind,
value: &str,
max_bytes: usize,
) -> Result<(), DomainError> {
if value.is_empty() {
return Err(DomainError::EmptyIdentifier { kind });
}
if value.len() > max_bytes {
return Err(DomainError::IdentifierTooLong { kind, max_bytes });
}
for (offset, byte) in value.bytes().enumerate() {
let valid = byte.is_ascii_graphic()
&& !matches!(byte, b'#' | b'@' | b':' | b'/' | b'\\')
&& !byte.is_ascii_whitespace();
if !valid {
return Err(DomainError::InvalidIdentifierByte { kind, offset });
}
}
Ok(())
}
impl TryFrom<&Relation> for RelationName {
type Error = DomainError;
fn try_from(value: &Relation) -> Result<Self, Self::Error> {
Self::try_from(value.0.as_str())
}
}
impl From<&ObjectType> for SubjectType {
fn from(value: &ObjectType) -> Self {
Self(value.0.clone())
}
}
impl From<&ObjectId> for SubjectId {
fn from(value: &ObjectId) -> Self {
Self(value.0.clone())
}
}