Skip to main content

razeri_plugin_contract/
storage_platform.rs

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