injectable-rs-graph 0.1.0

Compile-time dependency graph validation for the injectable-rs DI framework
Documentation
//! Graph node representation for the dependency graph.

/// A node in the dependency graph representing an injectable type.
///
/// Each node corresponds to a type annotated with `#[derive(Injectable)]`
/// and records its direct dependencies and scope.
///
/// # Construction
///
/// Nodes are created by the proc macro from constructor parameter analysis:
///
/// ```rust,ignore
/// // From:
/// #[injectable(ctor)]
/// pub async fn new(db: Inject<Database>, cache: Inject<Cache>) -> Self
///
/// // The macro generates:
/// GraphNode {
///     name: "UserService",
///     dependencies: &["Database", "Cache"],
///     scope: "singleton",
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GraphNode {
    /// The fully qualified type name of this injectable.
    pub name: &'static str,

    /// The type names of direct dependencies.
    pub dependencies: &'static [&'static str],

    /// The scope of this injectable ("singleton" or "transient").
    pub scope: &'static str,
}

impl GraphNode {
    /// Create a new graph node with default singleton scope.
    pub const fn new(name: &'static str, dependencies: &'static [&'static str]) -> Self {
        Self {
            name,
            dependencies,
            scope: "singleton",
        }
    }

    /// Create a graph node with no dependencies (singleton scope by default).
    pub const fn leaf(name: &'static str) -> Self {
        Self {
            name,
            dependencies: &[],
            scope: "singleton",
        }
    }

    /// Create a graph node with an explicit scope.
    pub const fn with_scope(
        name: &'static str,
        dependencies: &'static [&'static str],
        scope: &'static str,
    ) -> Self {
        Self {
            name,
            dependencies,
            scope,
        }
    }

    /// Create a leaf graph node with an explicit scope.
    pub const fn leaf_with_scope(name: &'static str, scope: &'static str) -> Self {
        Self {
            name,
            dependencies: &[],
            scope,
        }
    }

    /// Returns `true` if this node has no dependencies.
    pub fn is_leaf(&self) -> bool {
        self.dependencies.is_empty()
    }

    /// Returns the number of direct dependencies.
    pub fn dependency_count(&self) -> usize {
        self.dependencies.len()
    }

    /// Returns `true` if this node is in singleton scope.
    pub fn is_singleton(&self) -> bool {
        self.scope == "singleton"
    }

    /// Returns `true` if this node is in transient scope.
    pub fn is_transient(&self) -> bool {
        self.scope == "transient"
    }
}

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

    #[test]
    fn leaf_node() {
        let n = GraphNode::leaf("Database");
        assert_eq!(n.name, "Database");
        assert!(n.is_leaf());
        assert_eq!(n.dependency_count(), 0);
        assert!(n.is_singleton());
        assert!(!n.is_transient());
    }

    #[test]
    fn new_node_with_deps() {
        let n = GraphNode::new("UserService", &["Database", "Cache"]);
        assert_eq!(n.name, "UserService");
        assert!(!n.is_leaf());
        assert_eq!(n.dependency_count(), 2);
        assert!(n.is_singleton());
    }

    #[test]
    fn with_scope_transient() {
        let n = GraphNode::with_scope("Logger", &["Config"], "transient");
        assert!(n.is_transient());
        assert!(!n.is_singleton());
        assert_eq!(n.dependency_count(), 1);
    }

    #[test]
    fn leaf_with_scope_singleton() {
        let n = GraphNode::leaf_with_scope("Config", "singleton");
        assert!(n.is_leaf());
        assert!(n.is_singleton());
        assert!(!n.is_transient());
    }

    #[test]
    fn leaf_with_scope_transient() {
        let n = GraphNode::leaf_with_scope("Req", "transient");
        assert!(n.is_transient());
        assert!(!n.is_singleton());
    }

    #[test]
    fn derive_traits() {
        let a = GraphNode::leaf("A");
        let b = GraphNode::leaf("A");
        let c = GraphNode::leaf("B");
        assert_eq!(a, b);
        assert_ne!(a, c);
        let _ = format!("{a:?}");
    }
}