1use crate::{Error, ErrorKind, LocatorId, ResourceId, Result, Timestamp, uri::normalize_uri};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct FileFacts {
8 size_bytes: u64,
9 modified_at: Option<Timestamp>,
10}
11
12impl FileFacts {
13 #[must_use]
15 pub const fn new(size_bytes: u64, modified_at: Option<Timestamp>) -> Self {
16 Self {
17 size_bytes,
18 modified_at,
19 }
20 }
21
22 #[must_use]
24 pub const fn size_bytes(self) -> u64 {
25 self.size_bytes
26 }
27
28 #[must_use]
30 pub const fn modified_at(self) -> Option<Timestamp> {
31 self.modified_at
32 }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36struct FingerprintData {
37 algorithm: String,
38 version: u16,
39 value: Vec<u8>,
40}
41
42impl FingerprintData {
43 fn new(algorithm: impl Into<String>, version: u16, value: Vec<u8>) -> Result<Self> {
44 let algorithm = algorithm.into();
45 if algorithm.is_empty()
46 || algorithm.len() > 64
47 || !algorithm
48 .bytes()
49 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
50 {
51 return Err(Error::new(
52 ErrorKind::InvalidArgument,
53 "fingerprint algorithm must be 1-64 ASCII letters, digits, '-' or '_'",
54 ));
55 }
56 if value.is_empty() {
57 return Err(Error::new(
58 ErrorKind::InvalidArgument,
59 "fingerprint value must not be empty",
60 ));
61 }
62 Ok(Self {
63 algorithm,
64 version,
65 value,
66 })
67 }
68}
69
70macro_rules! typed_fingerprint {
71 ($(#[$metadata:meta])* $name:ident) => {
72 $(#[$metadata])*
73 #[derive(Clone, Debug, Eq, PartialEq)]
74 pub struct $name(FingerprintData);
75
76 impl $name {
77 pub fn new(
83 algorithm: impl Into<String>,
84 version: u16,
85 value: Vec<u8>,
86 ) -> Result<Self> {
87 FingerprintData::new(algorithm, version, value).map(Self)
88 }
89
90 #[must_use]
92 pub fn algorithm(&self) -> &str {
93 &self.0.algorithm
94 }
95
96 #[must_use]
98 pub const fn version(&self) -> u16 {
99 self.0.version
100 }
101
102 #[must_use]
104 pub fn value(&self) -> &[u8] {
105 &self.0.value
106 }
107 }
108 };
109}
110
111typed_fingerprint!(
112 ResourceFingerprint
114);
115typed_fingerprint!(
116 RepresentationFingerprint
118);
119
120#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct Resource {
123 id: ResourceId,
124 fingerprints: Vec<ResourceFingerprint>,
125 file_facts: Option<FileFacts>,
126}
127
128impl Resource {
129 #[must_use]
131 pub fn new(
132 id: ResourceId,
133 fingerprints: Vec<ResourceFingerprint>,
134 file_facts: Option<FileFacts>,
135 ) -> Self {
136 Self {
137 id,
138 fingerprints,
139 file_facts,
140 }
141 }
142
143 #[must_use]
145 pub const fn id(&self) -> ResourceId {
146 self.id
147 }
148
149 #[must_use]
151 pub fn fingerprints(&self) -> &[ResourceFingerprint] {
152 &self.fingerprints
153 }
154
155 #[must_use]
157 pub const fn file_facts(&self) -> Option<FileFacts> {
158 self.file_facts
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
164#[non_exhaustive]
165pub enum LocatorAvailability {
166 Unknown,
168 Online,
170 Offline,
172}
173
174#[derive(Clone, Debug, Eq, PartialEq)]
176pub struct Locator {
177 id: LocatorId,
178 resource_id: ResourceId,
179 uri: String,
180 last_seen: Option<Timestamp>,
181 availability: LocatorAvailability,
182}
183
184impl Locator {
185 pub fn new(
191 id: LocatorId,
192 resource_id: ResourceId,
193 uri: impl Into<String>,
194 last_seen: Option<Timestamp>,
195 availability: LocatorAvailability,
196 ) -> Result<Self> {
197 let uri = normalize_uri(uri, "locator")?;
198 Ok(Self {
199 id,
200 resource_id,
201 uri,
202 last_seen,
203 availability,
204 })
205 }
206
207 #[must_use]
209 pub const fn id(&self) -> LocatorId {
210 self.id
211 }
212
213 #[must_use]
215 pub const fn resource_id(&self) -> ResourceId {
216 self.resource_id
217 }
218
219 #[must_use]
221 pub fn uri(&self) -> &str {
222 &self.uri
223 }
224
225 #[must_use]
227 pub const fn last_seen(&self) -> Option<Timestamp> {
228 self.last_seen
229 }
230
231 #[must_use]
233 pub const fn availability(&self) -> LocatorAvailability {
234 self.availability
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn locator_belongs_to_a_resource() {
244 let resource_id = ResourceId::new();
245 let locator = Locator::new(
246 LocatorId::new(),
247 resource_id,
248 "file:///media/clip.mov",
249 None,
250 LocatorAvailability::Unknown,
251 )
252 .expect("valid locator");
253
254 assert_eq!(locator.resource_id(), resource_id);
255 assert_eq!(locator.uri(), "file:///media/clip.mov");
256 }
257
258 #[test]
259 fn fingerprint_domains_are_explicit() {
260 let resource = ResourceFingerprint::new("blake3", 1, vec![1]).expect("valid");
261 let representation =
262 RepresentationFingerprint::new("tree-blake3", 1, vec![2]).expect("valid");
263
264 assert_eq!(resource.algorithm(), "blake3");
265 assert_eq!(representation.algorithm(), "tree-blake3");
266 assert!(ResourceFingerprint::new("contains spaces", 1, vec![1]).is_err());
267 assert!(RepresentationFingerprint::new("valid", 1, Vec::new()).is_err());
268 }
269}