graph_d 1.3.2

A native graph database implementation in Rust with built-in JSON support and SQLite-like simplicity
Documentation
//! Node implementation for the graph database.
//!
//! This module provides the [`Node`] struct, which represents vertices in the graph.
//! Nodes can store arbitrary JSON properties and are uniquely identified by their ID.

use crate::graph::Id;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;

/// A node (vertex) in the graph database.
///
/// Nodes are the fundamental entities in a graph, representing discrete objects
/// or entities such as people, places, things, or concepts. Each node has a unique
/// identifier and can store arbitrary JSON properties.
///
/// # Properties
///
/// Node properties are stored as JSON values, providing flexibility to store:
/// - Strings, numbers, booleans
/// - Arrays and nested objects
/// - Null values
///
/// # Serialization
///
/// Nodes implement [`Serialize`] and [`Deserialize`] for persistence and
/// network transmission.
///
/// # Example
///
/// ```rust
/// use graph_d::Node;
/// use serde_json::json;
/// use std::collections::HashMap;
///
/// let mut properties = HashMap::new();
/// properties.insert("name".to_string(), json!("Alice"));
/// properties.insert("age".to_string(), json!(30));
/// properties.insert("interests".to_string(), json!(["reading", "hiking"]));
///
/// let node = Node::new(1, properties);
/// assert_eq!(node.id, 1);
/// assert_eq!(node.get_property("name"), Some(&json!("Alice")));
/// ```
///
/// # Security Note (S3 Compliance)
///
/// The `Debug` implementation for `Node` intentionally omits property VALUES
/// to prevent accidental leakage of sensitive data (passwords, API keys, PII)
/// in logs, error messages, or debug output. Only property KEYS are shown.
///
/// To access property values, use the explicit accessor methods like
/// [`get_property`](Node::get_property).
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct Node {
    /// Unique identifier for the node within the graph
    pub id: Id,
    /// JSON properties associated with the node
    pub properties: HashMap<String, Value>,
}

/// Custom Debug implementation that redacts property values for security (S3).
///
/// Shows property keys but never property values to prevent sensitive data leakage.
impl fmt::Debug for Node {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let keys: Vec<&String> = self.properties.keys().collect();
        f.debug_struct("Node")
            .field("id", &self.id)
            .field("properties", &format!("[keys: {keys:?}]"))
            .finish()
    }
}

impl Node {
    /// Creates a new node with the specified ID and properties.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for the node
    /// * `properties` - HashMap of property names to JSON values
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Node;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("name".to_string(), json!("Bob"));
    /// props.insert("active".to_string(), json!(true));
    ///
    /// let node = Node::new(42, props);
    /// assert_eq!(node.id, 42);
    /// ```
    pub fn new(id: Id, properties: HashMap<String, Value>) -> Self {
        Node { id, properties }
    }

    /// Retrieves a property value by its key.
    ///
    /// # Arguments
    ///
    /// * `key` - The property name to look up
    ///
    /// # Returns
    ///
    /// Returns `Some(&Value)` if the property exists, `None` otherwise.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Node;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("name".to_string(), json!("Alice"));
    /// let node = Node::new(1, props);
    ///
    /// assert_eq!(node.get_property("name"), Some(&json!("Alice")));
    /// assert_eq!(node.get_property("missing"), None);
    /// ```
    pub fn get_property(&self, key: &str) -> Option<&Value> {
        self.properties.get(key)
    }

    /// Sets or updates a property value.
    ///
    /// If the property already exists, its value will be replaced.
    /// If it doesn't exist, a new property will be created.
    ///
    /// # Arguments
    ///
    /// * `key` - Property name
    /// * `value` - JSON value to store
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Node;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut node = Node::new(1, HashMap::new());
    /// node.set_property("name".to_string(), json!("Charlie"));
    /// node.set_property("scores".to_string(), json!([95, 87, 92]));
    ///
    /// assert_eq!(node.get_property("name"), Some(&json!("Charlie")));
    /// ```
    pub fn set_property(&mut self, key: String, value: Value) {
        self.properties.insert(key, value);
    }

    /// Removes a property and returns its value.
    ///
    /// # Arguments
    ///
    /// * `key` - Name of the property to remove
    ///
    /// # Returns
    ///
    /// Returns the removed value if the property existed, `None` otherwise.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Node;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("temp".to_string(), json!("delete_me"));
    /// let mut node = Node::new(1, props);
    ///
    /// let removed = node.remove_property("temp");
    /// assert_eq!(removed, Some(json!("delete_me")));
    /// assert!(!node.has_property("temp"));
    /// ```
    pub fn remove_property(&mut self, key: &str) -> Option<Value> {
        self.properties.remove(key)
    }

    /// Checks whether the node has a property with the given key.
    ///
    /// # Arguments
    ///
    /// * `key` - Property name to check
    ///
    /// # Returns
    ///
    /// Returns `true` if the property exists, `false` otherwise.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Node;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("name".to_string(), json!("Dave"));
    /// let node = Node::new(1, props);
    ///
    /// assert!(node.has_property("name"));
    /// assert!(!node.has_property("age"));
    /// ```
    pub fn has_property(&self, key: &str) -> bool {
        self.properties.contains_key(key)
    }

    /// Returns all property keys for this node.
    ///
    /// # Returns
    ///
    /// A vector containing references to all property names.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Node;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("name".to_string(), json!("Eve"));
    /// props.insert("age".to_string(), json!(25));
    /// let node = Node::new(1, props);
    ///
    /// let keys = node.property_keys();
    /// assert_eq!(keys.len(), 2);
    /// assert!(keys.contains(&&"name".to_string()));
    /// assert!(keys.contains(&&"age".to_string()));
    /// ```
    pub fn property_keys(&self) -> Vec<&String> {
        self.properties.keys().collect()
    }
}

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

    #[test]
    fn test_node_creation() {
        let properties = [
            ("name".to_string(), json!("Alice")),
            ("age".to_string(), json!(30)),
        ]
        .into();

        let node = Node::new(1, properties);
        assert_eq!(node.id, 1);
        assert_eq!(node.get_property("name"), Some(&json!("Alice")));
        assert_eq!(node.get_property("age"), Some(&json!(30)));
    }

    #[test]
    fn test_property_operations() {
        let mut node = Node::new(1, HashMap::new());

        // Set property
        node.set_property("name".to_string(), json!("Bob"));
        assert_eq!(node.get_property("name"), Some(&json!("Bob")));
        assert!(node.has_property("name"));

        // Remove property
        let removed = node.remove_property("name");
        assert_eq!(removed, Some(json!("Bob")));
        assert!(!node.has_property("name"));
    }
}