use core::marker::PhantomData;
use std::time::SystemTime;
use thiserror::Error;
use crate::identity::SessionId;
use crate::proto::{AtUri, Did, Nsid, Rkey};
use crate::sealed;
pub trait HasResourceLocation: sealed::Sealed {
fn resource_did(&self) -> &Did;
fn resource_nsid(&self) -> &Nsid;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResourceId {
did: Did,
nsid: Nsid,
rkey: Rkey,
_private: PhantomData<sealed::Token>,
}
impl ResourceId {
#[must_use]
pub fn new(did: Did, nsid: Nsid, rkey: Rkey) -> Self {
ResourceId {
did,
nsid,
rkey,
_private: PhantomData,
}
}
#[must_use]
pub fn did(&self) -> &Did {
&self.did
}
#[must_use]
pub fn nsid(&self) -> &Nsid {
&self.nsid
}
#[must_use]
pub fn rkey(&self) -> &Rkey {
&self.rkey
}
}
impl sealed::Sealed for ResourceId {}
impl HasResourceLocation for ResourceId {
fn resource_did(&self) -> &Did {
&self.did
}
fn resource_nsid(&self) -> &Nsid {
&self.nsid
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AudienceListId(AtUri);
impl AudienceListId {
#[must_use]
pub fn new(uri: AtUri) -> Self {
AudienceListId(uri)
}
#[must_use]
pub fn uri(&self) -> &AtUri {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChannelBinding {
pub peer: crate::identity::ServiceIdentity,
pub session_id: SessionId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ShardId([u8; 8]);
impl ShardId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 8]) -> Self {
ShardId(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 8] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ShardRange {
start: ShardId,
end_exclusive: ShardId,
}
impl ShardRange {
pub fn new(start: ShardId, end_exclusive: ShardId) -> Result<Self, ScopeError> {
if start >= end_exclusive {
return Err(ScopeError::EmptyOrInvertedRange);
}
Ok(ShardRange { start, end_exclusive })
}
#[must_use]
pub fn start(&self) -> ShardId {
self.start
}
#[must_use]
pub fn end_exclusive(&self) -> ShardId {
self.end_exclusive
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RecordStateFilter {
AllNonLive,
TombstonedOnly,
TakenDownOnly,
SealedOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeWindow {
start: SystemTime,
end_exclusive: SystemTime,
}
impl TimeWindow {
pub fn new(start: SystemTime, end_exclusive: SystemTime) -> Result<Self, ScopeError> {
if start >= end_exclusive {
return Err(ScopeError::EmptyOrInvertedRange);
}
Ok(TimeWindow { start, end_exclusive })
}
#[must_use]
pub fn start(&self) -> SystemTime {
self.start
}
#[must_use]
pub fn end_exclusive(&self) -> SystemTime {
self.end_exclusive
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopeSelector {
Shard(ShardRange),
GarbageCollect {
state_filter: RecordStateFilter,
window: TimeWindow,
},
Replicate {
peer: crate::identity::ServiceIdentity,
shard: ShardRange,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ScopeError {
#[error("empty or inverted range")]
EmptyOrInvertedRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModerationCaseId([u8; 16]);
impl ModerationCaseId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
ModerationCaseId(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ManageAudienceSubject {
pub resource: ResourceId,
pub audience_list: AudienceListId,
}
impl sealed::Sealed for ManageAudienceSubject {}
impl HasResourceLocation for ManageAudienceSubject {
fn resource_did(&self) -> &Did {
self.resource.did()
}
fn resource_nsid(&self) -> &Nsid {
self.resource.nsid()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ModerationSubject {
pub resource: ResourceId,
pub case: ModerationCaseId,
}
impl sealed::Sealed for ModerationSubject {}
impl HasResourceLocation for ModerationSubject {
fn resource_did(&self) -> &Did {
self.resource.did()
}
fn resource_nsid(&self) -> &Nsid {
self.resource.nsid()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shard_range_rejects_empty() {
let s = ShardId::from_bytes([0; 8]);
assert!(matches!(
ShardRange::new(s, s),
Err(ScopeError::EmptyOrInvertedRange)
));
}
#[test]
fn shard_range_rejects_inverted() {
let lo = ShardId::from_bytes([0; 8]);
let hi = ShardId::from_bytes([0xFF; 8]);
assert!(ShardRange::new(lo, hi).is_ok());
assert!(matches!(
ShardRange::new(hi, lo),
Err(ScopeError::EmptyOrInvertedRange)
));
}
#[test]
fn has_resource_location_returns_did_and_nsid_for_all_three_impls() {
let did = Did::new("did:plc:phase7dtest").unwrap();
let nsid = Nsid::new("tools.kryphocron.feed.postPrivate").unwrap();
let rkey = Rkey::new("3jzfcijpj2z2a").unwrap();
let resource = ResourceId::new(did.clone(), nsid.clone(), rkey);
assert_eq!(resource.resource_did(), &did);
assert_eq!(resource.resource_nsid(), &nsid);
let mas = ManageAudienceSubject {
resource: resource.clone(),
audience_list: AudienceListId::new(
AtUri::new("at://did:plc:x/tools.kryphocron.policy.audience/3jzfcijpj2z2a")
.unwrap(),
),
};
assert_eq!(mas.resource_did(), &did);
assert_eq!(mas.resource_nsid(), &nsid);
let mod_subj = ModerationSubject {
resource: resource.clone(),
case: ModerationCaseId::from_bytes([0u8; 16]),
};
assert_eq!(mod_subj.resource_did(), &did);
assert_eq!(mod_subj.resource_nsid(), &nsid);
}
#[test]
fn resource_id_construction_does_not_expose_private_field() {
let r = ResourceId::new(
Did::new("did:plc:example").unwrap(),
Nsid::new("tools.kryphocron.feed.postPrivate").unwrap(),
Rkey::new("3jzfcijpj2z2a").unwrap(),
);
assert_eq!(r.did().as_str(), "did:plc:example");
}
}