cipherstash-config 0.34.1-alpha.2

Configuration management for CipherStash libraries and products
Documentation
use bitflags::bitflags;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

bitflags! {
    /// Controls which array selectors are generated during JSON indexing.
    ///
    /// By default, no array selectors are generated (opt-in).
    /// Users can enable specific selectors or use `ALL` for current behavior.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
    pub struct ArrayIndexMode: u8 {
        /// No array indexing (default)
        const NONE     = 0b0000;
        /// Generate [@] selectors for "any item" queries
        const ITEM     = 0b0001;
        /// Generate [*] selectors for JSONPath wildcard queries
        const WILDCARD = 0b0010;
        /// Generate [n] selectors for positional queries
        const POSITION = 0b0100;
        /// Generate all selectors (current behavior)
        const ALL      = Self::ITEM.bits() | Self::WILDCARD.bits() | Self::POSITION.bits();
    }
}

impl ArrayIndexMode {
    /// Returns true if ITEM selector should be generated
    pub fn has_item(&self) -> bool {
        self.contains(Self::ITEM)
    }

    /// Returns true if WILDCARD selector should be generated
    pub fn has_wildcard(&self) -> bool {
        self.contains(Self::WILDCARD)
    }

    /// Returns true if POSITION selector should be generated
    pub fn has_position(&self) -> bool {
        self.contains(Self::POSITION)
    }
}

/// Helper struct for object-form serialization
#[derive(Serialize, Deserialize)]
struct ArrayIndexModeObject {
    #[serde(default)]
    item: bool,
    #[serde(default)]
    wildcard: bool,
    #[serde(default)]
    position: bool,
}

impl Serialize for ArrayIndexMode {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Emit presets for ALL and NONE, object form for partial combinations
        if *self == Self::ALL {
            serializer.serialize_str("all")
        } else if *self == Self::NONE {
            serializer.serialize_str("none")
        } else {
            let obj = ArrayIndexModeObject {
                item: self.has_item(),
                wildcard: self.has_wildcard(),
                position: self.has_position(),
            };
            obj.serialize(serializer)
        }
    }
}

impl<'de> Deserialize<'de> for ArrayIndexMode {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de::Error;

        // First try to deserialize as a string preset
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum ModeOrPreset {
            Preset(String),
            Object(ArrayIndexModeObject),
        }

