1use 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 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 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 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 pub fn requires_same_kind(&self) -> bool {
95 matches!(self, Self::Duplicates | Self::Supersedes)
96 }
97}
98
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum Certainty {
103 #[default]
105 Unverified,
106 Asserted,
108 Sourced,
110 Disputed,
112}
113
114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
116pub struct Relation {
117 pub id: RelationId,
118 pub kind: RelationKind,
119 pub target: RecordId,
120 #[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 #[serde(default)]
132 pub retracted: bool,
133}
134
135#[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}