graph_d 1.3.2

A native graph database implementation in Rust with built-in JSON support and SQLite-like simplicity
Documentation
//! Relationship implementation for the graph database.
//!
//! This module provides the [`Relationship`] struct, which represents directed edges
//! in the graph. Relationships connect nodes and can store arbitrary JSON properties.

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

/// A directed relationship (edge) in the graph database.
///
/// Relationships represent connections between nodes in the graph. They are always
/// directed, having a source node (`from_id`) and a target node (`to_id`). Each
/// relationship has a type/label and can store arbitrary JSON properties.
///
/// # Relationship Types
///
/// Relationship types are string labels that categorize the nature of the connection:
/// - Social: "KNOWS", "FOLLOWS", "FRIEND_OF"
/// - Hierarchical: "OWNS", "MANAGES", "PARENT_OF"
/// - Temporal: "HAPPENED_AFTER", "CREATED_ON"
/// - Spatial: "LOCATED_IN", "NEAR"
///
/// # Properties
///
/// Like nodes, relationships can store arbitrary JSON properties such as:
/// - Weights or scores (e.g., relationship strength)
/// - Timestamps (e.g., when the relationship was established)
/// - Metadata (e.g., confidence levels, sources)
///
/// # Example
///
/// ```rust
/// use graph_d::Relationship;
/// use serde_json::json;
/// use std::collections::HashMap;
///
/// let mut properties = HashMap::new();
/// properties.insert("since".to_string(), json!("2020-01-01"));
/// properties.insert("strength".to_string(), json!(0.8));
///
/// let relationship = Relationship::new(
///     1,    // relationship ID
///     10,   // from node ID
///     20,   // to node ID  
///     "KNOWS".to_string(),
///     properties
/// );
///
/// assert_eq!(relationship.from_id, 10);
/// assert_eq!(relationship.to_id, 20);
/// assert_eq!(relationship.rel_type, "KNOWS");
/// ```
///
/// # Security Note (S3 Compliance)
///
/// The `Debug` implementation for `Relationship` 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`](Relationship::get_property).
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct Relationship {
    /// Unique identifier for the relationship
    pub id: Id,
    /// ID of the source node (relationship origin)
    pub from_id: Id,
    /// ID of the target node (relationship destination)
    pub to_id: Id,
    /// Type/label describing the nature of the relationship
    pub rel_type: String,
    /// JSON properties associated with the relationship
    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 Relationship {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let keys: Vec<&String> = self.properties.keys().collect();
        f.debug_struct("Relationship")
            .field("id", &self.id)
            .field("from_id", &self.from_id)
            .field("to_id", &self.to_id)
            .field("rel_type", &self.rel_type)
            .field("properties", &format!("[keys: {keys:?}]"))
            .finish()
    }
}

