Skip to content

Commit 31e12e8

Browse files
committed
Declare blob metadata capabilities explicitly.
1 parent e6af0f6 commit 31e12e8

13 files changed

Lines changed: 408 additions & 39 deletions

File tree

‎rust/lib/bitcache-core/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ pub use put_options::*;
7979
mod repository;
8080
pub use repository::*;
8181

82+
mod repository_capabilities;
83+
pub use repository_capabilities::*;
84+
8285
mod repository_error;
8386
pub use repository_error::*;
8487

‎rust/lib/bitcache-core/src/put_options.rs‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ use core::time::Duration;
1111
pub struct PutOptions {
1212
/// Expire the blob this long after it is stored.
1313
///
14-
/// Honored by repositories that support blob expiration; others store
15-
/// the blob persistently. See
14+
/// Honored by repositories whose
15+
/// [`BlobMetadataCapabilities::expires`](crate::BlobMetadataCapabilities::expires)
16+
/// capability is set; others store the blob persistently. See
1617
/// [`Repository::put_with_options`](crate::Repository::put_with_options).
1718
pub ttl: Option<Duration>,
1819

1920
/// The explicit media type (MIME type) of the blob's contents.
2021
///
21-
/// Honored by repositories that support media-type metadata; others store
22-
/// the blob without it.
22+
/// Honored by repositories whose
23+
/// [`BlobMetadataCapabilities::media_type`](crate::BlobMetadataCapabilities::media_type)
24+
/// capability is set; others store the blob without it.
2325
#[cfg(feature = "alloc")]
2426
pub media_type: Option<Cow<'static, str>>,
2527
}

‎rust/lib/bitcache-core/src/repository.rs‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This is free and unencumbered software released into the public domain.
22

