Skip to main content

laminar_connectors/storage/
provider.rs

1//! Cloud storage provider detection from URI schemes.
2//!
3//! [`StorageProvider`] identifies the cloud backend from a table path, enabling
4//! provider-specific credential resolution and validation.
5
6use std::fmt;
7
8/// Cloud storage provider detected from URI scheme.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum StorageProvider {
11    /// Amazon S3 or S3-compatible (`MinIO`, `LocalStack`).
12    AwsS3,
13    /// Azure Data Lake Storage Gen2 / Azure Blob Storage.
14    AzureAdls,
15    /// Google Cloud Storage.
16    Gcs,
17    /// Local filesystem (no credentials needed).
18    Local,
19}
20
21impl StorageProvider {
22    /// Detects the storage provider from a table path URI.
23    ///
24    /// # Examples
25    ///
26    /// ```
27    /// use laminar_connectors::storage::StorageProvider;
28    ///
29    /// assert_eq!(StorageProvider::detect("s3://bucket/path"), StorageProvider::AwsS3);
30    /// assert_eq!(StorageProvider::detect("az://container/path"), StorageProvider::AzureAdls);
31    /// assert_eq!(StorageProvider::detect("gs://bucket/path"), StorageProvider::Gcs);
32    /// assert_eq!(StorageProvider::detect("/local/path"), StorageProvider::Local);
33    /// ```
34    #[must_use]
35    pub fn detect(table_path: &str) -> Self {
36        let lower = table_path.to_lowercase();
37
38        if lower.starts_with("s3://") || lower.starts_with("s3a://") {
39            return Self::AwsS3;
40        }
41
42        if lower.starts_with("az://")
43            || lower.starts_with("abfs://")
44            || lower.starts_with("abfss://")
45            || lower.starts_with("wasb://")
46            || lower.starts_with("wasbs://")
47        {
48            return Self::AzureAdls;
49        }
50
51        if lower.starts_with("gs://") || lower.starts_with("gcs://") {
52            return Self::Gcs;
53        }
54
55        Self::Local
56    }
57
58    /// Returns true if this provider requires cloud credentials.
59    #[must_use]
60    pub const fn requires_credentials(self) -> bool {
61        !matches!(self, Self::Local)
62    }
63
64    /// Returns the display name for this provider.
65    #[must_use]
66    pub const fn name(self) -> &'static str {
67        match self {
68            Self::AwsS3 => "AWS S3",
69            Self::AzureAdls => "Azure ADLS",
70            Self::Gcs => "Google Cloud Storage",
71            Self::Local => "Local Filesystem",
72        }
73    }
74}
75
76impl fmt::Display for StorageProvider {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}", self.name())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    // ── S3 detection ──
87
88    #[test]
89    fn test_detect_s3_scheme() {
90        assert_eq!(
91            StorageProvider::detect("s3://bucket/path"),
92            StorageProvider::AwsS3
93        );
94    }
95
96    #[test]
97    fn test_detect_s3a_scheme() {
98        assert_eq!(
99            StorageProvider::detect("s3a://bucket/path"),
100            StorageProvider::AwsS3
101        );
102    }
103
104    #[test]
105    fn test_detect_s3_case_insensitive() {
106        assert_eq!(
107            StorageProvider::detect("S3://Bucket/Path"),
108            StorageProvider::AwsS3
109        );
110    }
111
112    // ── Azure detection ──
113
114    #[test]
115    fn test_detect_az_scheme() {
116        assert_eq!(
117            StorageProvider::detect("az://container/path"),
118            StorageProvider::AzureAdls
119        );
120    }
121
122    #[test]
123    fn test_detect_abfss_scheme() {
124        assert_eq!(
125            StorageProvider::detect("abfss://container@account.dfs.core.windows.net/path"),
126            StorageProvider::AzureAdls
127        );
128    }
129
130    #[test]
131    fn test_detect_abfs_scheme() {
132        assert_eq!(
133            StorageProvider::detect("abfs://container@account/path"),
134            StorageProvider::AzureAdls
135        );
136    }
137
138    #[test]
139    fn test_detect_wasbs_scheme() {
140        assert_eq!(
141            StorageProvider::detect("wasbs://container@account.blob.core.windows.net/path"),
142            StorageProvider::AzureAdls
143        );
144    }
145
146    // ── GCS detection ──
147
148    #[test]
149    fn test_detect_gs_scheme() {
150        assert_eq!(
151            StorageProvider::detect("gs://bucket/path"),
152            StorageProvider::Gcs
153        );
154    }
155
156    #[test]
157    fn test_detect_gcs_scheme() {
158        assert_eq!(
159            StorageProvider::detect("gcs://bucket/path"),
160            StorageProvider::Gcs
161        );
162    }
163
164    // ── Local detection ──
165
166    #[test]
167    fn test_detect_local_absolute() {
168        assert_eq!(
169            StorageProvider::detect("/data/tables/t1"),
170            StorageProvider::Local
171        );
172    }
173
174    #[test]
175    fn test_detect_local_relative() {
176        assert_eq!(
177            StorageProvider::detect("./data/tables"),
178            StorageProvider::Local
179        );
180    }
181
182    #[test]
183    fn test_detect_file_scheme() {
184        assert_eq!(
185            StorageProvider::detect("file:///data/tables"),
186            StorageProvider::Local
187        );
188    }
189
190    #[test]
191    fn test_detect_windows_path() {
192        assert_eq!(
193            StorageProvider::detect("C:\\data\\tables"),
194            StorageProvider::Local
195        );
196    }
197
198    #[test]
199    fn test_detect_windows_forward_slash() {
200        assert_eq!(
201            StorageProvider::detect("C:/data/tables"),
202            StorageProvider::Local
203        );
204    }
205
206    // ── Properties ──
207
208    #[test]
209    fn test_requires_credentials_cloud() {
210        assert!(StorageProvider::AwsS3.requires_credentials());
211        assert!(StorageProvider::AzureAdls.requires_credentials());
212        assert!(StorageProvider::Gcs.requires_credentials());
213    }
214
215    #[test]
216    fn test_requires_credentials_local() {
217        assert!(!StorageProvider::Local.requires_credentials());
218    }
219
220    #[test]
221    fn test_display() {
222        assert_eq!(StorageProvider::AwsS3.to_string(), "AWS S3");
223        assert_eq!(StorageProvider::AzureAdls.to_string(), "Azure ADLS");
224        assert_eq!(StorageProvider::Gcs.to_string(), "Google Cloud Storage");
225        assert_eq!(StorageProvider::Local.to_string(), "Local Filesystem");
226    }
227
228    #[test]
229    fn test_name() {
230        assert_eq!(StorageProvider::AwsS3.name(), "AWS S3");
231        assert_eq!(StorageProvider::Local.name(), "Local Filesystem");
232    }
233}