        match ModeOrPreset::deserialize(deserializer)? {
            ModeOrPreset::Preset(s) => match s.to_lowercase().as_str() {
                "all" => Ok(Self::ALL),
                "none" => Ok(Self::NONE),
                other => Err(D::Error::custom(format!(
                    "unknown preset '{}', expected 'all' or 'none'",
                    other
                ))),
            },
            ModeOrPreset::Object(obj) => {
                let mut mode = Self::NONE;
                if obj.item {
                    mode |= Self::ITEM;
                }
                if obj.wildcard {
                    mode |= Self::WILDCARD;
                }
                if obj.position {
                    mode |= Self::POSITION;
                }
                Ok(mode)
            }
        }
    }
}

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

    #[test]
    fn test_default_is_none() {
        let mode = ArrayIndexMode::default();
        assert_eq!(mode, ArrayIndexMode::NONE);
        assert!(!mode.has_item());
        assert!(!mode.has_wildcard());
        assert!(!mode.has_position());
    }

    #[test]
    fn test_all_contains_all_flags() {
        let mode = ArrayIndexMode::ALL;
        assert!(mode.has_item());
        assert!(mode.has_wildcard());
        assert!(mode.has_position());
    }

    #[test]
    fn test_individual_flags() {
        let item = ArrayIndexMode::ITEM;
        assert!(item.has_item());
        assert!(!item.has_wildcard());
        assert!(!item.has_position());

        let wildcard = ArrayIndexMode::WILDCARD;
        assert!(!wildcard.has_item());
        assert!(wildcard.has_wildcard());
        assert!(!wildcard.has_position());

        let position = ArrayIndexMode::POSITION;
        assert!(!position.has_item());
        assert!(!position.has_wildcard());
        assert!(position.has_position());
    }

    #[test]
    fn test_flag_combinations() {
        let combo = ArrayIndexMode::ITEM | ArrayIndexMode::WILDCARD;
        assert!(combo.has_item());
        assert!(combo.has_wildcard());
        assert!(!combo.has_position());
    }

    #[test]
    fn test_serialize_all_as_preset() {
        let mode = ArrayIndexMode::ALL;
        let json = serde_json::to_string(&mode).unwrap();
        assert_eq!(json, r#""all""#);
    }

    #[test]
    fn test_serialize_none_as_preset() {
        let mode = ArrayIndexMode::NONE;
        let json = serde_json::to_string(&mode).unwrap();
        assert_eq!(json, r#""none""#);
    }

    #[test]
    fn test_serialize_partial_as_object() {
        let mode = ArrayIndexMode::ITEM | ArrayIndexMode::WILDCARD;
        let json = serde_json::to_string(&mode).unwrap();
        // Partial combinations serialize as object
        assert!(json.contains("\"item\":true"));
        assert!(json.contains("\"wildcard\":true"));
        assert!(json.contains("\"position\":false"));
    }

    #[test]
    fn test_round_trip_none() {
        let original = ArrayIndexMode::NONE;
        let json = serde_json::to_string(&original).unwrap();
        let parsed: ArrayIndexMode = serde_json::from_str(&json).unwrap();
        assert_eq!(original, parsed);
    }

    #[test]
    fn test_round_trip_all() {
        let original = ArrayIndexMode::ALL;
        let json = serde_json::to_string(&original).unwrap();
        let parsed: ArrayIndexMode = serde_json::from_str(&json).unwrap();
        assert_eq!(original, parsed);
    }

    #[test]
    fn test_round_trip_partial() {
        let original = ArrayIndexMode::ITEM | ArrayIndexMode::WILDCARD;
        let json = serde_json::to_string(&original).unwrap();
        let parsed: ArrayIndexMode = serde_json::from_str(&json).unwrap();
        assert_eq!(original, parsed);
    }

    #[test]
    fn test_deserialize_object_form() {
        let json = r#"{"item":true,"wildcard":true,"position":false}"#;
        let mode: ArrayIndexMode = serde_json::from_str(json).unwrap();
        assert!(mode.has_item());
        assert!(mode.has_wildcard());
        assert!(!mode.has_position());
    }

    #[test]
    fn test_deserialize_preset_all() {
        let json = r#""all""#;
        let mode: ArrayIndexMode = serde_json::from_str(json).unwrap();
        assert_eq!(mode, ArrayIndexMode::ALL);
    }

    #[test]
    fn test_deserialize_preset_none() {
        let json = r#""none""#;
        let mode: ArrayIndexMode = serde_json::from_str(json).unwrap();
        assert_eq!(mode, ArrayIndexMode::NONE);
    }

    #[test]
    fn test_deserialize_preset_case_insensitive() {
        let cases = [
            ("\"ALL\"", ArrayIndexMode::ALL),
            ("\"None\"", ArrayIndexMode::NONE),
            ("\"NONE\"", ArrayIndexMode::NONE),
            ("\"All\"", ArrayIndexMode::ALL),
        ];
        for (json, expected) in cases {
            let mode: ArrayIndexMode = serde_json::from_str(json).unwrap();
            assert_eq!(mode, expected, "Failed for input: {}", json);
        }
    }

    #[test]
    fn test_deserialize_empty_object_is_none() {
        let json = r#"{}"#;
        let mode: ArrayIndexMode = serde_json::from_str(json).unwrap();
        assert_eq!(mode, ArrayIndexMode::NONE);
    }

    #[test]
    fn test_deserialize_partial_object() {
        // Missing fields default to false
        let json = r#"{"wildcard":true}"#;
        let mode: ArrayIndexMode = serde_json::from_str(json).unwrap();
        assert!(!mode.has_item());
        assert!(mode.has_wildcard());
        assert!(!mode.has_position());
    }

    #[test]
    fn test_deserialize_invalid_preset_returns_error() {
        let json = r#""invalid""#;
        let result: Result<ArrayIndexMode, _> = serde_json::from_str(json);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("unknown preset"),
            "Error should mention 'unknown preset': {}",
            err
        );
    }
}