1use serde::{Deserialize, Serialize};
7
8use crate::event::{Event, Seq};
9use crate::manifest::AttributionManifest;
10use crate::time::Timestamp;
11use crate::token::Token;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case", tag = "kind")]
18pub enum Change {
19 Revoked,
21 Superseded {
23 by: Token,
25 },
26 ExpirySet {
28 expires_at: Option<Timestamp>,
30 },
31 CampaignSet {
33 campaign: Option<String>,
35 },
36 LabelSet {
38 key: String,
40 value: String,
42 },
43 LabelUnset {
45 key: String,
47 },
48}
49
50impl Change {
51 #[must_use]
54 pub fn is_lifecycle(&self) -> bool {
55 matches!(
56 self,
57 Self::Revoked | Self::Superseded { .. } | Self::ExpirySet { .. }
58 )
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "kebab-case", tag = "record")]
67pub enum LogRecord {
68 Minted {
70 manifest: Box<AttributionManifest>,
72 },
73 Mutation {
75 token: Token,
77 at: Timestamp,
79 seq: Seq,
81 change: Change,
83 },
84 Event(Event),
86}
87
88impl LogRecord {
89 #[must_use]
91 pub fn token(&self) -> Token {
92 match self {
93 Self::Minted { manifest } => manifest.token,
94 Self::Mutation { token, .. } => *token,
95 Self::Event(e) => e.token,
96 }
97 }
98
99 #[must_use]
102 pub fn seq(&self) -> Seq {
103 match self {
104 Self::Minted { .. } => Seq(0),
105 Self::Mutation { seq, .. } => *seq,
106 Self::Event(e) => e.seq,
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::{CanonicalUrl, Channel, MintOptions, MintSpec, Sharer};
115
116 #[test]
117 fn lifecycle_split_is_exact() {
118 assert!(Change::Revoked.is_lifecycle());
119 assert!(Change::Superseded {
120 by: Token::parse("abc").unwrap()
121 }
122 .is_lifecycle());
123 assert!(Change::ExpirySet { expires_at: None }.is_lifecycle());
124 assert!(!Change::CampaignSet { campaign: None }.is_lifecycle());
125 assert!(!Change::LabelSet {
126 key: "k".into(),
127 value: "v".into()
128 }
129 .is_lifecycle());
130 assert!(!Change::LabelUnset { key: "k".into() }.is_lifecycle());
131 }
132
133 #[test]
134 fn record_identity_and_serde() {
135 let mut entropy = |buf: &mut [u8]| {
136 buf.fill(3);
137 Ok(())
138 };
139 let m = crate::mint(
140 MintSpec::new(
141 CanonicalUrl::new("ws://a/b").unwrap(),
142 Sharer::new("lead").unwrap(),
143 Channel::subagent_general(),
144 ),
145 &MintOptions::default(),
146 &mut entropy,
147 crate::Timestamp::from_unix_ms(1),
148 )
149 .unwrap();
150 let token = m.token;
151 let rec = LogRecord::Minted {
152 manifest: Box::new(m),
153 };
154 assert_eq!(rec.token(), token);
155 assert_eq!(rec.seq(), Seq(0));
156
157 let json = serde_json::to_string(&rec).unwrap();
158 let back: LogRecord = serde_json::from_str(&json).unwrap();
159 assert_eq!(back, rec);
160 assert!(
161 json.contains("\"record\":\"minted\""),
162 "tagged for the JSONL wire format"
163 );
164 }
165}