3-
use crate::{Blob, Id, ListOptions, PutOptions};
3+
use crate::{Blob, Id, ListOptions, PutOptions, RepositoryCapabilities};
44
use bytes::Bytes;
55
use core::{future::Future, time::Duration};
66
use futures_core::Stream;
@@ -20,6 +20,15 @@ pub trait Repository: Send + Sync {
2020
/// The error type returned by repository operations.
2121
type Error: Send + Sync;
2222

23+
/// Returns the optional functionality supported by this repository.
24+
///
25+
/// Capability inspection is local and does not access the backing store.
26+
/// Clients should inspect capabilities before requesting metadata that they
27+
/// require the repository to preserve.
28+
fn capabilities(&self) -> RepositoryCapabilities {
29+
RepositoryCapabilities::NONE
30+
}
31+
2332
/// Returns `true` if the repository contains no blobs.
2433
fn is_empty(&self) -> impl Future<Output = Result<bool, Self::Error>> + Send {
2534
async {
@@ -72,21 +81,26 @@ pub trait Repository: Send + Sync {
7281
/// it — where possible atomically, as part of the store itself.
7382
///
7483
/// The default implementation stores the blob with [`Repository::put`]
75-
/// and then applies the requested metadata on a best-effort basis.
76-
/// Unsupported metadata is ignored; use [`Repository::set_expiry`] or
77-
/// [`Repository::set_media_type`] directly to detect support.
84+
/// and then applies supported metadata on a best-effort basis. It consults
85+
/// [`Repository::capabilities`] first and does not attempt metadata
86+
/// operations the repository reports as unsupported.
7887
fn put_with_options(
7988
&mut self,
8089
data: Bytes,
8190
options: PutOptions,
8291
) -> impl Future<Output = Result<Id, Self::Error>> + Send {
8392
async move {
93+
let metadata_capabilities = self.capabilities().blob_metadata();
8494
let id = self.put(data).await?;
85-
if let Some(expires_nanos) = options.expires_nanos() {
95+
if metadata_capabilities.expires()
96+
&& let Some(expires_nanos) = options.expires_nanos()
97+
{
8698
self.set_expiry(&id, Some(expires_nanos)).await?;
8799
}
88100
#[cfg(feature = "alloc")]
89-
if let Some(media_type) = options.media_type() {
101+
if metadata_capabilities.media_type()
102+
&& let Some(media_type) = options.media_type()
103+
{
90104
self.set_media_type(&id, Some(media_type)).await?;
91105
}
92106
Ok(id)
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// This is free and unencumbered software released into the public domain.
2+
3+
/// Capabilities advertised by a blob repository.
4+
///
5+
/// Clients can inspect these capabilities before performing an operation that
6+
/// depends on optional repository functionality. A capability indicates that
7+
/// the repository adapter supports the feature; individual operations can
8+
/// still fail because of backing-store errors or platform limitations.
9+
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
10+
pub struct RepositoryCapabilities {
11+
blob_metadata: BlobMetadataCapabilities,
12+
}
13+
14+
impl RepositoryCapabilities {
15+
/// No optional repository capabilities.
16+
pub const NONE: Self = Self::new();
17+
18+
/// Creates an empty set of repository capabilities.
19+
pub const fn new() -> Self {
20+
Self {
21+
blob_metadata: BlobMetadataCapabilities::NONE,
22+
}
23+
}
24+
25+
/// Returns the repository's blob metadata capabilities.
26+
pub const fn blob_metadata(self) -> BlobMetadataCapabilities {
27+
self.blob_metadata
28+
}
29+
30+
/// Sets the repository's blob metadata capabilities.
31+
pub const fn with_blob_metadata(mut self, capabilities: BlobMetadataCapabilities) -> Self {
32+
self.blob_metadata = capabilities;
33+
self
34+
}
35+
}
36+
37+
/// Capabilities for metadata associated with stored blobs.
38+
///
39+
/// Creation, update, and access timestamps are maintained automatically by
40+
/// repositories that support them. Expiry and media type can additionally be
41+
/// requested through [`PutOptions`](crate::PutOptions) and changed through the
42+
/// corresponding [`Repository`](crate::Repository) methods.
43+
///
44+
/// Timestamp values can remain absent for an individual blob when its backing
45+
/// store cannot provide a reliable value, even when the adapter advertises the
46+
/// corresponding capability.
47+
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
48+
pub struct BlobMetadataCapabilities(u8);
49+
50+
impl BlobMetadataCapabilities {
51+
const CREATED: u8 = 1 << 0;
52+
const UPDATED: u8 = 1 << 1;
53+
const ACCESSED: u8 = 1 << 2;
54+
const EXPIRES: u8 = 1 << 3;
55+
const MEDIA_TYPE: u8 = 1 << 4;
56+
57+
/// No blob metadata capabilities.
58+
pub const NONE: Self = Self(0);
59+
60+
/// All blob metadata capabilities.
61+
pub const ALL: Self =
62+
Self(Self::CREATED | Self::UPDATED | Self::ACCESSED | Self::EXPIRES | Self::MEDIA_TYPE);
63+
64+
/// Creates an empty set of blob metadata capabilities.
65+
pub const fn new() -> Self {
66+
Self::NONE
67+
}
68+
69+
/// Returns whether the repository records the blob's original insertion
70+
/// timestamp.
71+
pub const fn created(self) -> bool {
72+
self.contains(Self::CREATED)
73+
}
74+
75+
/// Returns whether the repository records the blob's most recent insertion
76+
/// timestamp.
77+
pub const fn updated(self) -> bool {
78+
self.contains(Self::UPDATED)
79+
}
80+
81+
/// Returns whether the repository records identifier-based blob accesses.
82+
pub const fn accessed(self) -> bool {
83+
self.contains(Self::ACCESSED)
84+
}
85+
86+
/// Returns whether the repository can store and enforce blob expiration.
87+
pub const fn expires(self) -> bool {
88+
self.contains(Self::EXPIRES)
89+
}
90+
91+
/// Returns whether the repository can store an explicit media (MIME) type.
92+
pub const fn media_type(self) -> bool {
93+
self.contains(Self::MEDIA_TYPE)
94+
}
95+
96+
/// Adds support for the creation timestamp.
97+
pub const fn with_created(mut self) -> Self {
98+
self.0 |= Self::CREATED;
99+
self
100+
}
101+
102+
/// Adds support for the most recent insertion timestamp.
103+
pub const fn with_updated(mut self) -> Self {
104+
self.0 |= Self::UPDATED;
105+
self
106+
}
107+
108+
/// Adds support for the most recent identifier-based access timestamp.
109+
pub const fn with_accessed(mut self) -> Self {
110+
self.0 |= Self::ACCESSED;
111+
self
112+
}
113+
114+
/// Adds support for blob expiration.
115+
pub const fn with_expires(mut self) -> Self {
116+
self.0 |= Self::EXPIRES;
117+
self
118+
}
119+
120+
/// Adds support for an explicit media (MIME) type.
121+
pub const fn with_media_type(mut self) -> Self {
122+
self.0 |= Self::MEDIA_TYPE;
123+
self
124+
}
125+
126+
const fn contains(self, capability: u8) -> bool {
127+
self.0 & capability != 0
128+
}
129+
}
130+
131+
#[cfg(test)]
132+
mod tests {
133+
use super::*;
134+
use crate::{Blob, Bytes, Id, PutOptions, Repository};
135+
use core::{convert::Infallible, time::Duration};
136+
use futures_util::FutureExt;
137+
138+
struct UnsupportedMetadataRepository;
139+
140+
impl Repository for UnsupportedMetadataRepository {
141+
type Error = Infallible;
142+
143+
async fn get(&self, _id: &Id) -> Result<Option<Blob>, Self::Error> {
144+
Ok(None)
145+
}
146+
147+
async fn put(&mut self, data: Bytes) -> Result<Id, Self::Error> {
148+
Ok(Id::of(data))
149+
}
150+
151+
async fn set_expiry(
152+
&mut self,
153+
_id: &Id,
154+
_expires_nanos: Option<u64>,
155+
) -> Result<bool, Self::Error> {
156+
panic!("unsupported expiry setter must not be called")
157+
}
158+
159+
async fn set_media_type(
160+
&mut self,
161+
_id: &Id,
162+
_media_type: Option<&str>,
163+
) -> Result<bool, Self::Error> {
164+
panic!("unsupported media-type setter must not be called")
165+
}
166+
167+
async fn remove(&mut self, _id: &Id) -> Result<bool, Self::Error> {
168+
Ok(false)
169+
}
170+
171+
async fn clear(&mut self) -> Result<(), Self::Error> {
172+
Ok(())
173+
}
174+
}
175+
176+
#[test]
177+
fn default_put_skips_unsupported_metadata_operations() {
178+
let mut repository = UnsupportedMetadataRepository;
179+
let options = PutOptions::new()
180+
.with_ttl(Duration::from_secs(60))
181+
.with_media_type(Some("text/plain".into()));
182+
let result = repository
183+
.put_with_options(Bytes::from_static(b"capabilities"), options)
184+
.now_or_never()
185+
.expect("the test repository future is immediately ready");
186+
assert!(result.is_ok());
187+
}
188+
189+
struct SupportedMetadataRepository {
190+
expiry_set: bool,
191+
media_type_set: bool,
192+
}
193+
194+
impl Repository for SupportedMetadataRepository {
195+
type Error = Infallible;
196+
197+
fn capabilities(&self) -> RepositoryCapabilities {
198+
RepositoryCapabilities::new().with_blob_metadata(
199+
BlobMetadataCapabilities::new()
200+
.with_expires()
201+
.with_media_type(),
202+
)
203+
}
204+
205+
async fn get(&self, _id: &Id) -> Result<Option<Blob>, Self::Error> {
206+
Ok(None)
207+
}
208+
209+
async fn put(&mut self, data: Bytes) -> Result<Id, Self::Error> {
210+
Ok(Id::of(data))
211+
}
212+
213+
async fn set_expiry(
214+
&mut self,
215+
_id: &Id,
216+
_expires_nanos: Option<u64>,
217+
) -> Result<bool, Self::Error> {
218+
self.expiry_set = true;
219+
Ok(true)
220+
}
221+
222+
async fn set_media_type(
223+
&mut self,
224+
_id: &Id,
225+
_media_type: Option<&str>,
226+
) -> Result<bool, Self::Error> {
227+
self.media_type_set = true;
228+
Ok(true)
229+
}
230+
231+
async fn remove(&mut self, _id: &Id) -> Result<bool, Self::Error> {
232+
Ok(false)
233+
}
234+
235+
async fn clear(&mut self) -> Result<(), Self::Error> {
236+
Ok(())
237+
}
238+
}
239+
240+
#[test]
241+
fn default_put_applies_supported_metadata_operations() {
242+
let mut repository = SupportedMetadataRepository {
243+
expiry_set: false,
244+
media_type_set: false,
245+
};
246+
let options = PutOptions::new()
247+
.with_ttl(Duration::from_secs(60))
248+
.with_media_type(Some("text/plain".into()));
249+
let result = repository
250+
.put_with_options(Bytes::from_static(b"capabilities"), options)
251+
.now_or_never()
252+
.expect("the test repository future is immediately ready");
253+
254+
assert!(result.is_ok());
255+
assert!(repository.expiry_set);
256+
assert!(repository.media_type_set);
257+
}
258+
259+
#[test]
260+
fn metadata_capability_queries_reflect_builders() {
261+
let metadata = BlobMetadataCapabilities::new()
262+
.with_created()
263+
.with_updated()
264+
.with_accessed()
265+
.with_expires()
266+
.with_media_type();
267+
let capabilities = RepositoryCapabilities::new().with_blob_metadata(metadata);
268+
269+
assert!(capabilities.blob_metadata().created());
270+
assert!(capabilities.blob_metadata().updated());
271+
assert!(capabilities.blob_metadata().accessed());
272+
assert!(capabilities.blob_metadata().expires());
273+
assert!(capabilities.blob_metadata().media_type());
274+
}
275+
}

0 commit comments

Comments
 (0)