1use std::{fmt, str::FromStr};
4
5use crate::model::{Object, Relation, RelationTuple, User};
6
7const MAX_TYPE_BYTES: usize = 64;
8const MAX_RELATION_BYTES: usize = 64;
9const MAX_ID_BYTES: usize = 256;
10const MAX_RELATIONSHIP_BYTES: usize = 768;
11const LEGACY_USER_SUBJECT_TYPE: &str = "user";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum IdentifierKind {
16 ObjectType,
18 ObjectId,
20 RelationName,
22 SubjectType,
24 SubjectId,
26}
27
28impl fmt::Display for IdentifierKind {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::ObjectType => formatter.write_str("object type"),
32 Self::ObjectId => formatter.write_str("object id"),
33 Self::RelationName => formatter.write_str("relation name"),
34 Self::SubjectType => formatter.write_str("subject type"),
35 Self::SubjectId => formatter.write_str("subject id"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum DomainError {
43 #[error("identifier {kind} exceeds {max_bytes} bytes")]
45 IdentifierTooLong {
46 kind: IdentifierKind,
48 max_bytes: usize,
50 },
51
52 #[error("identifier {kind} must not be empty")]
54 EmptyIdentifier {
55 kind: IdentifierKind,
57 },
58
59 #[error("identifier {kind} contains invalid byte at offset {offset}")]
61 InvalidIdentifierByte {
62 kind: IdentifierKind,
64 offset: usize,
66 },
67
68 #[error("relationship is malformed: {reason}")]
70 MalformedRelationship {
71 reason: &'static str,
73 },
74}
75
76macro_rules! validated_identifier {
77 ($name:ident, $kind:expr, $max:expr, $validator:ident) => {
78 #[doc = concat!("Validated ", stringify!($name), " domain primitive.")]
79 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
80 pub struct $name(String);
81
82 impl $name {
83 #[doc = concat!("Creates a validated ", stringify!($name), ".")]
84 pub fn new(value: impl Into<String>) -> Result<Self, DomainError> {
90 let value = value.into();
91 $validator($kind, &value, $max)?;
92 Ok(Self(value))
93 }
94
95 #[must_use]
97 pub fn as_str(&self) -> &str {
98 &self.0
99 }
100 }
101
102 impl fmt::Display for $name {
103 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104 formatter.write_str(&self.0)
105 }
106 }
107
108 impl FromStr for $name {
109 type Err = DomainError;
110
111 fn from_str(value: &str) -> Result<Self, Self::Err> {
112 Self::new(value)
113 }
114 }
115
116 impl TryFrom<&str> for $name {
117 type Error = DomainError;
118
119 fn try_from(value: &str) -> Result<Self, Self::Error> {
120 Self::new(value)
121 }
122 }
123
124 impl TryFrom<String> for $name {
125 type Error = DomainError;
126
127 fn try_from(value: String) -> Result<Self, Self::Error> {
128 Self::new(value)
129 }
130 }
131
132 #[cfg(feature = "serde")]
133 impl serde::Serialize for $name {
134 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135 where
136 S: serde::Serializer,
137 {
138 serializer.serialize_str(self.as_str())
139 }
140 }
141
142 #[cfg(feature = "serde")]
143 impl<'de> serde::Deserialize<'de> for $name {
144 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145 where
146 D: serde::Deserializer<'de>,
147 {
148 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
149 Self::try_from(value).map_err(serde::de::Error::custom)
150 }
151 }
152 };
153}
154
155validated_identifier!(
156 ObjectType,
157 IdentifierKind::ObjectType,
158 MAX_TYPE_BYTES,
159 validate_type_identifier
160);
161validated_identifier!(
162 ObjectId,
163 IdentifierKind::ObjectId,
164 MAX_ID_BYTES,
165 validate_id_identifier
166);
167validated_identifier!(
168 RelationName,
169 IdentifierKind::RelationName,
170 MAX_RELATION_BYTES,
171 validate_type_identifier
172);
173validated_identifier!(
174 SubjectType,
175 IdentifierKind::SubjectType,
176 MAX_TYPE_BYTES,
177 validate_type_identifier
178);
179validated_identifier!(
180 SubjectId,
181 IdentifierKind::SubjectId,
182 MAX_ID_BYTES,
183 validate_id_identifier
184);
185
186#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
188pub struct ObjectRef {
189 object_type: ObjectType,
190 object_id: ObjectId,
191}
192
193impl ObjectRef {
194 #[must_use]
196 pub fn new(object_type: ObjectType, object_id: ObjectId) -> Self {
197 Self {
198 object_type,
199 object_id,
200 }
201 }
202
203 #[must_use]
205 pub fn object_type(&self) -> &ObjectType {
206 &self.object_type
207 }
208
209 #[must_use]
211 pub fn object_id(&self) -> &ObjectId {
212 &self.object_id
213 }
214}
215
216impl fmt::Display for ObjectRef {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(formatter, "{}:{}", self.object_type, self.object_id)
219 }
220}
221
222impl FromStr for ObjectRef {
223 type Err = DomainError;
224
225 fn from_str(value: &str) -> Result<Self, Self::Err> {
226 let (object_type, object_id) = split_once(value, ':', "object reference must contain ':'")?;
227 Ok(Self::new(
228 ObjectType::try_from(object_type)?,
229 ObjectId::try_from(object_id)?,
230 ))
231 }
232}
233
234#[cfg(feature = "serde")]
235impl serde::Serialize for ObjectRef {
236 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
237 where
238 S: serde::Serializer,
239 {
240 serializer.serialize_str(&self.to_string())
241 }
242}
243
244#[cfg(feature = "serde")]
245impl<'de> serde::Deserialize<'de> for ObjectRef {
246 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
247 where
248 D: serde::Deserializer<'de>,
249 {
250 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
251 Self::from_str(&value).map_err(serde::de::Error::custom)
252 }
253}
254
255impl TryFrom<&Object> for ObjectRef {
256 type Error = DomainError;
257
258 fn try_from(value: &Object) -> Result<Self, Self::Error> {
259 Ok(Self::new(
260 ObjectType::try_from(value.namespace.as_str())?,
261 ObjectId::try_from(value.id.as_str())?,
262 ))
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
268pub enum SubjectRef {
269 Object(ObjectRef),
271 Userset {
273 object: ObjectRef,
275 relation: RelationName,
277 },
278}
279
280impl fmt::Display for SubjectRef {
281 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
282 match self {
283 Self::Object(object) => write!(formatter, "{object}"),
284 Self::Userset { object, relation } => write!(formatter, "{object}#{relation}"),
285 }
286 }
287}
288
289impl FromStr for SubjectRef {
290 type Err = DomainError;
291
292 fn from_str(value: &str) -> Result<Self, Self::Err> {
293 match value.split_once('#') {
294 Some((object, relation)) => Ok(Self::Userset {
295 object: object.parse()?,
296 relation: RelationName::try_from(relation)?,
297 }),
298 None => Ok(Self::Object(parse_subject_object(value)?)),
299 }
300 }
301}
302
303#[cfg(feature = "serde")]
304impl serde::Serialize for SubjectRef {
305 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
306 where
307 S: serde::Serializer,
308 {
309 serializer.serialize_str(&self.to_string())
310 }
311}
312
313#[cfg(feature = "serde")]
314impl<'de> serde::Deserialize<'de> for SubjectRef {
315 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
316 where
317 D: serde::Deserializer<'de>,
318 {
319 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
320 Self::from_str(&value).map_err(serde::de::Error::custom)
321 }
322}
323
324impl TryFrom<&User> for SubjectRef {
325 type Error = DomainError;
326
327 fn try_from(value: &User) -> Result<Self, Self::Error> {
328 match value {
329 User::UserId(id) => Ok(Self::Object(ObjectRef::new(
330 ObjectType::try_from(LEGACY_USER_SUBJECT_TYPE)?,
331 ObjectId::try_from(id.as_str())?,
332 ))),
333 User::Userset(object, relation) => Ok(Self::Userset {
334 object: ObjectRef::try_from(object)?,
335 relation: RelationName::try_from(relation.0.as_str())?,
336 }),
337 }
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Hash)]
343pub struct Relationship {
344 resource: ObjectRef,
345 relation: RelationName,
346 subject: SubjectRef,
347}
348
349impl Relationship {
350 #[must_use]
352 pub fn new(resource: ObjectRef, relation: RelationName, subject: SubjectRef) -> Self {
353 Self {
354 resource,
355 relation,
356 subject,
357 }
358 }
359
360 #[must_use]
362 pub fn resource(&self) -> &ObjectRef {
363 &self.resource
364 }
365
366 #[must_use]
368 pub fn relation(&self) -> &RelationName {
369 &self.relation
370 }
371
372 #[must_use]
374 pub fn subject(&self) -> &SubjectRef {
375 &self.subject
376 }
377}
378
379impl fmt::Display for Relationship {
380 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381 write!(
382 formatter,
383 "{}#{}@{}",
384 self.resource, self.relation, self.subject
385 )
386 }
387}
388
389impl FromStr for Relationship {
390 type Err = DomainError;
391
392 fn from_str(value: &str) -> Result<Self, Self::Err> {
393 if value.len() > MAX_RELATIONSHIP_BYTES {
394 return Err(DomainError::IdentifierTooLong {
395 kind: IdentifierKind::ObjectId,
396 max_bytes: MAX_RELATIONSHIP_BYTES,
397 });
398 }
399
400 let (resource, subject) =
401 split_once(value, '@', "relationship must contain one '@' separator")?;
402 if subject.contains('@') {
403 return Err(DomainError::MalformedRelationship {
404 reason: "relationship must contain one '@' separator",
405 });
406 }
407
408 let (object, relation) =
409 split_once(resource, '#', "relationship resource must contain '#'")?;
410 if relation.contains('#') {
411 return Err(DomainError::MalformedRelationship {
412 reason: "relationship resource must contain one '#'",
413 });
414 }
415
416 Ok(Self::new(
417 object.parse()?,
418 RelationName::try_from(relation)?,
419 subject.parse()?,
420 ))
421 }
422}
423
424#[cfg(feature = "serde")]
425impl serde::Serialize for Relationship {
426 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
427 where
428 S: serde::Serializer,
429 {
430 serializer.serialize_str(&self.to_string())
431 }
432}
433
434#[cfg(feature = "serde")]
435impl<'de> serde::Deserialize<'de> for Relationship {
436 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
437 where
438 D: serde::Deserializer<'de>,
439 {
440 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
441 Self::from_str(&value).map_err(serde::de::Error::custom)
442 }
443}
444
445impl TryFrom<&RelationTuple> for Relationship {
446 type Error = DomainError;
447
448 fn try_from(value: &RelationTuple) -> Result<Self, Self::Error> {
449 Ok(Self::new(
450 ObjectRef::try_from(&value.object)?,
451 RelationName::try_from(value.relation.0.as_str())?,
452 SubjectRef::try_from(&value.user)?,
453 ))
454 }
455}
456
457fn parse_subject_object(value: &str) -> Result<ObjectRef, DomainError> {
458 let (subject_type, subject_id) = split_once(value, ':', "subject reference must contain ':'")?;
459 Ok(ObjectRef::new(
460 ObjectType::new(SubjectType::try_from(subject_type)?.as_str())?,
461 ObjectId::new(SubjectId::try_from(subject_id)?.as_str())?,
462 ))
463}
464
465fn split_once<'a>(
466 value: &'a str,
467 delimiter: char,
468 missing_reason: &'static str,
469) -> Result<(&'a str, &'a str), DomainError> {
470 let (left, right) = value
471 .split_once(delimiter)
472 .ok_or(DomainError::MalformedRelationship {
473 reason: missing_reason,
474 })?;
475 if left.is_empty() || right.is_empty() {
476 return Err(DomainError::MalformedRelationship {
477 reason: missing_reason,
478 });
479 }
480 Ok((left, right))
481}
482
483fn validate_type_identifier(
484 kind: IdentifierKind,
485 value: &str,
486 max_bytes: usize,
487) -> Result<(), DomainError> {
488 if value.is_empty() {
489 return Err(DomainError::EmptyIdentifier { kind });
490 }
491 if value.len() > max_bytes {
492 return Err(DomainError::IdentifierTooLong { kind, max_bytes });
493 }
494
495 for (offset, byte) in value.bytes().enumerate() {
496 let valid = if offset == 0 {
497 byte.is_ascii_alphabetic()
498 } else {
499 byte.is_ascii_alphanumeric() || byte == b'_'
500 };
501 if !valid {
502 return Err(DomainError::InvalidIdentifierByte { kind, offset });
503 }
504 }
505
506 Ok(())
507}
508
509fn validate_id_identifier(
510 kind: IdentifierKind,
511 value: &str,
512 max_bytes: usize,
513) -> Result<(), DomainError> {
514 if value.is_empty() {
515 return Err(DomainError::EmptyIdentifier { kind });
516 }
517 if value.len() > max_bytes {
518 return Err(DomainError::IdentifierTooLong { kind, max_bytes });
519 }
520
521 for (offset, byte) in value.bytes().enumerate() {
522 let valid = byte.is_ascii_graphic()
523 && !matches!(byte, b'#' | b'@' | b':' | b'/' | b'\\')
524 && !byte.is_ascii_whitespace();
525 if !valid {
526 return Err(DomainError::InvalidIdentifierByte { kind, offset });
527 }
528 }
529
530 Ok(())
531}
532
533impl TryFrom<&Relation> for RelationName {
534 type Error = DomainError;
535
536 fn try_from(value: &Relation) -> Result<Self, Self::Error> {
537 Self::try_from(value.0.as_str())
538 }
539}
540
541impl From<&ObjectType> for SubjectType {
542 fn from(value: &ObjectType) -> Self {
543 Self(value.0.clone())
544 }
545}
546
547impl From<&ObjectId> for SubjectId {
548 fn from(value: &ObjectId) -> Self {
549 Self(value.0.clone())
550 }
551}