use super::Binding;
use crate::ir::NodeId;
use indexmap::{IndexMap, IndexSet};
use std::collections::BTreeMap;
pub struct DependencyGraph {
dependencies: IndexMap<String, IndexSet<NodeId>>,
node_bindings: IndexMap<NodeId, IndexSet<String>>,
prefix_index: BTreeMap<String, IndexSet<String>>,
registered_providers: IndexSet<String>,
unregistered_provider_refs: IndexSet<String>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
dependencies: IndexMap::new(),
node_bindings: IndexMap::new(),
prefix_index: BTreeMap::new(),
registered_providers: IndexSet::new(),
unregistered_provider_refs: IndexSet::new(),
}
}
pub fn add_dependency(
&mut self,
node_id: NodeId,
binding: &Binding,
module_scope: Option<&str>,
) {
use super::BindingSource;
let path = match &binding.source {
BindingSource::State => {
if let Some(scope) = module_scope {
format!("mod:{}:{}", scope, binding.full_path())
} else {
binding.full_path()
}
}
BindingSource::Item => return, BindingSource::DataSource(provider) => {
if !self.registered_providers.contains(provider.as_str()) {
self.unregistered_provider_refs.insert(provider.clone());
}
format!("ds:{}:{}", provider, binding.full_path())
}
};
self.dependencies
.entry(path.clone())
.or_default()
.insert(node_id);
self.node_bindings
.entry(node_id)
.or_default()
.insert(path.clone());
self.add_path_to_prefix_index(&path);
}
fn add_path_to_prefix_index(&mut self, path: &str) {
self.prefix_index
.entry(path.to_string())
.or_default()
.insert(path.to_string());
let mut current = path;
while let Some(dot_pos) = current.rfind('.') {
current = ¤t[..dot_pos];
self.prefix_index
.entry(current.to_string())
.or_default()
.insert(path.to_string());
}
}
pub fn remove_node(&mut self, node_id: NodeId) {
if let Some(paths) = self.node_bindings.shift_remove(&node_id) {
for path in paths {
if let Some(nodes) = self.dependencies.get_mut(&path) {
nodes.shift_remove(&node_id);
}
}
}
}
pub fn get_dependent_nodes(&self, path: &str) -> Option<&IndexSet<NodeId>> {
self.dependencies.get(path)
}
pub fn get_affected_nodes(&self, changed_path: &str) -> IndexSet<NodeId> {
let mut affected = IndexSet::new();
let mut affected_paths = IndexSet::new();
if self.dependencies.contains_key(changed_path) {
affected_paths.insert(changed_path.to_string());
}
let mut current = changed_path;
while let Some(dot_pos) = current.rfind('.') {
current = ¤t[..dot_pos];
if self.dependencies.contains_key(current) {
affected_paths.insert(current.to_string());
}
}
if let Some(child_paths) = self.prefix_index.get(changed_path) {
affected_paths.extend(child_paths.iter().cloned());
}
for path in affected_paths {
if let Some(nodes) = self.dependencies.get(&path) {
affected.extend(nodes.iter().copied());
}
}
affected
}
pub fn register_data_source_provider(&mut self, name: impl Into<String>) {
let name = name.into();
self.registered_providers.insert(name.clone());
self.unregistered_provider_refs.shift_remove(&name);
}
pub fn unregistered_provider_refs(&self) -> &IndexSet<String> {
&self.unregistered_provider_refs
}
pub fn get_data_source_affected_nodes(&self, provider: &str) -> IndexSet<NodeId> {
let prefix = format!("ds:{}:", provider);
let root = format!("ds:{}", provider);
let mut affected = IndexSet::new();
for (path, nodes) in &self.dependencies {
if path == &root || path.starts_with(&prefix) {
affected.extend(nodes.iter().copied());
}
}
affected
}
pub fn clear(&mut self) {
self.dependencies.clear();
self.node_bindings.clear();
self.prefix_index.clear();
self.unregistered_provider_refs.clear();
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prefix_index_basic() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let binding1 = Binding::state(vec!["user".to_string()]);
let binding2 = Binding::state(vec!["user".to_string(), "name".to_string()]);
let binding3 = Binding::state(vec!["user".to_string(), "email".to_string()]);
graph.add_dependency(node1, &binding1, None);
graph.add_dependency(node2, &binding2, None);
graph.add_dependency(node3, &binding3, None);
assert!(graph.prefix_index.contains_key("user"));
assert!(graph.prefix_index.contains_key("user.name"));
assert!(graph.prefix_index.contains_key("user.email"));
let user_paths = graph.prefix_index.get("user").unwrap();
assert_eq!(user_paths.len(), 3);
assert!(user_paths.contains("user"));
assert!(user_paths.contains("user.name"));
assert!(user_paths.contains("user.email"));
}
#[test]
fn test_get_affected_nodes_with_prefix_index() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]), None);
graph.add_dependency(
node2,
&Binding::state(vec!["user".to_string(), "name".to_string()]),
None,
);
graph.add_dependency(
node3,
&Binding::state(vec!["user".to_string(), "email".to_string()]),
None,
);
graph.add_dependency(node4, &Binding::state(vec!["posts".to_string()]), None);
let affected = graph.get_affected_nodes("user");
assert_eq!(affected.len(), 3);
assert!(affected.contains(&node1));
assert!(affected.contains(&node2));
assert!(affected.contains(&node3));
assert!(!affected.contains(&node4));
}
#[test]
fn test_get_affected_nodes_child_change() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]), None);
graph.add_dependency(
node2,
&Binding::state(vec!["user".to_string(), "name".to_string()]),
None,
);
let affected = graph.get_affected_nodes("user.name");
assert_eq!(affected.len(), 2);
assert!(affected.contains(&node1)); assert!(affected.contains(&node2)); }
#[test]
fn test_get_affected_nodes_exact_match_only() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
graph.add_dependency(
node1,
&Binding::state(vec!["user".to_string(), "name".to_string()]),
None,
);
let affected = graph.get_affected_nodes("user.email");
assert_eq!(affected.len(), 0);
}
#[test]
fn test_prefix_index_performance() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
for i in 0..100 {
let node_id = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let path = vec![
"user".to_string(),
"profile".to_string(),
"details".to_string(),
format!("field{}", i),
];
let binding = Binding::state(path);
graph.add_dependency(node_id, &binding, None);
}
let affected = graph.get_affected_nodes("user");
assert_eq!(affected.len(), 100);
let affected = graph.get_affected_nodes("user.profile");
assert_eq!(affected.len(), 100);
let affected = graph.get_affected_nodes("user.profile.details");
assert_eq!(affected.len(), 100);
}
#[test]
fn test_clear_clears_prefix_index() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
let node = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
graph.add_dependency(
node,
&Binding::state(vec!["user".to_string(), "name".to_string()]),
None,
);
assert!(!graph.prefix_index.is_empty());
graph.clear();
assert!(graph.prefix_index.is_empty());
assert!(graph.dependencies.is_empty());
assert!(graph.node_bindings.is_empty());
}
#[test]
fn test_get_data_source_affected_nodes() {
use super::Binding;
use crate::ir::Element;
use crate::reconcile::tree::InstanceTree;
let mut graph = DependencyGraph::new();
let mut tree = InstanceTree::new();
let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
graph.add_dependency(
node1,
&Binding::data_source("spacetime", vec!["messages".to_string()]),
None,
);
graph.add_dependency(
node2,
&Binding::data_source(
"spacetime",
vec!["users".to_string(), "name".to_string()],
),
None,
);
graph.add_dependency(
node3,
&Binding::data_source("firebase", vec!["docs".to_string()]),
None,
);
graph.add_dependency(node4, &Binding::state(vec!["count".to_string()]), None);
let affected = graph.get_data_source_affected_nodes("spacetime");
assert_eq!(affected.len(), 2);
assert!(affected.contains(&node1));
assert!(affected.contains(&node2));
assert!(!affected.contains(&node3)); assert!(!affected.contains(&node4));
let affected_old = graph.get_affected_nodes("ds:spacetime");
assert_eq!(
affected_old.len(),
0,
"get_affected_nodes should miss colon-separated children"
);
}
}