cipherstash-client 0.34.1-alpha.1

The official CipherStash SDK
Documentation
use cipherstash_config::column::ArrayIndexMode;
use serde_json::Value;

/// A path and the `Node` value at that path.
#[derive(Debug, PartialEq, Eq)]
pub(super) struct PathValue<'a>(pub(super) Vec<PathSegment<'a>>, pub(super) &'a Value);

pub fn flatmap_json<'a, F>(value: &'a Value, mode: ArrayIndexMode, mapper: F) -> Vec<PathValue<'a>>
where
    F: Fn(&[PathSegment<'a>], &'a Value) -> PathValue<'a>,
{
    let mut out: Vec<PathValue<'a>> = vec![];
    flatmap_json_internal(value, &mut vec![PathSegment::Root], &mapper, &mut out, mode);
    out
}

///
/// Map the json to a Vec of Values and Paths
///
/// Arrays are complicated
/// Arrays are indexed in several different ways.
///
/// Assuming data as:
///   `{ "a": [1,2 3] }`
///
/// The array name selector as the json literal value
/// On decryption, the data is the json array
///   `$.a = [1,2 3]`
///
/// Elements are indexed with the array index position
///   ```no-compile
///     $.a[0] = 1
///     $.a[1] = 2
///     $.a[2] = 3
///   ```
///
/// Elements are indexed with the wildcard
///   ```no-compile
///     $.a[*] = 1
///     $.a[*] = 2
///     $.a[*] = 3
///   ```
/// Elements are indexed with an "item" array selector
///   ```no-compile
///     $.a[@] = 1
///     $.a[@] = 2
///     $.a[@] = 3
///   ```
///
fn flatmap_json_internal<'a, F>(
    value: &'a Value,
    path: &mut Vec<PathSegment<'a>>,
    mapper: &F,
    out: &mut Vec<PathValue<'a>>,
    mode: ArrayIndexMode,
) where
    F: Fn(&[PathSegment<'a>], &'a Value) -> PathValue<'a>,
{
    match value {
        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
            out.push(mapper(&*path, value))
        }
        Value::Array(values) => {
            out.push(mapper(&*path, value));

            // Each element of the array is encoded based on the mode
            for (idx, value) in values.iter().enumerate() {
                // array [@] selector
                if mode.has_item() {
                    path.push(PathSegment::ArrayItem);
                    flatmap_json_internal(value, path, mapper, out, mode);
                    path.pop();
                }

                // array position [n] selector
                if mode.has_position() {
                    path.push(PathSegment::ArrayPositionItem(idx));
                    flatmap_json_internal(value, path, mapper, out, mode);
                    path.pop();
                }

                // array wildcard [*] selector
                if mode.has_wildcard() {
                    path.push(PathSegment::ArrayWildcardItem);
                    flatmap_json_internal(value, path, mapper, out, mode);
                    path.pop();
                }
            }
        }
        Value::Object(object) => {
            out.push(mapper(&*path, value));
            for (key, value) in object.iter() {
                path.push(PathSegment::ObjectItem(key));
                flatmap_json_internal(value, path, mapper, out, mode);
                path.pop();
            }
        }
    };
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PathSegment<'a> {
    /// The root node a JSON is not contained within a map or an array
    Root,

    ArrayWildcardItem,

    ArrayPositionItem(usize),

    ArrayItem,

    /// A property of a JSON object. The property is used to generate hashes used in containment queries so it is
    /// retained.
    ObjectItem(&'a str),
}

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

    #[test]
    fn test_flatmap_with_mode_none() {
        let json = json!({"ary": ["a", "b", "c"]});
        let mapped = flatmap_json(&json, ArrayIndexMode::NONE, |path, value| {
            PathValue(path.to_vec(), value)
        });

        // With NONE: root(1) + ary(1) = 2 entries
        assert_eq!(mapped.len(), 2);
    }

    #[test]
    fn test_flatmap_with_mode_wildcard_only() {
        let json = json!({"ary": ["a", "b", "c"]});
        let mapped = flatmap_json(&json, ArrayIndexMode::WILDCARD, |path, value| {
            PathValue(path.to_vec(), value)
        });

        // With WILDCARD: root(1) + ary(1) + 3 wildcard = 5 entries
        assert_eq!(mapped.len(), 5);
    }

    #[test]
    fn test_flatmap_with_mode_all() {
        let json = json!({"ary": ["a", "b", "c"]});
        let mapped = flatmap_json(&json, ArrayIndexMode::ALL, |path, value| {
            PathValue(path.to_vec(), value)
        });

        // With ALL: root(1) + ary(1) + 3*(item + position + wildcard) = 2 + 9 = 11 entries
        assert_eq!(mapped.len(), 11);
    }

    #[test]
    fn test_flatmap_with_mode_item_and_wildcard() {
        let json = json!({"ary": ["a", "b", "c"]});
        let mode = ArrayIndexMode::ITEM | ArrayIndexMode::WILDCARD;
        let mapped = flatmap_json(&json, mode, |path, value| PathValue(path.to_vec(), value));

        // With ITEM|WILDCARD: root(1) + ary(1) + 3*(item + wildcard) = 2 + 6 = 8 entries
        assert_eq!(mapped.len(), 8);
    }

    #[test]
    fn test_flatmap_with_mode_item_only() {
        let json = json!({"ary": ["a", "b", "c"]});
        let mapped = flatmap_json(&json, ArrayIndexMode::ITEM, |path, value| {
            PathValue(path.to_vec(), value)
        });
        // With ITEM mode: root(1) + ary(1) + 3 item entries = 5
        assert_eq!(mapped.len(), 5);
    }

    #[test]
    fn test_flatmap_with_mode_position_only() {
        let json = json!({"ary": ["a", "b", "c"]});
        let mapped = flatmap_json(&json, ArrayIndexMode::POSITION, |path, value| {
            PathValue(path.to_vec(), value)
        });
        // With POSITION mode: root(1) + ary(1) + 3 position entries = 5
        assert_eq!(mapped.len(), 5);
    }

    #[test]
    #[ignore] // Debug test - prints output without assertions
    fn map_json() {
        let json = json!({
            "ary": [
                "a", "b", "c"
            ]
        });

        let mapped = super::flatmap_json(&json, ArrayIndexMode::ALL, |path, value| {
            PathValue(path.to_vec(), value)
        });

        println!("mapped: {mapped:?}")
    }
}