razeri-plugin-contract 0.7.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! Storage platform enumeration for raw file management.
//!
//! [`StoragePlatform`] mirrors the `DialectFamily` pattern from `razeri-types`:
//! exhaustive matching, agnostic design-time variant, compiler-enforced handling.

use serde::{Deserialize, Serialize};

/// Storage platform for raw file management.
///
/// Adding a variant forces exhaustive handling across all `match` arms in
/// `StoragePath::render()`, `StorageOperations` implementations, and CLI commands.
///
/// # Platform Categories
///
/// - **Local filesystems**: `Linux`, `Windows`
/// - **Cloud object stores**: `S3`, `Adls`, `Gcs`
/// - **Distributed filesystems**: `Hdfs`, `JuiceFs`, `MaprFs`, `CephFs`, `SeaweedFs`
/// - **Abstract**: `Agnostic` (design-time only, cannot perform I/O)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StoragePlatform {
    /// Local filesystem (POSIX path separators)
    Linux,
    /// Local filesystem (Windows path separators, drive letters)
    Windows,
    /// Amazon Simple Storage Service
    S3,
    /// Azure Data Lake Storage Gen2
    Adls,
    /// Google Cloud Storage
    Gcs,
    /// Apache Hadoop Distributed File System
    Hdfs,
    /// `JuiceFS` — POSIX-compatible distributed filesystem
    JuiceFs,
    /// `MapR` Filesystem (HPE Ezmeral Data Fabric)
    MaprFs,
    /// Ceph Distributed Filesystem
    CephFs,
    /// `SeaweedFS` — distributed object/file store
    SeaweedFs,
    /// Platform-independent abstract paths (forward slash, no scheme).
    /// Design-time only — cannot be used for actual I/O operations.
    Agnostic,
}

impl StoragePlatform {
    /// Returns true if this platform supports atomic rename/move.
    #[must_use]
    pub fn supports_atomic_move(&self) -> bool {
        matches!(
            self,
            Self::Linux
                | Self::Windows
                | Self::Adls
                | Self::Hdfs
                | Self::JuiceFs
                | Self::MaprFs
                | Self::CephFs
        )
    }

    /// Returns true if this platform supports native file change notifications.
    #[must_use]
    pub fn supports_native_notify(&self) -> bool {
        matches!(
            self,
            Self::Linux
                | Self::Windows
                | Self::S3
                | Self::Adls
                | Self::Gcs
                | Self::JuiceFs
                | Self::MaprFs
                | Self::CephFs
        )
    }

    /// Returns true if this platform supports object versioning.
    #[must_use]
    pub fn supports_versioning(&self) -> bool {
        matches!(self, Self::S3 | Self::Adls | Self::Gcs | Self::MaprFs | Self::CephFs)
    }

    /// Returns true if this platform supports lifecycle/TTL rules.
    #[must_use]
    pub fn supports_lifecycle_rules(&self) -> bool {
        matches!(self, Self::S3 | Self::Adls | Self::Gcs | Self::JuiceFs | Self::SeaweedFs)
    }

    /// Returns the path separator for this platform.
    #[must_use]
    pub fn path_separator(&self) -> &'static str {
        match self {
            Self::Windows => "\\",
            _ => "/",
        }
    }

    /// Returns true if this is a design-time-only variant that cannot perform I/O.
    #[must_use]
    pub fn is_abstract(&self) -> bool {
        matches!(self, Self::Agnostic)
    }

    /// Returns all concrete (non-abstract) variants.
    #[must_use]
    pub fn concrete_variants() -> &'static [StoragePlatform] {
        &[
            Self::Linux,
            Self::Windows,
            Self::S3,
            Self::Adls,
            Self::Gcs,
            Self::Hdfs,
            Self::JuiceFs,
            Self::MaprFs,
            Self::CephFs,
            Self::SeaweedFs,
        ]
    }

    /// Returns all variants including Agnostic.
    #[must_use]
    pub fn all_variants() -> &'static [StoragePlatform] {
        &[
            Self::Linux,
            Self::Windows,
            Self::S3,
            Self::Adls,
            Self::Gcs,
            Self::Hdfs,
            Self::JuiceFs,
            Self::MaprFs,
            Self::CephFs,
            Self::SeaweedFs,
            Self::Agnostic,
        ]
    }
}

