Skip to main content

razeri_plugin_contract/
storage_platform.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Storage platform enumeration for raw file management.
6//!
7//! [`StoragePlatform`] mirrors the `DialectFamily` pattern from `razeri-types`:
8//! exhaustive matching, agnostic design-time variant, compiler-enforced handling.
9
10use serde::{Deserialize, Serialize};
11
12/// Storage platform for raw file management.
13///
14/// Adding a variant forces exhaustive handling across all `match` arms in
15/// `StoragePath::render()`, `StorageOperations` implementations, and CLI commands.
16///
17/// # Platform Categories
18///
19/// - **Local filesystems**: `Linux`, `Windows`
20/// - **Cloud object stores**: `S3`, `Adls`, `Gcs`
21/// - **Distributed filesystems**: `Hdfs`, `JuiceFs`, `MaprFs`, `CephFs`, `SeaweedFs`
22/// - **Abstract**: `Agnostic` (design-time only, cannot perform I/O)
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum StoragePlatform {
26    /// Local filesystem (POSIX path separators)
27    Linux,
28    /// Local filesystem (Windows path separators, drive letters)
29    Windows,
30    /// Amazon Simple Storage Service
31    S3,
32    /// Azure Data Lake Storage Gen2
33    Adls,
34    /// Google Cloud Storage
35    Gcs,
36    /// Apache Hadoop Distributed File System
37    Hdfs,
38    /// `JuiceFS` — POSIX-compatible distributed filesystem
39    JuiceFs,
40    /// `MapR` Filesystem (HPE Ezmeral Data Fabric)
41    MaprFs,
42    /// Ceph Distributed Filesystem
43    CephFs,
44    /// `SeaweedFS` — distributed object/file store
45    SeaweedFs,
46    /// Platform-independent abstract paths (forward slash, no scheme).
47    /// Design-time only — cannot be used for actual I/O operations.
48    Agnostic,
49}
50
51impl StoragePlatform {
52    /// Returns true if this platform supports atomic rename/move.
53    #[must_use]
54    pub fn supports_atomic_move(&self) -> bool {
55        matches!(
56            self,
57            Self::Linux
58                | Self::Windows
59                | Self::Adls
60                | Self::Hdfs
61                | Self::JuiceFs
62                | Self::MaprFs
63                | Self::CephFs
64        )
65    }
66
67    /// Returns true if this platform supports native file change notifications.
68    #[must_use]
69    pub fn supports_native_notify(&self) -> bool {
70        matches!(
71            self,
72            Self::Linux
73                | Self::Windows
74                | Self::S3
75                | Self::Adls
76                | Self::Gcs
77                | Self::JuiceFs
78                | Self::MaprFs
79                | Self::CephFs
80        )
81    }
82
83    /// Returns true if this platform supports object versioning.
84    #[must_use]
85    pub fn supports_versioning(&self) -> bool {
86        matches!(self, Self::S3 | Self::Adls | Self::Gcs | Self::MaprFs | Self::CephFs)
87    }
88
89    /// Returns true if this platform supports lifecycle/TTL rules.
90    #[must_use]
91    pub fn supports_lifecycle_rules(&self) -> bool {
92        matches!(self, Self::S3 | Self::Adls | Self::Gcs | Self::JuiceFs | Self::SeaweedFs)
93    }
94
95    /// Returns the path separator for this platform.
96    #[must_use]
97    pub fn path_separator(&self) -> &'static str {
98        match self {
99            Self::Windows => "\\",
100            _ => "/",
101        }
102    }
103
104    /// Returns true if this is a design-time-only variant that cannot perform I/O.
105    #[must_use]
106    pub fn is_abstract(&self) -> bool {
107        matches!(self, Self::Agnostic)
108    }
109
110    /// Returns all concrete (non-abstract) variants.
111    #[must_use]
112    pub fn concrete_variants() -> &'static [StoragePlatform] {
113        &[
114            Self::Linux,
115            Self::Windows,
116            Self::S3,
117            Self::Adls,
118            Self::Gcs,
119            Self::Hdfs,
120            Self::JuiceFs,
121            Self::MaprFs,
122            Self::CephFs,
123            Self::SeaweedFs,
124        ]
125    }
126
127    /// Returns all variants including Agnostic.
128    #[must_use]
129    pub fn all_variants() -> &'static [StoragePlatform] {
130        &[
131            Self::Linux,
132            Self::Windows,
133            Self::S3,
134            Self::Adls,
135            Self::Gcs,
136            Self::Hdfs,
137            Self::JuiceFs,
138            Self::MaprFs,
139            Self::CephFs,
140            Self::SeaweedFs,
141            Self::Agnostic,
142        ]
143    }
144}
145
146impl std::fmt::Display for StoragePlatform {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        match self {
149            Self::Linux => write!(f, "linux"),
150            Self::Windows => write!(f, "windows"),
151            Self::S3 => write!(f, "s3"),
152            Self::Adls => write!(f, "adls"),
153            Self::Gcs => write!(f, "gcs"),
154            Self::Hdfs => write!(f, "hdfs"),
155            Self::JuiceFs => write!(f, "juicefs"),
156            Self::MaprFs => write!(f, "maprfs"),
157            Self::CephFs => write!(f, "cephfs"),
158            Self::SeaweedFs => write!(f, "seaweedfs"),
159            Self::Agnostic => write!(f, "agnostic"),
160        }
161    }
162}
163
164impl std::str::FromStr for StoragePlatform {
165    type Err = String;
166
167    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
168        match s.to_lowercase().as_str() {
169            "linux" => Ok(Self::Linux),
170            "windows" => Ok(Self::Windows),
171            "s3" => Ok(Self::S3),
172            "adls" => Ok(Self::Adls),
173            "gcs" => Ok(Self::Gcs),
174            "hdfs" => Ok(Self::Hdfs),
175            "juicefs" => Ok(Self::JuiceFs),
176            "maprfs" => Ok(Self::MaprFs),
177            "cephfs" => Ok(Self::CephFs),
178            "seaweedfs" => Ok(Self::SeaweedFs),
179            "agnostic" => Ok(Self::Agnostic),
180            _ => Err(format!("Unknown storage platform: '{s}'")),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn serde_json_round_trip_all_variants() {
191        let variants = vec![
192            (StoragePlatform::Linux, "\"linux\""),
193            (StoragePlatform::Windows, "\"windows\""),
194            (StoragePlatform::S3, "\"s3\""),
195            (StoragePlatform::Adls, "\"adls\""),
196            (StoragePlatform::Gcs, "\"gcs\""),
197            (StoragePlatform::Hdfs, "\"hdfs\""),
198            (StoragePlatform::JuiceFs, "\"juice_fs\""),
199            (StoragePlatform::MaprFs, "\"mapr_fs\""),
200            (StoragePlatform::CephFs, "\"ceph_fs\""),
201            (StoragePlatform::SeaweedFs, "\"seaweed_fs\""),
202            (StoragePlatform::Agnostic, "\"agnostic\""),
203        ];
204        for (variant, expected_json) in &variants {
205            let serialized = serde_json::to_string(variant).unwrap();
206            assert_eq!(&serialized, expected_json, "serialize {variant:?}");
207            let deserialized: StoragePlatform = serde_json::from_str(expected_json).unwrap();
208            assert_eq!(&deserialized, variant, "deserialize {expected_json}");
209        }
210    }
211
212    #[test]
213    fn serde_yaml_round_trip() {
214        for variant in StoragePlatform::all_variants() {
215            let yaml = serde_yaml::to_string(variant).unwrap();
216            let parsed: StoragePlatform = serde_yaml::from_str(&yaml).unwrap();
217            assert_eq!(parsed, *variant, "YAML round-trip failed for {variant:?}");
218        }
219    }
220
221    #[test]
222    fn capabilities_s3() {
223        let s3 = StoragePlatform::S3;
224        assert!(!s3.supports_atomic_move());
225        assert!(s3.supports_native_notify());
226        assert!(s3.supports_versioning());
227        assert!(s3.supports_lifecycle_rules());
228        assert_eq!(s3.path_separator(), "/");
229        assert!(!s3.is_abstract());
230    }
231
232    #[test]
233    fn capabilities_linux() {
234        let linux = StoragePlatform::Linux;
235        assert!(linux.supports_atomic_move());
236        assert!(linux.supports_native_notify());
237        assert!(!linux.supports_versioning());
238        assert!(!linux.supports_lifecycle_rules());
239        assert_eq!(linux.path_separator(), "/");
240        assert!(!linux.is_abstract());
241    }
242
243    #[test]
244    fn capabilities_windows() {
245        let win = StoragePlatform::Windows;
246        assert!(win.supports_atomic_move());
247        assert!(win.supports_native_notify());
248        assert!(!win.supports_versioning());
249        assert!(!win.supports_lifecycle_rules());
250        assert_eq!(win.path_separator(), "\\");
251        assert!(!win.is_abstract());
252    }
253
254    #[test]
255    fn capabilities_adls() {
256        let adls = StoragePlatform::Adls;
257        assert!(adls.supports_atomic_move());
258        assert!(adls.supports_native_notify());
259        assert!(adls.supports_versioning());
260        assert!(adls.supports_lifecycle_rules());
261    }
262
263    #[test]
264    fn capabilities_gcs() {
265        let gcs = StoragePlatform::Gcs;
266        assert!(!gcs.supports_atomic_move());
267        assert!(gcs.supports_native_notify());
268        assert!(gcs.supports_versioning());
269        assert!(gcs.supports_lifecycle_rules());
270    }
271
272    #[test]
273    fn capabilities_hdfs() {
274        let hdfs = StoragePlatform::Hdfs;
275        assert!(hdfs.supports_atomic_move());
276        assert!(!hdfs.supports_native_notify());
277        assert!(!hdfs.supports_versioning());
278        assert!(!hdfs.supports_lifecycle_rules());
279    }
280
281    #[test]
282    fn capabilities_juicefs() {
283        let jfs = StoragePlatform::JuiceFs;
284        assert!(jfs.supports_atomic_move());
285        assert!(jfs.supports_native_notify());
286        assert!(!jfs.supports_versioning());
287        assert!(jfs.supports_lifecycle_rules());
288    }
289
290    #[test]
291    fn capabilities_maprfs() {
292        let mapr = StoragePlatform::MaprFs;
293        assert!(mapr.supports_atomic_move());
294        assert!(mapr.supports_native_notify());
295        assert!(mapr.supports_versioning());
296        assert!(!mapr.supports_lifecycle_rules());
297    }
298
299    #[test]
300    fn capabilities_cephfs() {
301        let ceph = StoragePlatform::CephFs;
302        assert!(ceph.supports_atomic_move());
303        assert!(ceph.supports_native_notify());
304        assert!(ceph.supports_versioning());
305        assert!(!ceph.supports_lifecycle_rules());
306    }
307
308    #[test]
309    fn capabilities_seaweedfs() {
310        let sw = StoragePlatform::SeaweedFs;
311        assert!(!sw.supports_atomic_move());
312        assert!(!sw.supports_native_notify());
313        assert!(!sw.supports_versioning());
314        assert!(sw.supports_lifecycle_rules());
315    }
316
317    #[test]
318    fn agnostic_is_abstract() {
319        assert!(StoragePlatform::Agnostic.is_abstract());
320        for variant in StoragePlatform::concrete_variants() {
321            assert!(!variant.is_abstract(), "{variant:?} should not be abstract");
322        }
323    }
324
325    #[test]
326    fn display_all_variants() {
327        assert_eq!(StoragePlatform::Linux.to_string(), "linux");
328        assert_eq!(StoragePlatform::Windows.to_string(), "windows");
329        assert_eq!(StoragePlatform::S3.to_string(), "s3");
330        assert_eq!(StoragePlatform::Adls.to_string(), "adls");
331        assert_eq!(StoragePlatform::Gcs.to_string(), "gcs");
332        assert_eq!(StoragePlatform::Hdfs.to_string(), "hdfs");
333        assert_eq!(StoragePlatform::JuiceFs.to_string(), "juicefs");
334        assert_eq!(StoragePlatform::MaprFs.to_string(), "maprfs");
335        assert_eq!(StoragePlatform::CephFs.to_string(), "cephfs");
336        assert_eq!(StoragePlatform::SeaweedFs.to_string(), "seaweedfs");
337        assert_eq!(StoragePlatform::Agnostic.to_string(), "agnostic");
338    }
339
340    #[test]
341    fn from_str_all_variants() {
342        assert_eq!("linux".parse::<StoragePlatform>().unwrap(), StoragePlatform::Linux);
343        assert_eq!("S3".parse::<StoragePlatform>().unwrap(), StoragePlatform::S3);
344        assert_eq!("ADLS".parse::<StoragePlatform>().unwrap(), StoragePlatform::Adls);
345        assert_eq!("GCS".parse::<StoragePlatform>().unwrap(), StoragePlatform::Gcs);
346        assert_eq!("HdFs".parse::<StoragePlatform>().unwrap(), StoragePlatform::Hdfs);
347        assert_eq!("agnostic".parse::<StoragePlatform>().unwrap(), StoragePlatform::Agnostic);
348    }
349
350    #[test]
351    fn from_str_unknown_returns_error() {
352        let result = "unknown".parse::<StoragePlatform>();
353        assert!(result.is_err());
354        assert!(result.unwrap_err().contains("Unknown storage platform"));
355    }
356
357    #[test]
358    fn all_variants_has_correct_count() {
359        assert_eq!(StoragePlatform::all_variants().len(), 11);
360    }
361
362    #[test]
363    fn concrete_variants_excludes_agnostic() {
364        let concrete = StoragePlatform::concrete_variants();
365        assert_eq!(concrete.len(), 10);
366        assert!(!concrete.contains(&StoragePlatform::Agnostic));
367    }
368
369    #[test]
370    fn all_variants_have_non_empty_path_separator() {
371        for variant in StoragePlatform::all_variants() {
372            let sep = variant.path_separator();
373            assert!(!sep.is_empty(), "{variant:?} has empty separator");
374        }
375    }
376
377    #[test]
378    fn display_round_trips_through_from_str() {
379        for variant in StoragePlatform::all_variants() {
380            let display = variant.to_string();
381            let parsed: StoragePlatform = display.parse().unwrap();
382            assert_eq!(parsed, *variant, "Display -> FromStr round-trip failed for {variant:?}");
383        }
384    }
385}