Skip to main content

pardarsh_core/
relation.rs

1//! Typed relationships between records.
2//!
3//! A relation is stored on its *source* record as an outgoing edge
4//! `source --kind--> target`. Every core relation kind has a documented
5//! direction and meaning ([`RelationKind::meaning`]) and validation rules
6//! ([`RelationKind::target_kinds`], [`RelationKind::requires_same_kind`]).
7//! A relation also carries a [`Certainty`] so that a link never implies more
8//! than its sources support.
9
10use serde::{Deserialize, Serialize};
11
12use crate::id::{RecordId, RelationId, open_vocabulary};
13use crate::model::{Attribution, RecordKind, Timestamp};
14use crate::provenance::SourceRef;
15
16open_vocabulary!(
17    /// The controlled relation vocabulary (plus namespaced extensions).
18    RelationKind {
19        Supports => "supports",
20        Contradicts => "contradicts",
21        RespondsTo => "responds_to",
22        Duplicates => "duplicates",
23        Supersedes => "supersedes",
24        Corrects => "corrects",
25        Retracts => "retracts",
26        DerivedFrom => "derived_from",
27        Cites => "cites",
28        ProposesChangeTo => "proposes_change_to",
29        DecidedBy => "decided_by",
30        IssuedBy => "issued_by",
31        AssignedTo => "assigned_to",
32        OwnedBy => "owned_by",
33        Implements => "implements",
34        FundedBy => "funded_by",
35        ProcuredThrough => "procured_through",
36        Affects => "affects",
37        LocatedAt => "located_at",
38        Fulfills => "fulfills",
39        MeasuredBy => "measured_by",
40        ResultedIn => "resulted_in",
41        Disputes => "disputes",
42        RelatedTo => "related_to",
43    }
44);
45
46impl RelationKind {
47    /// Meaning of `source --kind--> target`.
48    pub fn meaning(&self) -> &'static str {
49        match self {
50            Self::Supports => "source is offered as support for target",
51            Self::Contradicts => "source is offered as contradicting target",
52            Self::RespondsTo => "source is a response to target",
53            Self::Duplicates => "source describes the same thing as target (target is canonical)",
54            Self::Supersedes => "source replaces target",
55            Self::Corrects => "source corrects an error in target",
56            Self::Retracts => "source withdraws target",
57            Self::DerivedFrom => "source was derived from target",
58            Self::Cites => "source cites target",
59            Self::ProposesChangeTo => "source proposes a change to target",
60            Self::DecidedBy => "source was decided by decision target",
61            Self::IssuedBy => "source was issued by actor target",
62            Self::AssignedTo => "source is assigned to actor target",
63            Self::OwnedBy => "source is owned by actor target",
64            Self::Implements => "source implements target",
65            Self::FundedBy => "source is funded by target",
66            Self::ProcuredThrough => "source was procured through target",
67            Self::Affects => "source affects target",
68            Self::LocatedAt => "source is located at target",
69            Self::Fulfills => "source fulfills obligation target",
70            Self::MeasuredBy => "source is measured by target",
71            Self::ResultedIn => "source resulted in target",
72            Self::Disputes => "source disputes target",
73            Self::RelatedTo => "source is related to target in an unspecified way",
74            Self::Extension(_) => "extension relation; meaning defined by its namespace",
75        }
76    }
77
78    /// Kinds the *target* must have, when the target is known locally.
79    pub fn target_kinds(&self) -> Option<&'static [RecordKind]> {
80        const ACTOR: &[RecordKind] = &[RecordKind::Actor];
81        const DECISION: &[RecordKind] = &[RecordKind::Decision];
82        const ACTION: &[RecordKind] = &[RecordKind::Action];
83        const MEASURE: &[RecordKind] = &[RecordKind::Outcome, RecordKind::Evidence];
84        match self {
85            Self::IssuedBy | Self::AssignedTo | Self::OwnedBy => Some(ACTOR),
86            Self::DecidedBy => Some(DECISION),
87            Self::Fulfills => Some(ACTION),
88            Self::MeasuredBy => Some(MEASURE),
89            _ => None,
90        }
91    }
92
93    /// Relations that only make sense between records of the same kind.
94    pub fn requires_same_kind(&self) -> bool {
95        matches!(self, Self::Duplicates | Self::Supersedes)
96    }
97}
98
99/// How certain the link is, as asserted by whoever recorded it.
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum Certainty {
103    /// Suggested but not checked (e.g. "this might be the responsible body").
104    #[default]
105    Unverified,
106    /// Asserted by the recording actor on their own authority.
107    Asserted,
108    /// Backed by at least one cited source. Requires `sources` to be non-empty.
109    Sourced,
110    /// The link itself is contested.
111    Disputed,
112}
113
114/// A relationship as stored on the source record.
115#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
116pub struct Relation {
117    pub id: RelationId,
118    pub kind: RelationKind,
119    pub target: RecordId,
120    /// Where a remote target can be retrieved, if known.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub target_locator: Option<String>,
123    pub certainty: Certainty,
124    pub asserted_by: Attribution,
125    pub asserted_at: Timestamp,
126    #[serde(default, skip_serializing_if = "Vec::is_empty")]
127    pub sources: Vec<SourceRef>,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub note: Option<String>,
130    /// Retracted relations are kept for history but excluded from traversal.
131    #[serde(default)]
132    pub retracted: bool,
133}
134
135/// Input for asserting a relation. Assertion metadata comes from the command.
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
137pub struct RelationDraft {
138    pub kind: RelationKind,
139    pub target: RecordId,
140    #[serde(default)]
141    pub target_locator: Option<String>,
142    #[serde(default)]
143    pub certainty: Certainty,
144    #[serde(default)]
145    pub sources: Vec<SourceRef>,
146    #[serde(default)]
147    pub note: Option<String>,
148}
149
150impl RelationDraft {
151    pub fn new(kind: RelationKind, target: RecordId) -> Self {
152        Self { kind, target, target_locator: None, certainty: Certainty::Asserted, sources: Vec::new(), note: None }
153    }
154
155    pub fn certainty(mut self, c: Certainty) -> Self {
156        self.certainty = c;
157        self
158    }
159
160    pub fn source(mut self, s: SourceRef) -> Self {
161        self.sources.push(s);
162        self.certainty = Certainty::Sourced;
163        self
164    }
165
166    pub fn note(mut self, n: impl Into<String>) -> Self {
167        self.note = Some(n.into());
168        self
169    }
170
171    pub(crate) fn into_relation(self, asserted_by: Attribution, asserted_at: Timestamp) -> Relation {
172        Relation {
173            id: RelationId::generate(),
174            kind: self.kind,
175            target: self.target,
176            target_locator: self.target_locator,
177            certainty: self.certainty,
178            asserted_by,
179            asserted_at,
180            sources: self.sources,
181            note: self.note,
182            retracted: false,
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn vocabulary_round_trips() {
193        for term in RelationKind::CORE {
194            let k: RelationKind = term.parse().unwrap();
195            assert_eq!(&k.as_str(), term);
196            assert!(!k.meaning().is_empty());
197        }
198        let ext: RelationKind = "procurement:awarded_to".parse().unwrap();
199        assert!(ext.is_extension());
200        assert!("not_a_relation".parse::<RelationKind>().is_err());
201    }
202}