impl std::fmt::Display for StoragePlatform {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Linux => write!(f, "linux"),
            Self::Windows => write!(f, "windows"),
            Self::S3 => write!(f, "s3"),
            Self::Adls => write!(f, "adls"),
            Self::Gcs => write!(f, "gcs"),
            Self::Hdfs => write!(f, "hdfs"),
            Self::JuiceFs => write!(f, "juicefs"),
            Self::MaprFs => write!(f, "maprfs"),
            Self::CephFs => write!(f, "cephfs"),
            Self::SeaweedFs => write!(f, "seaweedfs"),
            Self::Agnostic => write!(f, "agnostic"),
        }
    }
}

impl std::str::FromStr for StoragePlatform {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "linux" => Ok(Self::Linux),
            "windows" => Ok(Self::Windows),
            "s3" => Ok(Self::S3),
            "adls" => Ok(Self::Adls),
            "gcs" => Ok(Self::Gcs),
            "hdfs" => Ok(Self::Hdfs),
            "juicefs" => Ok(Self::JuiceFs),
            "maprfs" => Ok(Self::MaprFs),
            "cephfs" => Ok(Self::CephFs),
            "seaweedfs" => Ok(Self::SeaweedFs),
            "agnostic" => Ok(Self::Agnostic),
            _ => Err(format!("Unknown storage platform: '{s}'")),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_json_round_trip_all_variants() {
        let variants = vec![
            (StoragePlatform::Linux, "\"linux\""),
            (StoragePlatform::Windows, "\"windows\""),
            (StoragePlatform::S3, "\"s3\""),
            (StoragePlatform::Adls, "\"adls\""),
            (StoragePlatform::Gcs, "\"gcs\""),
            (StoragePlatform::Hdfs, "\"hdfs\""),
            (StoragePlatform::JuiceFs, "\"juice_fs\""),
            (StoragePlatform::MaprFs, "\"mapr_fs\""),
            (StoragePlatform::CephFs, "\"ceph_fs\""),
            (StoragePlatform::SeaweedFs, "\"seaweed_fs\""),
            (StoragePlatform::Agnostic, "\"agnostic\""),
        ];
        for (variant, expected_json) in &variants {
            let serialized = serde_json::to_string(variant).unwrap();
            assert_eq!(&serialized, expected_json, "serialize {variant:?}");
            let deserialized: StoragePlatform = serde_json::from_str(expected_json).unwrap();
            assert_eq!(&deserialized, variant, "deserialize {expected_json}");
        }
    }

    #[test]
    fn serde_yaml_round_trip() {
        for variant in StoragePlatform::all_variants() {
            let yaml = serde_yaml::to_string(variant).unwrap();
            let parsed: StoragePlatform = serde_yaml::from_str(&yaml).unwrap();
            assert_eq!(parsed, *variant, "YAML round-trip failed for {variant:?}");
        }
    }

    #[test]
    fn capabilities_s3() {
        let s3 = StoragePlatform::S3;
        assert!(!s3.supports_atomic_move());
        assert!(s3.supports_native_notify());
        assert!(s3.supports_versioning());
        assert!(s3.supports_lifecycle_rules());
        assert_eq!(s3.path_separator(), "/");
        assert!(!s3.is_abstract());
    }

    #[test]
    fn capabilities_linux() {
        let linux = StoragePlatform::Linux;
        assert!(linux.supports_atomic_move());
        assert!(linux.supports_native_notify());
        assert!(!linux.supports_versioning());
        assert!(!linux.supports_lifecycle_rules());
        assert_eq!(linux.path_separator(), "/");
        assert!(!linux.is_abstract());
    }

    #[test]
    fn capabilities_windows() {
        let win = StoragePlatform::Windows;
        assert!(win.supports_atomic_move());
        assert!(win.supports_native_notify());
        assert!(!win.supports_versioning());
        assert!(!win.supports_lifecycle_rules());
        assert_eq!(win.path_separator(), "\\");
        assert!(!win.is_abstract());
    }

    #[test]
    fn capabilities_adls() {
        let adls = StoragePlatform::Adls;
        assert!(adls.supports_atomic_move());
        assert!(adls.supports_native_notify());
        assert!(adls.supports_versioning());
        assert!(adls.supports_lifecycle_rules());
    }

    #[test]
    fn capabilities_gcs() {
        let gcs = StoragePlatform::Gcs;
        assert!(!gcs.supports_atomic_move());
        assert!(gcs.supports_native_notify());
        assert!(gcs.supports_versioning());
        assert!(gcs.supports_lifecycle_rules());
    }

