use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::{KeepsakeError, Result};
use crate::policy::ExpiryPolicy;
use super::{KeepsakeId, RelationDefinition, RelationId, SubjectRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LifecycleState {
Applied,
Revoked,
Expired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExpiryCause {
Timed,
Fulfilled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum KeepsakeLifecycle {
Applied,
Revoked {
revoked_at: DateTime<Utc>,
},
Expired {
expired_at: DateTime<Utc>,
cause: ExpiryCause,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Keepsake {
id: KeepsakeId,
subject: SubjectRef,
relation_id: RelationId,
expiry: ExpiryPolicy,
applied_at: DateTime<Utc>,
lifecycle: KeepsakeLifecycle,
metadata: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeepsakeRecord {
pub id: KeepsakeId,
pub subject: SubjectRef,
pub relation_id: RelationId,
pub state: LifecycleState,
pub expiry: ExpiryPolicy,
pub applied_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub fulfilled_at: Option<DateTime<Utc>>,
pub revoked_at: Option<DateTime<Utc>>,
pub metadata: BTreeMap<String, String>,
}
impl Keepsake {
pub fn applied(
id: KeepsakeId,
subject: SubjectRef,
relation: &RelationDefinition,
applied_at: DateTime<Utc>,
metadata: BTreeMap<String, String>,
) -> Result<Self> {
subject.validate()?;
relation.expiry.validate()?;
Ok(Self {
id,
subject,
relation_id: relation.id,
expiry: relation.expiry.clone(),
applied_at,
lifecycle: KeepsakeLifecycle::Applied,
metadata,
})
}
#[must_use]
pub const fn id(&self) -> KeepsakeId {
self.id
}
#[must_use]
pub const fn subject(&self) -> &SubjectRef {
&self.subject
}
#[must_use]
pub const fn relation_id(&self) -> RelationId {
self.relation_id
}
#[must_use]
pub const fn expiry(&self) -> &ExpiryPolicy {
&self.expiry
}
#[must_use]
pub const fn applied_at(&self) -> DateTime<Utc> {
self.applied_at
}
#[must_use]
pub const fn metadata(&self) -> &BTreeMap<String, String> {
&self.metadata
}
#[must_use]
pub const fn state(&self) -> LifecycleState {
match self.lifecycle {
KeepsakeLifecycle::Applied => LifecycleState::Applied,
KeepsakeLifecycle::Revoked { .. } => LifecycleState::Revoked,
KeepsakeLifecycle::Expired { .. } => LifecycleState::Expired,
}
}
#[must_use]
pub const fn lifecycle(&self) -> &KeepsakeLifecycle {
&self.lifecycle
}
#[must_use]
pub const fn is_active(&self) -> bool {
matches!(self.lifecycle, KeepsakeLifecycle::Applied)
}
#[must_use]
pub const fn is_revoked(&self) -> bool {
matches!(self.lifecycle, KeepsakeLifecycle::Revoked { .. })
}
#[must_use]
pub const fn is_expired(&self) -> bool {
matches!(self.lifecycle, KeepsakeLifecycle::Expired { .. })
}
#[must_use]
pub const fn expires_at(&self) -> Option<DateTime<Utc>> {
self.expiry.timed_expiry()
}
#[must_use]
pub const fn ended_at(&self) -> Option<DateTime<Utc>> {
match self.lifecycle {
KeepsakeLifecycle::Applied => None,
KeepsakeLifecycle::Revoked { revoked_at } => Some(revoked_at),
KeepsakeLifecycle::Expired { expired_at, .. } => Some(expired_at),
}
}
#[must_use]
pub const fn revoked_at(&self) -> Option<DateTime<Utc>> {
match self.lifecycle {
KeepsakeLifecycle::Revoked { revoked_at } => Some(revoked_at),
KeepsakeLifecycle::Applied | KeepsakeLifecycle::Expired { .. } => None,
}
}
#[must_use]
pub const fn expired_at(&self) -> Option<DateTime<Utc>> {
match self.lifecycle {
KeepsakeLifecycle::Expired { expired_at, .. } => Some(expired_at),
KeepsakeLifecycle::Applied | KeepsakeLifecycle::Revoked { .. } => None,
}
}
#[must_use]
pub const fn fulfilled_at(&self) -> Option<DateTime<Utc>> {
match self.lifecycle {
KeepsakeLifecycle::Expired {
expired_at,
cause: ExpiryCause::Fulfilled,
} => Some(expired_at),
KeepsakeLifecycle::Applied
| KeepsakeLifecycle::Revoked { .. }
| KeepsakeLifecycle::Expired {
cause: ExpiryCause::Timed,
..
} => None,
}
}
}
impl TryFrom<KeepsakeRecord> for Keepsake {
type Error = KeepsakeError;
fn try_from(record: KeepsakeRecord) -> Result<Self> {
record.subject.validate()?;
record.expiry.validate()?;
let expected_expires_at = record.expiry.timed_expiry();
if record.expires_at != expected_expires_at {
return Err(invalid_lifecycle("expires_at must match the expiry policy"));
}
let lifecycle = match record.state {
LifecycleState::Applied => {
if record.fulfilled_at.is_some() || record.revoked_at.is_some() {
return Err(invalid_lifecycle(
"applied keepsakes must not have terminal timestamps",
));
}
KeepsakeLifecycle::Applied
}
LifecycleState::Revoked => {
let Some(revoked_at) = record.revoked_at else {
return Err(invalid_lifecycle("revoked keepsakes require revoked_at"));
};
if record.fulfilled_at.is_some() {
return Err(invalid_lifecycle(
"revoked keepsakes must not have fulfilled_at",
));
}
KeepsakeLifecycle::Revoked { revoked_at }
}
LifecycleState::Expired => match &record.expiry {
ExpiryPolicy::ManualOnly => {
return Err(invalid_lifecycle("manual-only keepsakes cannot expire"));
}
ExpiryPolicy::At { timestamp } => {
if record.fulfilled_at.is_some() || record.revoked_at.is_some() {
return Err(invalid_lifecycle(
"timed expiry must not have revoked_at or fulfilled_at",
));
}
KeepsakeLifecycle::Expired {
expired_at: *timestamp,
cause: ExpiryCause::Timed,
}
}
ExpiryPolicy::WhenFulfilled { .. } => {
let Some(fulfilled_at) = record.fulfilled_at else {
return Err(invalid_lifecycle(
"fulfillment expiry requires fulfilled_at",
));
};
if record.revoked_at.is_some() {
return Err(invalid_lifecycle(
"fulfillment expiry must not have revoked_at",
));
}
KeepsakeLifecycle::Expired {
expired_at: fulfilled_at,
cause: ExpiryCause::Fulfilled,
}
}
},
};
Ok(Self {
id: record.id,
subject: record.subject,
relation_id: record.relation_id,
expiry: record.expiry,
applied_at: record.applied_at,
lifecycle,
metadata: record.metadata,
})
}
}
impl From<&Keepsake> for KeepsakeRecord {
fn from(keepsake: &Keepsake) -> Self {
Self {
id: keepsake.id,
subject: keepsake.subject.clone(),
relation_id: keepsake.relation_id,
state: keepsake.state(),
expiry: keepsake.expiry.clone(),
applied_at: keepsake.applied_at,
expires_at: keepsake.expires_at(),
fulfilled_at: keepsake.fulfilled_at(),
revoked_at: keepsake.revoked_at(),
metadata: keepsake.metadata.clone(),
}
}
}
impl Serialize for Keepsake {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
KeepsakeRecord::from(self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Keepsake {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
KeepsakeRecord::deserialize(deserializer)?
.try_into()
.map_err(serde::de::Error::custom)
}
}
const fn invalid_lifecycle(reason: &'static str) -> KeepsakeError {
KeepsakeError::InvalidKeepsakeLifecycle { reason }
}