iceberg/io/config/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18// TODO Add specific configs
19//! Storage configuration for storage backends.
20//!
21//! This module provides configuration types for various storage backends.
22//! The configuration types are designed to be used with the `StorageFactory`
23//! trait to create storage instances.
24//!
25//! # Available Configurations
26//!
27//! - [`StorageConfig`]: Base configuration containing properties for storage backends
28//! - [`S3Config`]: Amazon S3 specific configuration
29//! - [`GcsConfig`]: Google Cloud Storage specific configuration
30//! - [`OssConfig`]: Alibaba Cloud OSS specific configuration
31//! - [`AzdlsConfig`]: Azure Data Lake Storage specific configuration
32
33mod azdls;
34mod gcs;
35mod oss;
36mod s3;
37
38use std::collections::HashMap;
39
40pub use azdls::*;
41pub use gcs::*;
42pub use oss::*;
43pub use s3::*;
44use serde::{Deserialize, Serialize};
45
46/// Configuration properties for storage backends.
47///
48/// This struct contains only configuration properties without specifying
49/// which storage backend to use. The storage type is determined by the
50/// explicit factory selection.
51/// ```
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
53pub struct StorageConfig {
54    /// Configuration properties for the storage backend
55    props: HashMap<String, String>,
56}
57
58impl StorageConfig {
59    /// Create a new empty StorageConfig.
60    pub fn new() -> Self {
61        Self {
62            props: HashMap::new(),
63        }
64    }
65
66    /// Create a StorageConfig from existing properties.
67    ///
68    /// # Arguments
69    ///
70    /// * `props` - Configuration properties for the storage backend
71    pub fn from_props(props: HashMap<String, String>) -> Self {
72        Self { props }
73    }
74
75    /// Get all configuration properties.
76    pub fn props(&self) -> &HashMap<String, String> {
77        &self.props
78    }
79
80    /// Get a specific configuration property by key.
81    ///
82    /// # Arguments
83    ///
84    /// * `key` - The property key to look up
85    ///
86    /// # Returns
87    ///
88    /// An `Option` containing a reference to the property value if it exists.
89    pub fn get(&self, key: &str) -> Option<&String> {
90        self.props.get(key)
91    }
92
93    /// Add a configuration property.
94    ///
95    /// This is a builder-style method that returns `self` for chaining.
96    ///
97    /// # Arguments
98    ///
99    /// * `key` - The property key
100    /// * `value` - The property value
101    pub fn with_prop(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
102        self.props.insert(key.into(), value.into());
103        self
104    }
105
106    /// Add multiple configuration properties.
107    ///
108    /// This is a builder-style method that returns `self` for chaining.
109    ///
110    /// # Arguments
111    ///
112    /// * `props` - An iterator of key-value pairs to add
113    pub fn with_props(
114        mut self,
115        props: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
116    ) -> Self {
117        self.props
118            .extend(props.into_iter().map(|(k, v)| (k.into(), v.into())));
119        self
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_storage_config_new() {
129        let config = StorageConfig::new();
130
131        assert!(config.props().is_empty());
132    }
133
134    #[test]
135    fn test_storage_config_from_props() {
136        let props = HashMap::from([
137            ("region".to_string(), "us-east-1".to_string()),
138            ("bucket".to_string(), "my-bucket".to_string()),
139        ]);
140        let config = StorageConfig::from_props(props.clone());
141
142        assert_eq!(config.props(), &props);
143    }
144
145    #[test]
146    fn test_storage_config_default() {
147        let config = StorageConfig::default();
148
149        assert!(config.props().is_empty());
150    }
151
152    #[test]
153    fn test_storage_config_get() {
154        let config = StorageConfig::new().with_prop("region", "us-east-1");
155
156        assert_eq!(config.get("region"), Some(&"us-east-1".to_string()));
157        assert_eq!(config.get("nonexistent"), None);
158    }
159
160    #[test]
161    fn test_storage_config_with_prop() {
162        let config = StorageConfig::new()
163            .with_prop("region", "us-east-1")
164            .with_prop("bucket", "my-bucket");
165
166        assert_eq!(config.get("region"), Some(&"us-east-1".to_string()));
167        assert_eq!(config.get("bucket"), Some(&"my-bucket".to_string()));
168    }
169
170    #[test]
171    fn test_storage_config_with_props() {
172        let additional_props = vec![("key1", "value1"), ("key2", "value2")];
173        let config = StorageConfig::new().with_props(additional_props);
174
175        assert_eq!(config.get("key1"), Some(&"value1".to_string()));
176        assert_eq!(config.get("key2"), Some(&"value2".to_string()));
177    }
178
179    #[test]
180    fn test_storage_config_clone() {
181        let config = StorageConfig::new().with_prop("region", "us-east-1");
182        let cloned = config.clone();
183
184        assert_eq!(config, cloned);
185        assert_eq!(cloned.get("region"), Some(&"us-east-1".to_string()));
186    }
187
188    #[test]
189    fn test_storage_config_serialization_roundtrip() {
190        let config = StorageConfig::new()
191            .with_prop("region", "us-east-1")
192            .with_prop("bucket", "my-bucket");
193
194        let serialized = serde_json::to_string(&config).unwrap();
195        let deserialized: StorageConfig = serde_json::from_str(&serialized).unwrap();
196
197        assert_eq!(config, deserialized);
198    }
199
200    #[test]
201    fn test_storage_config_clone_independence() {
202        let original = StorageConfig::new().with_prop("region", "us-east-1");
203        let mut cloned = original.clone();
204
205        // Modify the clone
206        cloned = cloned.with_prop("region", "eu-west-1");
207        cloned = cloned.with_prop("new_key", "new_value");
208
209        // Original should be unchanged
210        assert_eq!(original.get("region"), Some(&"us-east-1".to_string()));
211        assert_eq!(original.get("new_key"), None);
212
213        // Clone should have the new values
214        assert_eq!(cloned.get("region"), Some(&"eu-west-1".to_string()));
215        assert_eq!(cloned.get("new_key"), Some(&"new_value".to_string()));
216    }
217
218    #[test]
219    fn test_storage_config_from_props_empty() {
220        let config = StorageConfig::from_props(HashMap::new());
221
222        assert!(config.props().is_empty());
223    }
224
225    #[test]
226    fn test_storage_config_serialization_empty() {
227        let config = StorageConfig::new();
228
229        let serialized = serde_json::to_string(&config).unwrap();
230        let deserialized: StorageConfig = serde_json::from_str(&serialized).unwrap();
231
232        assert_eq!(config, deserialized);
233        assert!(deserialized.props().is_empty());
234    }
235}