    #[test]
    fn capabilities_hdfs() {
        let hdfs = StoragePlatform::Hdfs;
        assert!(hdfs.supports_atomic_move());
        assert!(!hdfs.supports_native_notify());
        assert!(!hdfs.supports_versioning());
        assert!(!hdfs.supports_lifecycle_rules());
    }

    #[test]
    fn capabilities_juicefs() {
        let jfs = StoragePlatform::JuiceFs;
        assert!(jfs.supports_atomic_move());
        assert!(jfs.supports_native_notify());
        assert!(!jfs.supports_versioning());
        assert!(jfs.supports_lifecycle_rules());
    }

    #[test]
    fn capabilities_maprfs() {
        let mapr = StoragePlatform::MaprFs;
        assert!(mapr.supports_atomic_move());
        assert!(mapr.supports_native_notify());
        assert!(mapr.supports_versioning());
        assert!(!mapr.supports_lifecycle_rules());
    }

    #[test]
    fn capabilities_cephfs() {
        let ceph = StoragePlatform::CephFs;
        assert!(ceph.supports_atomic_move());
        assert!(ceph.supports_native_notify());
        assert!(ceph.supports_versioning());
        assert!(!ceph.supports_lifecycle_rules());
    }

    #[test]
    fn capabilities_seaweedfs() {
        let sw = StoragePlatform::SeaweedFs;
        assert!(!sw.supports_atomic_move());
        assert!(!sw.supports_native_notify());
        assert!(!sw.supports_versioning());
        assert!(sw.supports_lifecycle_rules());
    }

    #[test]
    fn agnostic_is_abstract() {
        assert!(StoragePlatform::Agnostic.is_abstract());
        for variant in StoragePlatform::concrete_variants() {
            assert!(!variant.is_abstract(), "{variant:?} should not be abstract");
        }
    }

    #[test]
    fn display_all_variants() {
        assert_eq!(StoragePlatform::Linux.to_string(), "linux");
        assert_eq!(StoragePlatform::Windows.to_string(), "windows");
        assert_eq!(StoragePlatform::S3.to_string(), "s3");
        assert_eq!(StoragePlatform::Adls.to_string(), "adls");
        assert_eq!(StoragePlatform::Gcs.to_string(), "gcs");
        assert_eq!(StoragePlatform::Hdfs.to_string(), "hdfs");
        assert_eq!(StoragePlatform::JuiceFs.to_string(), "juicefs");
        assert_eq!(StoragePlatform::MaprFs.to_string(), "maprfs");
        assert_eq!(StoragePlatform::CephFs.to_string(), "cephfs");
        assert_eq!(StoragePlatform::SeaweedFs.to_string(), "seaweedfs");
        assert_eq!(StoragePlatform::Agnostic.to_string(), "agnostic");
    }

    #[test]
    fn from_str_all_variants() {
        assert_eq!("linux".parse::<StoragePlatform>().unwrap(), StoragePlatform::Linux);
        assert_eq!("S3".parse::<StoragePlatform>().unwrap(), StoragePlatform::S3);
        assert_eq!("ADLS".parse::<StoragePlatform>().unwrap(), StoragePlatform::Adls);
        assert_eq!("GCS".parse::<StoragePlatform>().unwrap(), StoragePlatform::Gcs);
        assert_eq!("HdFs".parse::<StoragePlatform>().unwrap(), StoragePlatform::Hdfs);
        assert_eq!("agnostic".parse::<StoragePlatform>().unwrap(), StoragePlatform::Agnostic);
    }

    #[test]
    fn from_str_unknown_returns_error() {
        let result = "unknown".parse::<StoragePlatform>();
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Unknown storage platform"));
    }

    #[test]
    fn all_variants_has_correct_count() {
        assert_eq!(StoragePlatform::all_variants().len(), 11);
    }

    #[test]
    fn concrete_variants_excludes_agnostic() {
        let concrete = StoragePlatform::concrete_variants();
        assert_eq!(concrete.len(), 10);
        assert!(!concrete.contains(&StoragePlatform::Agnostic));
    }

    #[test]
    fn all_variants_have_non_empty_path_separator() {
        for variant in StoragePlatform::all_variants() {
            let sep = variant.path_separator();
            assert!(!sep.is_empty(), "{variant:?} has empty separator");
        }
    }

    #[test]
    fn display_round_trips_through_from_str() {
        for variant in StoragePlatform::all_variants() {
            let display = variant.to_string();
            let parsed: StoragePlatform = display.parse().unwrap();
            assert_eq!(parsed, *variant, "Display → FromStr round-trip failed for {variant:?}");
        }
    }
}