iceberg/io/storage/config/
mod.rs1mod azdls;
34mod gcs;
35mod hf;
36mod oss;
37mod s3;
38
39use std::collections::HashMap;
40
41pub use azdls::*;
42pub use gcs::*;
43pub use hf::*;
44pub use oss::*;
45pub use s3::*;
46use serde::{Deserialize, Serialize};
47
48#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
55pub struct StorageConfig {
56 props: HashMap<String, String>,
58}
59
60impl std::fmt::Debug for StorageConfig {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("StorageConfig")
66 .field("keys", &self.props.keys().collect::<Vec<_>>())
67 .finish_non_exhaustive()
68 }
69}
70
71impl StorageConfig {
72 pub fn new() -> Self {
74 Self {
75 props: HashMap::new(),
76 }
77 }
78
79 pub fn from_props(props: HashMap<String, String>) -> Self {
85 Self { props }
86 }
87
88 pub fn props(&self) -> &HashMap<String, String> {
90 &self.props
91 }
92
93 pub fn get(&self, key: &str) -> Option<&String> {
103 self.props.get(key)
104 }
105
106 pub fn with_prop(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
115 self.props.insert(key.into(), value.into());
116 self
117 }
118
119 pub fn with_props(
127 mut self,
128 props: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
129 ) -> Self {
130 self.props
131 .extend(props.into_iter().map(|(k, v)| (k.into(), v.into())));
132 self
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_storage_config_new() {
142 let config = StorageConfig::new();
143
144 assert!(config.props().is_empty());
145 }
146
147 #[test]
148 fn test_storage_config_from_props() {
149 let props = HashMap::from([
150 ("region".to_string(), "us-east-1".to_string()),
151 ("bucket".to_string(), "my-bucket".to_string()),
152 ]);
153 let config = StorageConfig::from_props(props.clone());
154
155 assert_eq!(config.props(), &props);
156 }
157
158 #[test]
159 fn test_storage_config_default() {
160 let config = StorageConfig::default();
161
162 assert!(config.props().is_empty());
163 }
164
165 #[test]
166 fn test_debug_redacts_credential_values() {
167 let config = StorageConfig::from_props(HashMap::from([
168 ("s3.access-key-id".to_string(), "vended-key".to_string()),
169 (
170 "s3.secret-access-key".to_string(),
171 "super-secret".to_string(),
172 ),
173 ("s3.session-token".to_string(), "vended-token".to_string()),
174 ]));
175
176 let rendered = format!("{config:?}");
177 assert!(
179 !rendered.contains("super-secret"),
180 "leaked secret: {rendered}"
181 );
182 assert!(
183 !rendered.contains("vended-token"),
184 "leaked token: {rendered}"
185 );
186 assert!(
188 rendered.contains("s3.secret-access-key"),
189 "keys hidden: {rendered}"
190 );
191 }
192
193 #[test]
194 fn test_storage_config_get() {
195 let config = StorageConfig::new().with_prop("region", "us-east-1");
196
197 assert_eq!(config.get("region"), Some(&"us-east-1".to_string()));
198 assert_eq!(config.get("nonexistent"), None);
199 }
200
201 #[test]
202 fn test_storage_config_with_prop() {
203 let config = StorageConfig::new()
204 .with_prop("region", "us-east-1")
205 .with_prop("bucket", "my-bucket");
206
207 assert_eq!(config.get("region"), Some(&"us-east-1".to_string()));
208 assert_eq!(config.get("bucket"), Some(&"my-bucket".to_string()));
209 }
210
211 #[test]
212 fn test_storage_config_with_props() {
213 let additional_props = vec![("key1", "value1"), ("key2", "value2")];
214 let config = StorageConfig::new().with_props(additional_props);
215
216 assert_eq!(config.get("key1"), Some(&"value1".to_string()));
217 assert_eq!(config.get("key2"), Some(&"value2".to_string()));
218 }
219
220 #[test]
221 fn test_storage_config_clone() {
222 let config = StorageConfig::new().with_prop("region", "us-east-1");
223 let cloned = config.clone();
224
225 assert_eq!(config, cloned);
226 assert_eq!(cloned.get("region"), Some(&"us-east-1".to_string()));
227 }
228
229 #[test]
230 fn test_storage_config_serialization_roundtrip() {
231 let config = StorageConfig::new()
232 .with_prop("region", "us-east-1")
233 .with_prop("bucket", "my-bucket");
234
235 let serialized = serde_json::to_string(&config).unwrap();
236 let deserialized: StorageConfig = serde_json::from_str(&serialized).unwrap();
237
238 assert_eq!(config, deserialized);
239 }
240
241 #[test]
242 fn test_storage_config_clone_independence() {
243 let original = StorageConfig::new().with_prop("region", "us-east-1");
244 let mut cloned = original.clone();
245
246 cloned = cloned.with_prop("region", "eu-west-1");
248 cloned = cloned.with_prop("new_key", "new_value");
249
250 assert_eq!(original.get("region"), Some(&"us-east-1".to_string()));
252 assert_eq!(original.get("new_key"), None);
253
254 assert_eq!(cloned.get("region"), Some(&"eu-west-1".to_string()));
256 assert_eq!(cloned.get("new_key"), Some(&"new_value".to_string()));
257 }
258
259 #[test]
260 fn test_storage_config_from_props_empty() {
261 let config = StorageConfig::from_props(HashMap::new());
262
263 assert!(config.props().is_empty());
264 }
265
266 #[test]
267 fn test_storage_config_serialization_empty() {
268 let config = StorageConfig::new();
269
270 let serialized = serde_json::to_string(&config).unwrap();
271 let deserialized: StorageConfig = serde_json::from_str(&serialized).unwrap();
272
273 assert_eq!(config, deserialized);
274 assert!(deserialized.props().is_empty());
275 }
276}