1use std::fmt;
10use std::str::FromStr;
11
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13use sha2::{Digest, Sha256};
14
15use crate::id::SourceId;
16use crate::model::{Attribution, Timestamp, Visibility};
17
18pub const ALLOWED_LOCATOR_SCHEMES: &[&str] = &["https", "http", "urn", "doi"];
21
22#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum SourceAuthority {
26 Official,
29 ThirdParty,
31 UserSubmitted,
33 #[default]
34 Unknown,
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(tag = "type", rename_all = "snake_case")]
40pub enum Derivation {
41 #[default]
43 Original,
44 Extracted { method: String },
46 Summary { author: Option<Attribution> },
48 MachineGenerated { method: String, reviewed: bool },
50}
51
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
54pub struct SourceRef {
55 pub id: SourceId,
56 pub locator: String,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub title: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub media_type: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub issuer: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub published_at: Option<Timestamp>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub retrieved_at: Option<Timestamp>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub content_hash: Option<ContentHash>,
74 #[serde(default)]
75 pub authority: SourceAuthority,
76 #[serde(default)]
77 pub derivation: Derivation,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub submitted_by: Option<Attribution>,
81 #[serde(default)]
84 pub visibility: Visibility,
85}
86
87impl SourceRef {
88 pub fn new(locator: impl Into<String>) -> Self {
89 Self {
90 id: SourceId::generate(),
91 locator: locator.into(),
92 title: None,
93 media_type: None,
94 issuer: None,
95 published_at: None,
96 retrieved_at: None,
97 content_hash: None,
98 authority: SourceAuthority::Unknown,
99 derivation: Derivation::Original,
100 submitted_by: None,
101 visibility: Visibility::Public,
102 }
103 }
104
105 pub fn title(mut self, t: impl Into<String>) -> Self {
106 self.title = Some(t.into());
107 self
108 }
109
110 pub fn issuer(mut self, i: impl Into<String>) -> Self {
111 self.issuer = Some(i.into());
112 self
113 }
114
115 pub fn authority(mut self, a: SourceAuthority) -> Self {
116 self.authority = a;
117 self
118 }
119
120 pub fn retrieved_at(mut self, t: Timestamp) -> Self {
121 self.retrieved_at = Some(t);
122 self
123 }
124
125 pub fn published_at(mut self, t: Timestamp) -> Self {
126 self.published_at = Some(t);
127 self
128 }
129
130 pub fn submitted_by(mut self, a: Attribution) -> Self {
131 self.submitted_by = Some(a);
132 self
133 }
134
135 pub fn visibility(mut self, v: Visibility) -> Self {
136 self.visibility = v;
137 self
138 }
139
140 pub fn derivation(mut self, d: Derivation) -> Self {
141 self.derivation = d;
142 self
143 }
144
145 pub fn scheme(&self) -> Option<String> {
147 let (scheme, _) = self.locator.split_once(':')?;
148 Some(scheme.to_ascii_lowercase())
149 }
150}
151
152#[derive(Clone, Debug, PartialEq, Eq, Hash)]
158pub struct ContentHash(String);
159
160impl ContentHash {
161 pub fn sha256(bytes: &[u8]) -> Self {
162 Self(format!("sha256:{}", hex::encode(Sha256::digest(bytes))))
163 }
164
165 pub fn as_str(&self) -> &str {
166 &self.0
167 }
168}
169
170impl fmt::Display for ContentHash {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 f.write_str(&self.0)
173 }
174}
175
176impl FromStr for ContentHash {
177 type Err = String;
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 match s.strip_prefix("sha256:") {
180 Some(h) if h.len() == 64 && h.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) => {
181 Ok(Self(s.to_string()))
182 }
183 _ => Err(format!("invalid content hash {s:?}: expected sha256:<64 lowercase hex>")),
184 }
185 }
186}
187
188impl Serialize for ContentHash {
189 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
190 s.serialize_str(&self.0)
191 }
192}
193
194impl<'de> Deserialize<'de> for ContentHash {
195 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
196 let s = String::deserialize(d)?;
197 s.parse().map_err(serde::de::Error::custom)
198 }
199}