1use std::collections::{BTreeMap, BTreeSet};
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::extension::ExtValue;
14use crate::id::{RecordId, open_vocabulary};
15use crate::provenance::SourceRef;
16use crate::relation::Relation;
17use crate::schema::SchemaVersion;
18
19pub type Timestamp = DateTime<Utc>;
20
21open_vocabulary!(
22 RecordKind {
25 Issue => "issue",
27 Evidence => "evidence",
29 Claim => "claim",
31 Proposal => "proposal",
33 Comment => "comment",
35 Decision => "decision",
37 Action => "action",
39 Project => "project",
41 Outcome => "outcome",
43 Actor => "actor",
45 Source => "source",
47 }
48);
49
50open_vocabulary!(
51 ActorRoleKind {
53 Creator => "creator",
54 Author => "author",
55 Reporter => "reporter",
56 Issuer => "issuer",
57 Publisher => "publisher",
58 Reviewer => "reviewer",
59 Subject => "subject",
60 Responsible => "responsible",
61 Contributor => "contributor",
62 }
63);
64
65#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum ActorRef {
69 Record { id: RecordId },
71 Anonymous,
73 System { name: String },
75}
76
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum Disclosure {
83 #[default]
84 Public,
85 Withheld,
86}
87
88#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
89pub struct Attribution {
90 pub actor: ActorRef,
91 #[serde(default)]
92 pub disclosure: Disclosure,
93}
94
95impl Attribution {
96 pub fn public(actor: ActorRef) -> Self {
97 Self { actor, disclosure: Disclosure::Public }
98 }
99
100 pub fn withheld(actor: ActorRef) -> Self {
101 Self { actor, disclosure: Disclosure::Withheld }
102 }
103
104 pub fn system(name: &str) -> Self {
105 Self::public(ActorRef::System { name: name.to_string() })
106 }
107
108 pub fn anonymous() -> Self {
109 Self::public(ActorRef::Anonymous)
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct ActorRole {
116 pub role: ActorRoleKind,
117 #[serde(flatten)]
118 pub attribution: Attribution,
119}
120
121#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
123#[serde(tag = "level", rename_all = "snake_case")]
124pub enum Visibility {
125 #[default]
127 Public,
128 Restricted { audiences: BTreeSet<String> },
131}
132
133impl Visibility {
134 pub fn restricted<I: IntoIterator<Item = S>, S: Into<String>>(audiences: I) -> Self {
135 Self::Restricted { audiences: audiences.into_iter().map(Into::into).collect() }
136 }
137
138 pub fn private() -> Self {
140 Self::Restricted { audiences: BTreeSet::new() }
141 }
142
143 pub fn is_public(&self) -> bool {
144 matches!(self, Self::Public)
145 }
146}
147
148#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(tag = "state", rename_all = "snake_case")]
152pub enum Lifecycle {
153 #[default]
154 Active,
155 Retracted,
157 Superseded { by: RecordId },
159}
160
161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
165pub struct RecordTimes {
166 pub recorded_at: Timestamp,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub published_at: Option<Timestamp>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub effective_from: Option<Timestamp>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub effective_until: Option<Timestamp>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub observed_at: Option<Timestamp>,
175}
176
177#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180pub struct ExtensionData {
181 pub schema_version: SchemaVersion,
182 pub fields: BTreeMap<String, ExtValue>,
183}
184
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
187pub struct Record {
188 pub id: RecordId,
189 pub kind: RecordKind,
190 pub schema_version: SchemaVersion,
191 pub title: String,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub summary: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub body: Option<String>,
196 pub times: RecordTimes,
197 #[serde(default)]
198 pub actors: Vec<ActorRole>,
199 #[serde(default)]
200 pub sources: Vec<SourceRef>,
201 #[serde(default)]
202 pub relations: Vec<Relation>,
203 #[serde(default)]
204 pub visibility: Visibility,
205 #[serde(default)]
206 pub lifecycle: Lifecycle,
207 #[serde(default)]
208 pub extensions: BTreeMap<String, ExtensionData>,
209 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub redactions: Vec<crate::event::FieldPath>,
212 pub version: u64,
214 pub updated_at: Timestamp,
216}
217
218impl Record {
219 pub fn extension_field(&self, namespace: &str, field: &str) -> Option<&ExtValue> {
220 self.extensions.get(namespace).and_then(|e| e.fields.get(field))
221 }
222
223 pub fn active_relations(&self) -> impl Iterator<Item = &Relation> {
225 self.relations.iter().filter(|r| !r.retracted)
226 }
227
228 pub fn actors_with_role<'a>(&'a self, role: &'a ActorRoleKind) -> impl Iterator<Item = &'a Attribution> {
229 self.actors.iter().filter(move |a| &a.role == role).map(|a| &a.attribution)
230 }
231}
232
233#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
236pub struct RecordDraft {
237 pub kind: RecordKind,
238 pub title: String,
239 #[serde(default)]
240 pub summary: Option<String>,
241 #[serde(default)]
242 pub body: Option<String>,
243 #[serde(default)]
244 pub published_at: Option<Timestamp>,
245 #[serde(default)]
246 pub effective_from: Option<Timestamp>,
247 #[serde(default)]
248 pub effective_until: Option<Timestamp>,
249 #[serde(default)]
250 pub observed_at: Option<Timestamp>,
251 #[serde(default)]
252 pub actors: Vec<ActorRole>,
253 #[serde(default)]
254 pub sources: Vec<SourceRef>,
255 #[serde(default)]
256 pub relations: Vec<crate::relation::RelationDraft>,
257 #[serde(default)]
258 pub visibility: Visibility,
259 #[serde(default)]
260 pub extensions: BTreeMap<String, ExtensionData>,
261}
262
263impl RecordDraft {
264 pub fn new(kind: RecordKind, title: impl Into<String>) -> Self {
265 Self {
266 kind,
267 title: title.into(),
268 summary: None,
269 body: None,
270 published_at: None,
271 effective_from: None,
272 effective_until: None,
273 observed_at: None,
274 actors: Vec::new(),
275 sources: Vec::new(),
276 relations: Vec::new(),
277 visibility: Visibility::Public,
278 extensions: BTreeMap::new(),
279 }
280 }
281
282 pub fn summary(mut self, s: impl Into<String>) -> Self {
283 self.summary = Some(s.into());
284 self
285 }
286
287 pub fn body(mut self, s: impl Into<String>) -> Self {
288 self.body = Some(s.into());
289 self
290 }
291
292 pub fn actor(mut self, role: ActorRoleKind, attribution: Attribution) -> Self {
293 self.actors.push(ActorRole { role, attribution });
294 self
295 }
296
297 pub fn source(mut self, source: SourceRef) -> Self {
298 self.sources.push(source);
299 self
300 }
301
302 pub fn relation(mut self, relation: crate::relation::RelationDraft) -> Self {
303 self.relations.push(relation);
304 self
305 }
306
307 pub fn visibility(mut self, v: Visibility) -> Self {
308 self.visibility = v;
309 self
310 }
311
312 pub fn extension(mut self, namespace: &str, version: SchemaVersion, fields: BTreeMap<String, ExtValue>) -> Self {
313 self.extensions.insert(namespace.to_string(), ExtensionData { schema_version: version, fields });
314 self
315 }
316}