shuttle-opendal 0.47.0

Plugin to obtain a client connected to Apache OpenDAL
Documentation
use std::collections::HashMap;
use std::str::FromStr;

use async_trait::async_trait;
use opendal::{Operator, Scheme};
use serde::{Deserialize, Serialize};
use shuttle_service::{
    error::{CustomError, Error as ShuttleError},
    IntoResource, ResourceFactory, ResourceInputBuilder,
};

#[derive(Serialize)]
pub struct Opendal {
    scheme: String,
}

impl Default for Opendal {
    fn default() -> Self {
        Self {
            scheme: "memory".to_string(),
        }
    }
}

impl Opendal {
    pub fn scheme(mut self, scheme: &str) -> Self {
        self.scheme = scheme.to_string();
        self
    }
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct OpendalOutput {
    scheme: String,
    cfg: HashMap<String, String>,
}

pub struct Error(opendal::Error);

impl From<Error> for shuttle_service::Error {
    fn from(error: Error) -> Self {
        let msg = format!("Failed to build opendal resource: {:?}", error.0);
        ShuttleError::Custom(CustomError::msg(msg))
    }
}

#[async_trait]
impl ResourceInputBuilder for Opendal {
    type Input = OpendalOutput;
    type Output = OpendalOutput;

    async fn build(self, factory: &ResourceFactory) -> Result<Self::Input, ShuttleError> {
        Ok(OpendalOutput {
            scheme: self.scheme,
            cfg: factory
                .get_secrets()
                .into_iter()
                .map(|(k, v)| (k, v.expose().clone()))
                .collect(),
        })
    }
}

#[async_trait]
impl IntoResource<Operator> for OpendalOutput {
    async fn into_resource(self) -> Result<Operator, shuttle_service::Error> {
        let scheme = Scheme::from_str(&self.scheme).map_err(Error)?;

        Ok(Operator::via_map(scheme, self.cfg).map_err(Error)?)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use shuttle_service::Secret;

    #[tokio::test]
    async fn opendal_fs() {
        let factory = ResourceFactory::new(
            Default::default(),
            [("root", "/tmp")]
                .into_iter()
                .map(|(k, v)| (k.to_string(), Secret::new(v.to_string())))
                .collect(),
            Default::default(),
        );

        let odal = Opendal::default().scheme("fs");
        let output = odal.build(&factory).await.unwrap();
        assert_eq!(output.scheme, "fs");

        let op: Operator = output.into_resource().await.unwrap();
        assert_eq!(op.info().scheme(), Scheme::Fs)
    }

    #[tokio::test]
    async fn opendal_s3() {
        let factory = ResourceFactory::new(
            Default::default(),
            [
                ("bucket", "test"),
                ("access_key_id", "ak"),
                ("secret_access_key", "sk"),
                ("region", "us-east-1"),
            ]
            .into_iter()
            .map(|(k, v)| (k.to_string(), Secret::new(v.to_string())))
            .collect(),
            Default::default(),
        );

        let odal = Opendal::default().scheme("s3");
        let output = odal.build(&factory).await.unwrap();
        assert_eq!(output.scheme, "s3");
        assert_eq!(output.cfg.get("access_key_id").unwrap(), "ak");
        assert_eq!(output.cfg.get("secret_access_key").unwrap(), "sk");

        let op: Operator = output.into_resource().await.unwrap();
        assert_eq!(op.info().scheme(), Scheme::S3)
    }
}