Skip to main content

postproject_core/
resource.rs

1//! Storage-level resource identity and access values.
2
3use crate::{Error, ErrorKind, LocatorId, ResourceId, Result, Timestamp, uri::normalize_uri};
4
5/// Cheap filesystem facts observed for one resource.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct FileFacts {
8    size_bytes: u64,
9    modified_at: Option<Timestamp>,
10}
11
12impl FileFacts {
13    /// Creates file facts from a byte size and optional modification time.
14    #[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    /// Returns the observed file size.
23    #[must_use]
24    pub const fn size_bytes(self) -> u64 {
25        self.size_bytes
26    }
27
28    /// Returns the observed modification time, when available.
29    #[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            /// Creates typed fingerprint evidence.
78            ///
79            /// # Errors
80            ///
81            /// Returns an error when the algorithm or value is invalid.
82            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            /// Returns the algorithm identifier.
91            #[must_use]
92            pub fn algorithm(&self) -> &str {
93                &self.0.algorithm
94            }
95
96            /// Returns the algorithm format version.
97            #[must_use]
98            pub const fn version(&self) -> u16 {
99                self.0.version
100            }
101
102            /// Returns the opaque fingerprint bytes.
103            #[must_use]
104            pub fn value(&self) -> &[u8] {
105                &self.0.value
106            }
107        }
108    };
109}
110
111typed_fingerprint!(
112    /// Versioned identity evidence derived from one storage resource.
113    ResourceFingerprint
114);
115typed_fingerprint!(
116    /// Versioned, structure-aware identity evidence for a representation.
117    RepresentationFingerprint
118);
119
120/// A storage-level component used to realize a representation.
121#[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    /// Creates a resource from content evidence known at observation time.
130    #[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    /// Returns the resource's stable identity.
144    #[must_use]
145    pub const fn id(&self) -> ResourceId {
146        self.id
147    }
148
149    /// Returns stored content identity evidence.
150    #[must_use]
151    pub fn fingerprints(&self) -> &[ResourceFingerprint] {
152        &self.fingerprints
153    }
154
155    /// Returns cheap stored file facts, when available.
156    #[must_use]
157    pub const fn file_facts(&self) -> Option<FileFacts> {
158        self.file_facts
159    }
160}
161
162/// The last observed availability of a resource locator.
163#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
164#[non_exhaustive]
165pub enum LocatorAvailability {
166    /// Availability has not been checked.
167    Unknown,
168    /// The locator resolved when last checked.
169    Online,
170    /// The locator did not resolve when last checked.
171    Offline,
172}
173
174/// A URI identifying one access route to a resource.
175#[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    /// Creates a locator with a syntactically valid absolute URI.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error when `uri` is invalid or relative.
190    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    /// Returns the locator's stable identity.
208    #[must_use]
209    pub const fn id(&self) -> LocatorId {
210        self.id
211    }
212
213    /// Returns the resource made accessible by this locator.
214    #[must_use]
215    pub const fn resource_id(&self) -> ResourceId {
216        self.resource_id
217    }
218
219    /// Returns the UTF-8 URI.
220    #[must_use]
221    pub fn uri(&self) -> &str {
222        &self.uri
223    }
224
225    /// Returns when the locator was last observed online.
226    #[must_use]
227    pub const fn last_seen(&self) -> Option<Timestamp> {
228        self.last_seen
229    }
230
231    /// Returns its last observed availability.
232    #[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}