use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use bytes::Bytes;
use chrono::{DateTime, Utc};
use crate::err::Error;
use crate::val::{CoerceError, Datetime, File, Number, Object, Value};
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod file;
pub(crate) mod memory;
pub(crate) mod path;
pub(crate) mod prefixed;
pub use path::ObjectKey;
pub struct ObjectMeta {
pub size: u64,
pub updated: DateTime<Utc>,
pub key: ObjectKey,
}
impl ObjectMeta {
pub(crate) fn into_value(self, bucket: String) -> Value {
Value::from(map! {
"updated" => Value::from(Datetime(self.updated)),
"size" => Value::from(self.size),
"file" => Value::File(File {
bucket,
key: self.key.to_string(),
})
})
}
}
#[derive(Default)]
pub struct ListOptions {
pub start: Option<ObjectKey>,
pub prefix: Option<ObjectKey>,
pub limit: Option<usize>,
}
impl TryFrom<Object> for ListOptions {
type Error = Error;
fn try_from(mut obj: Object) -> Result<Self, Self::Error> {
let mut opts = ListOptions::default();
if let Some(start) = obj.remove("start") {
opts.start = Some(ObjectKey::new(start.coerce_to::<String>()?));
}
if let Some(prefix) = obj.remove("prefix") {
opts.prefix = Some(ObjectKey::new(prefix.coerce_to::<String>()?));
}
if let Some(limit) = obj.remove("limit") {
let n = limit.coerce_to::<i64>()?;
opts.limit = Some(usize::try_from(n).map_err(|_| CoerceError::InvalidKind {
from: Value::Number(Number::Int(n)),
into: "non-negative int".into(),
})?);
}
Ok(opts)
}
}
pub trait ObjectStore: Send + Sync + 'static {
fn put<'a>(
&'a self,
key: &'a ObjectKey,
data: Bytes,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn put_if_not_exists<'a>(
&'a self,
key: &'a ObjectKey,
data: Bytes,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn get<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<Option<Bytes>, String>> + Send + 'a>>;
fn head<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<Option<ObjectMeta>, String>> + Send + 'a>>;
fn delete<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn exists<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<bool, String>> + Send + 'a>>;
fn copy<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn copy_if_not_exists<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn rename<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn rename_if_not_exists<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
fn list<'a>(
&'a self,
prefix: &'a ListOptions,
) -> Pin<Box<dyn Future<Output = Result<Vec<ObjectMeta>, String>> + Send + 'a>>;
}
impl ObjectStore for Arc<dyn ObjectStore> {
fn put<'a>(
&'a self,
key: &'a ObjectKey,
data: Bytes,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).put(key, data)
}
fn put_if_not_exists<'a>(
&'a self,
key: &'a ObjectKey,
data: Bytes,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).put_if_not_exists(key, data)
}
fn get<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<Option<Bytes>, String>> + Send + 'a>> {
(**self).get(key)
}
fn head<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<Option<ObjectMeta>, String>> + Send + 'a>> {
(**self).head(key)
}
fn delete<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).delete(key)
}
fn exists<'a>(
&'a self,
key: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<bool, String>> + Send + 'a>> {
(**self).exists(key)
}
fn copy<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).copy(key, target)
}
fn copy_if_not_exists<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).copy_if_not_exists(key, target)
}
fn rename<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).rename(key, target)
}
fn rename_if_not_exists<'a>(
&'a self,
key: &'a ObjectKey,
target: &'a ObjectKey,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
(**self).rename_if_not_exists(key, target)
}
fn list<'a>(
&'a self,
opts: &'a ListOptions,
) -> Pin<Box<dyn Future<Output = Result<Vec<ObjectMeta>, String>> + Send + 'a>> {
(**self).list(opts)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::{ListOptions, Object, Value};
fn obj(entries: &[(&'static str, Value)]) -> Object {
let map: BTreeMap<&str, Value> = entries.iter().cloned().collect();
Object::from(map)
}
#[test]
fn limit_accepts_zero() {
let opts = ListOptions::try_from(obj(&[("limit", Value::from(0i64))]))
.expect("limit=0 should be accepted");
assert_eq!(opts.limit, Some(0));
}
#[test]
fn limit_accepts_positive() {
let opts = ListOptions::try_from(obj(&[("limit", Value::from(42i64))]))
.expect("limit=42 should be accepted");
assert_eq!(opts.limit, Some(42));
}
#[test]
fn limit_rejects_negative() {
let err = ListOptions::try_from(obj(&[("limit", Value::from(-1i64))]))
.err()
.expect("negative limit should be rejected");
assert!(err.to_string().contains("non-negative int"), "unexpected error message: {err}");
}
}