impl Relationship {
    /// Creates a new relationship with the specified parameters.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this relationship
    /// * `from_id` - ID of the source node
    /// * `to_id` - ID of the target node
    /// * `rel_type` - Type/label for the relationship
    /// * `properties` - HashMap of property names to JSON values
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Relationship;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("weight".to_string(), json!(0.75));
    /// props.insert("verified".to_string(), json!(true));
    ///
    /// let rel = Relationship::new(
    ///     1,
    ///     100,  // Alice's ID
    ///     200,  // Bob's ID
    ///     "FOLLOWS".to_string(),
    ///     props
    /// );
    ///
    /// assert_eq!(rel.id, 1);
    /// assert_eq!(rel.rel_type, "FOLLOWS");
    /// ```
    pub fn new(
        id: Id,
        from_id: Id,
        to_id: Id,
        rel_type: String,
        properties: HashMap<String, Value>,
    ) -> Self {
        Relationship {
            id,
            from_id,
            to_id,
            rel_type,
            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::Relationship;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("weight".to_string(), json!(0.8));
    /// let rel = Relationship::new(1, 10, 20, "KNOWS".to_string(), props);
    ///
    /// assert_eq!(rel.get_property("weight"), Some(&json!(0.8)));
    /// assert_eq!(rel.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::Relationship;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut rel = Relationship::new(1, 10, 20, "KNOWS".to_string(), HashMap::new());
    /// rel.set_property("strength".to_string(), json!(0.9));
    /// rel.set_property("tags".to_string(), json!(["friend", "colleague"]));
    ///
    /// assert_eq!(rel.get_property("strength"), Some(&json!(0.9)));
    /// ```
    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::Relationship;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("temp".to_string(), json!("remove_me"));
    /// let mut rel = Relationship::new(1, 10, 20, "TEST".to_string(), props);
    ///
    /// let removed = rel.remove_property("temp");
    /// assert_eq!(removed, Some(json!("remove_me")));
    /// assert!(!rel.has_property("temp"));
    /// ```
    pub fn remove_property(&mut self, key: &str) -> Option<Value> {
        self.properties.remove(key)
    }

    /// Checks whether the relationship 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::Relationship;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("weight".to_string(), json!(0.5));
    /// let rel = Relationship::new(1, 10, 20, "LIKES".to_string(), props);
    ///
    /// assert!(rel.has_property("weight"));
    /// assert!(!rel.has_property("color"));
    /// ```
    pub fn has_property(&self, key: &str) -> bool {
        self.properties.contains_key(key)
    }

    /// Returns all property keys for this relationship.
    ///
    /// # Returns
    ///
    /// A vector containing references to all property names.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Relationship;
    /// use serde_json::json;
    /// use std::collections::HashMap;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("weight".to_string(), json!(0.8));
    /// props.insert("since".to_string(), json!("2023-01-01"));
    /// let rel = Relationship::new(1, 10, 20, "KNOWS".to_string(), props);
    ///
    /// let keys = rel.property_keys();
    /// assert_eq!(keys.len(), 2);
    /// assert!(keys.contains(&&"weight".to_string()));
    /// assert!(keys.contains(&&"since".to_string()));
    /// ```
    pub fn property_keys(&self) -> Vec<&String> {
        self.properties.keys().collect()
    }

    /// Checks if this relationship connects the given nodes in either direction.
    ///
    /// This method returns `true` if the relationship connects the two nodes,
    /// regardless of direction. Useful for undirected relationship semantics.
    ///
    /// # Arguments
    ///
    /// * `node1_id` - ID of the first node
    /// * `node2_id` - ID of the second node
    ///
    /// # Returns
    ///
    /// Returns `true` if the relationship connects these nodes in either direction.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Relationship;
    /// use std::collections::HashMap;
    ///
    /// let rel = Relationship::new(1, 10, 20, "KNOWS".to_string(), HashMap::new());
    ///
    /// assert!(rel.connects(10, 20));  // Forward direction
    /// assert!(rel.connects(20, 10));  // Reverse direction
    /// assert!(!rel.connects(10, 30)); // Different nodes
    /// ```
    pub fn connects(&self, node1_id: Id, node2_id: Id) -> bool {
        (self.from_id == node1_id && self.to_id == node2_id)
            || (self.from_id == node2_id && self.to_id == node1_id)
    }

    /// Checks if this relationship is outgoing from the given node.
    ///
    /// # Arguments
    ///
    /// * `node_id` - ID of the node to check
    ///
    /// # Returns
    ///
    /// Returns `true` if this relationship originates from the given node.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Relationship;
    /// use std::collections::HashMap;
    ///
    /// let rel = Relationship::new(1, 10, 20, "FOLLOWS".to_string(), HashMap::new());
    ///
    /// assert!(rel.is_outgoing_from(10));   // Node 10 -> Node 20
    /// assert!(!rel.is_outgoing_from(20));  // Not from node 20
    /// ```
    pub fn is_outgoing_from(&self, node_id: Id) -> bool {
        self.from_id == node_id
    }

    /// Checks if this relationship is incoming to the given node.
    ///
    /// # Arguments
    ///
    /// * `node_id` - ID of the node to check
    ///
    /// # Returns
    ///
    /// Returns `true` if this relationship targets the given node.
    ///
    /// # Example
    ///
    /// ```rust
    /// use graph_d::Relationship;
    /// use std::collections::HashMap;
    ///
    /// let rel = Relationship::new(1, 10, 20, "FOLLOWS".to_string(), HashMap::new());
    ///
    /// assert!(rel.is_incoming_to(20));    // Node 10 -> Node 20
    /// assert!(!rel.is_incoming_to(10));   // Not to node 10
    /// ```
    pub fn is_incoming_to(&self, node_id: Id) -> bool {
        self.to_id == node_id
    }
}

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

    #[test]
    fn test_relationship_creation() {
        let properties = [("weight".to_string(), json!(0.8))].into();

        let rel = Relationship::new(1, 10, 20, "KNOWS".to_string(), properties);
        assert_eq!(rel.id, 1);
        assert_eq!(rel.from_id, 10);
        assert_eq!(rel.to_id, 20);
        assert_eq!(rel.rel_type, "KNOWS");
        assert_eq!(rel.get_property("weight"), Some(&json!(0.8)));
    }

    #[test]
    fn test_relationship_direction() {
        let rel = Relationship::new(1, 10, 20, "KNOWS".to_string(), HashMap::new());

        assert!(rel.connects(10, 20));
        assert!(rel.connects(20, 10));
        assert!(!rel.connects(10, 30));

        assert!(rel.is_outgoing_from(10));
        assert!(!rel.is_outgoing_from(20));

        assert!(rel.is_incoming_to(20));
        assert!(!rel.is_incoming_to(10));
    }
}