Skip to main content

pardarsh_core/
model.rs

1//! The common record envelope.
2//!
3//! A [`Record`] is the current-state *projection* of a record's event
4//! history (see [`crate::event`] and [`crate::projection`]). Applications
5//! never mutate a `Record` directly; they append events through the
6//! [`crate::repository::Repository`].
7
8use 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    /// The type of a record. The core kinds are deliberately broader than any
23    /// one application: issues are only one of them.
24    RecordKind {
25        /// A problem, question, service failure, unmet commitment, or public concern.
26        Issue => "issue",
27        /// A document, URL, photograph, dataset, observation or other relevant material.
28        Evidence => "evidence",
29        /// An attributed statement that may be supported, disputed or qualified.
30        Claim => "claim",
31        /// A suggested remedy or change.
32        Proposal => "proposal",
33        /// A question, argument, objection, response or review.
34        Comment => "comment",
35        /// A formal or attributable choice by an actor or institution.
36        Decision => "decision",
37        /// A committed or required next step (action or obligation).
38        Action => "action",
39        /// Implementation activity (project or work item).
40        Project => "project",
41        /// A result or reported state of the world, with evidence and time.
42        Outcome => "outcome",
43        /// A person, institution, department, community group, contractor or other participant.
44        Actor => "actor",
45        /// A published source artifact (law, notice, report, dataset...).
46        Source => "source",
47    }
48);
49
50open_vocabulary!(
51    /// The role an actor plays with respect to a record.
52    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/// Who did or said something.
66#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum ActorRef {
69    /// An actor described by an `actor` record (local or remote).
70    Record { id: RecordId },
71    /// No identity was given or it was not retained.
72    Anonymous,
73    /// An automated process, named for auditability (e.g. `importer`).
74    System { name: String },
75}
76
77/// Whether the identity of an attributed actor may be shown to viewers who
78/// are neither the actor nor privileged. Withholding is *not* anonymity:
79/// the attribution is retained for accountability and moderation.
80#[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/// An actor with an explicit role on a record.
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct ActorRole {
116    pub role: ActorRoleKind,
117    #[serde(flatten)]
118    pub attribution: Attribution,
119}
120
121/// Who may see a record, an event or a source.
122#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
123#[serde(tag = "level", rename_all = "snake_case")]
124pub enum Visibility {
125    /// Anyone, including anonymous readers and exports.
126    #[default]
127    Public,
128    /// Only viewers holding one of the audiences (and privileged viewers).
129    /// An empty audience set means privileged viewers only.
130    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    /// Visible only to privileged viewers.
139    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/// Generic lifecycle of a record. Application workflow states (for example
149/// an issue's triage status) do *not* belong here; they live in extensions.
150#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(tag = "state", rename_all = "snake_case")]
152pub enum Lifecycle {
153    #[default]
154    Active,
155    /// Withdrawn by its author/issuer. History is preserved.
156    Retracted,
157    /// Replaced by another record. History is preserved.
158    Superseded { by: RecordId },
159}
160
161/// The distinct times a record can carry. These are never conflated:
162/// `recorded_at` is when *this system* first recorded it (ingestion time),
163/// the others are claims about the world and are optional.
164#[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/// Namespaced, typed extension fields attached to a record. Validated against
178/// an [`crate::extension::ExtensionSchema`] registered for the namespace.
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180pub struct ExtensionData {
181    pub schema_version: SchemaVersion,
182    pub fields: BTreeMap<String, ExtValue>,
183}
184
185/// The common record envelope (current state).
186#[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    /// Fields whose values were redacted (see ADR 0006).
210    #[serde(default, skip_serializing_if = "Vec::is_empty")]
211    pub redactions: Vec<crate::event::FieldPath>,
212    /// Sequence number of the last applied event (starts at 1).
213    pub version: u64,
214    /// Recorded time of the last applied event.
215    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    /// Active (non-retracted) relations.
224    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/// Input for creating a record. The repository assigns the id, version and
234/// recorded time; clients cannot supply them.
235#[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}