use std::fmt::{self, Debug, Formatter};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "backend", rename_all = "snake_case", deny_unknown_fields)]
pub enum StorageConfig {
Local {
root_uri: String,
},
Spfs {
endpoint: String,
root_uri: String,
credential: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
root_alias: Option<String>,
},
S3 {
root_uri: String,
credential: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
root_alias: Option<String>,
},
Gcs {
root_uri: String,
credential: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
root_alias: Option<String>,
},
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GcsSession {
pub access_token: String,
pub expires_at_micros: i64,
}
impl Debug for GcsSession {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("GcsSession")
.field("expires_at_micros", &self.expires_at_micros)
.finish_non_exhaustive()
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct S3Session {
pub access_key_id: String,
pub secret_access_key: String,
pub session_token: String,
pub region: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
pub expires_at_micros: i64,
}
impl Debug for S3Session {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("S3Session")
.field("region", &self.region)
.field("endpoint", &self.endpoint)
.field("expires_at_micros", &self.expires_at_micros)
.finish_non_exhaustive()
}
}
impl Debug for StorageConfig {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Local { root_uri } => formatter
.debug_struct("Local")
.field("root_uri", root_uri)
.finish(),
Self::Spfs {
endpoint, root_uri, ..
} => formatter
.debug_struct("Spfs")
.field("endpoint", endpoint)
.field("root_uri", root_uri)
.finish_non_exhaustive(),
Self::S3 { root_uri, .. } => formatter
.debug_struct("S3")
.field("root_uri", root_uri)
.finish_non_exhaustive(),
Self::Gcs { root_uri, .. } => formatter
.debug_struct("Gcs")
.field("root_uri", root_uri)
.finish_non_exhaustive(),
}
}
}
#[cfg(test)]
mod tests {
use super::StorageConfig;
#[test]
fn storage_configuration_is_typed_and_redacts_credentials() {
let config = StorageConfig::Spfs {
endpoint: "https://storage.example.test".into(),
root_uri: "spfsm://mount/table/".into(),
credential: "scoped-secret".into(),
root_alias: None,
};
assert!(!format!("{config:?}").contains("scoped-secret"));
let encoded = serde_json::to_value(config).unwrap();
assert_eq!(encoded["backend"], "spfs");
assert!(serde_json::from_value::<StorageConfig>(encoded).is_ok());
assert!(
serde_json::from_str::<StorageConfig>(
r#"{"backend":"local","root_uri":"file:///tmp/","credential":"unexpected"}"#
)
.is_err()
);
}
}