Compare commits
3 Commits
fe372b96fd
...
780bb4e372
Author | SHA1 | Date | |
---|---|---|---|
780bb4e372 | |||
0c369864d0 | |||
eec0dab6f0 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
result/
|
result/
|
||||||
result
|
result
|
||||||
target/
|
target/
|
||||||
|
*.avanterules
|
||||||
|
@ -21,7 +21,7 @@ there is one, and a list of any dependent resources.
|
|||||||
{
|
{
|
||||||
"objectId": <merkle-hash>,
|
"objectId": <merkle-hash>,
|
||||||
"content_address": <content-hash>,
|
"content_address": <content-hash>,
|
||||||
"path": "/path/name0",
|
"name": "/path/name0",
|
||||||
"dependents": [
|
"dependents": [
|
||||||
{
|
{
|
||||||
"path": "path/name1",
|
"path": "path/name1",
|
||||||
|
@ -17,7 +17,7 @@ async fn ref_path(refs: Arc<HashMap<String, Arc<Reference>>>, Path(path): Path<S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn object_path(objects: Arc<HashMap<String, String>>, Path(addr): Path<String>) -> String {
|
async fn object_path(objects: Arc<HashMap<String, Vec<u8>>>, Path(addr): Path<String>) -> Vec<u8> {
|
||||||
dbg!(&addr);
|
dbg!(&addr);
|
||||||
match objects.get(&addr) {
|
match objects.get(&addr) {
|
||||||
Some(o) => o.clone(),
|
Some(o) => o.clone(),
|
||||||
|
@ -21,7 +21,7 @@ async fn get_client_js() -> impl IntoResponse {
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
enum ServerMsg {
|
enum ServerMsg {
|
||||||
Reference(Reference),
|
Reference(Reference),
|
||||||
Object(String),
|
Object(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -4,88 +4,158 @@ use blake2::{Blake2b512, Digest};
|
|||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A `Reference` represents a node in a content-addressable graph structure.
|
||||||
|
///
|
||||||
|
/// Each reference contains:
|
||||||
|
/// - An id that uniquely identifies the referenced object
|
||||||
|
/// - A content address hash that represents the content
|
||||||
|
/// - A name that provides a human-readable name for the reference
|
||||||
|
/// - A list of dependent references that this reference depends on
|
||||||
|
///
|
||||||
|
/// References form a directed acyclic graph where each node can have multiple dependents.
|
||||||
|
/// When the content of a reference changes, the object id of all parent references
|
||||||
|
/// are automatically recalculated to maintain content addressability throughout the graph.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Reference {
|
pub struct Reference {
|
||||||
pub object_id: String,
|
pub id: String,
|
||||||
pub content_address: String,
|
pub content_address: Option<String>,
|
||||||
pub path: String,
|
pub name: String,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub dependents: Vec<Arc<Reference>>,
|
pub dependents: Vec<Arc<Reference>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reference {
|
impl Reference {
|
||||||
pub fn new(object_id: String, content_address: String, path: String) -> Self {
|
/// Creates a new Reference with the specified object ID, content address, and path.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// * `content_address` - The content-based hash address of the referenced object
|
||||||
|
/// * `name` - The name of the reference
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A new Reference instance with no dependents
|
||||||
|
pub fn new(content_address: Option<String>, name: String) -> Self {
|
||||||
|
// Calculate the reference_id from the content_address and path
|
||||||
|
let hasher = Self::initial_hash(&content_address, &name);
|
||||||
|
let calculated_id = format!("{:x}", hasher.finalize());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
object_id,
|
id: calculated_id,
|
||||||
content_address,
|
content_address,
|
||||||
path,
|
name,
|
||||||
dependents: Vec::new(),
|
dependents: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_dep(mut self, dep: Arc<Reference>) -> Self {
|
/// Adds a dependent reference to this reference.
|
||||||
self.dependents.push(dep);
|
///
|
||||||
self
|
/// This establishes a parent-child relationship where this reference depends on
|
||||||
|
/// the provided reference. When a dependent reference changes, the parent's
|
||||||
|
/// id will be recalculated.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// * `dep` - An Arc-wrapped Reference to add as a dependent
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The modified Reference with the new dependent added
|
||||||
|
pub fn add_dep(&self, dep: Arc<Reference>) -> Self {
|
||||||
|
let mut cloned = self.clone();
|
||||||
|
cloned.dependents.push(dep);
|
||||||
|
// Recalculate the ID based on dependents, content_address, and path
|
||||||
|
let mut hasher = Self::initial_hash(&self.content_address, &self.name);
|
||||||
|
for dependent in &self.dependents {
|
||||||
|
hasher.update(&dependent.id);
|
||||||
|
}
|
||||||
|
cloned.id = format!("{:x}", hasher.finalize());
|
||||||
|
cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Converts this Reference into an Arc-wrapped Reference for shared ownership.
|
||||||
|
///
|
||||||
|
/// This is useful when a Reference needs to be shared across multiple owners
|
||||||
|
/// in the graph structure.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An Arc-wrapped version of this Reference
|
||||||
pub fn to_arc(self) -> Arc<Self> {
|
pub fn to_arc(self) -> Arc<Self> {
|
||||||
Arc::new(self)
|
Arc::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines if this Reference is a leaf node (has no dependents).
|
||||||
|
///
|
||||||
|
/// Leaf nodes directly represent content, while non-leaf nodes derive their
|
||||||
|
/// content address from their dependents.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `true` if this Reference has no dependents, `false` otherwise
|
||||||
pub fn is_leaf(&self) -> bool {
|
pub fn is_leaf(&self) -> bool {
|
||||||
return self.dependents.is_empty();
|
return self.dependents.is_empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates a content address hash based on dependent references
|
fn initial_hash(content_address: &Option<String>, path: &String) -> Blake2b512 {
|
||||||
pub fn calculate_content_address(&self) -> String {
|
|
||||||
if self.is_leaf() {
|
|
||||||
return self.content_address.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hasher = Blake2b512::new();
|
let mut hasher = Blake2b512::new();
|
||||||
for dep in &self.dependents {
|
if let Some(content_address) = content_address {
|
||||||
hasher.update(&dep.content_address);
|
hasher.update(content_address);
|
||||||
}
|
}
|
||||||
format!("{:x}", hasher.finalize())
|
hasher.update(path);
|
||||||
|
hasher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Graph {
|
pub struct Graph {
|
||||||
pub root: Arc<Reference>,
|
pub root: Arc<Reference>,
|
||||||
pub refs: Arc<HashMap<String, Arc<Reference>>>,
|
pub refs: Arc<HashMap<String, Arc<Reference>>>,
|
||||||
pub objects: Arc<HashMap<String, String>>,
|
pub objects: Arc<HashMap<String, Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_object() -> (String, Vec<u8>) {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let random_size = rng.random_range(50..=4096);
|
||||||
|
let random_bytes: Vec<u8> = (0..random_size)
|
||||||
|
.map(|_| rng.random::<u8>())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut hasher = Blake2b512::new();
|
||||||
|
hasher.update(&random_bytes);
|
||||||
|
let hash = format!("{:x}", hasher.finalize());
|
||||||
|
|
||||||
|
(hash, random_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Graph {
|
impl Graph {
|
||||||
|
pub fn new(root: Arc<Reference>) -> Self {
|
||||||
|
let mut refs = HashMap::new();
|
||||||
|
refs.insert(root.name.clone(), root.clone());
|
||||||
|
let refs = Arc::new(refs);
|
||||||
|
let objects = Arc::new(HashMap::new());
|
||||||
|
Self {
|
||||||
|
root,
|
||||||
|
refs,
|
||||||
|
objects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets a reference by its path
|
/// Gets a reference by its path
|
||||||
pub fn get_reference(&self, path: &str) -> Option<Arc<Reference>> {
|
pub fn get_reference(&self, path: &str) -> Option<Arc<Reference>> {
|
||||||
self.refs.get(path).cloned()
|
self.refs.get(path).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets an object by its content address
|
/// Gets an object by its content address
|
||||||
pub fn get_object(&self, content_address: &str) -> Option<&String> {
|
pub fn get_object(&self, content_address: &str) -> Option<&Vec<u8>> {
|
||||||
self.objects.get(content_address)
|
self.objects.get(content_address)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn random_object() -> (String, String) {
|
/// Updates a reference to point to a new object, recalculating content addresses and IDs
|
||||||
let mut rng = rand::rng();
|
/// for all affected references in the graph.
|
||||||
let random_size = rng.random_range(50..=4096);
|
///
|
||||||
let random_string: String = (0..random_size)
|
/// The reference ID is calculated from the content address, name, and any dependents,
|
||||||
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
/// ensuring that it's truly content-addressable.
|
||||||
.collect();
|
pub fn update_object_reference(&mut self, name: &String, new_content: Vec<u8>) -> Result<(), String> {
|
||||||
|
// Update the root reference if needed
|
||||||
let mut hasher = Blake2b512::new();
|
if name == &self.root.name {
|
||||||
hasher.update(&random_string);
|
self.root = self.refs.get(name).unwrap().clone();
|
||||||
let hash = format!("{:x}", hasher.finalize());
|
return Ok(());
|
||||||
|
}
|
||||||
(hash, random_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Graph {
|
|
||||||
/// Updates a reference to point to a new object, recalculating content addresses
|
|
||||||
/// for all affected references in the graph
|
|
||||||
pub fn update_reference(&mut self, path: &String, new_object_id: String, new_content: String) -> Result<(), String> {
|
|
||||||
// Create a mutable copy of our maps
|
// Create a mutable copy of our maps
|
||||||
let mut refs = HashMap::new();
|
let mut refs = HashMap::new();
|
||||||
for (k, v) in self.refs.as_ref() {
|
for (k, v) in self.refs.as_ref() {
|
||||||
@ -98,34 +168,36 @@ impl Graph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the reference to update
|
// Find the reference to update
|
||||||
let ref_to_update = refs.get(path).ok_or_else(|| format!("Reference with path {} not found", path))?;
|
let ref_to_update = refs.get(name).ok_or_else(|| format!("Reference with name {} not found", name))?;
|
||||||
|
|
||||||
// Calculate hash for the new content
|
// Calculate hash for the new content
|
||||||
let mut hasher = Blake2b512::new();
|
let mut hasher = Blake2b512::new();
|
||||||
hasher.update(&new_content);
|
hasher.update(&new_content);
|
||||||
let new_address = format!("{:x}", hasher.finalize());
|
let new_address = format!("{:x}", hasher.finalize());
|
||||||
|
|
||||||
// Create updated reference
|
// Create a new reference with the updated content address
|
||||||
let updated_ref = Arc::new(Reference {
|
// The ID will be calculated based on the content address, name, and dependents
|
||||||
object_id: new_object_id,
|
let mut updated_ref = Reference::new(
|
||||||
content_address: new_address.clone(),
|
Some(new_address.clone()),
|
||||||
path: path.to_string(),
|
name.to_string()
|
||||||
dependents: ref_to_update.dependents.clone(),
|
);
|
||||||
});
|
|
||||||
|
// Add all dependents to the updated reference
|
||||||
|
for dep in &ref_to_update.dependents {
|
||||||
|
updated_ref = updated_ref.add_dep(dep.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let updated_ref = updated_ref.to_arc();
|
||||||
|
|
||||||
// Update objects map with new content
|
// Update objects map with new content
|
||||||
objects.insert(new_address.clone(), new_content);
|
objects.insert(new_address.clone(), new_content);
|
||||||
|
|
||||||
// Update references map with new reference
|
// Update references map with new reference
|
||||||
refs.insert(path.to_string(), updated_ref.clone());
|
refs.insert(name.to_string(), updated_ref.clone());
|
||||||
|
|
||||||
// Find and update all parent references that contain this reference
|
// Find and update all parent references that contain this reference
|
||||||
self.update_parent_references(&mut refs, path)?;
|
self.update_parent_references(&mut refs, name);
|
||||||
|
|
||||||
// Update the root reference if needed
|
|
||||||
if path == &self.root.path {
|
|
||||||
self.root = refs.get(path).unwrap().clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the Arc maps
|
// Update the Arc maps
|
||||||
self.refs = Arc::new(refs);
|
self.refs = Arc::new(refs);
|
||||||
@ -133,102 +205,114 @@ impl Graph {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TODO(jwall): Add new reference
|
||||||
|
|
||||||
/// Recursively updates parent references when a child reference changes
|
/// Recursively updates parent references when a child reference changes
|
||||||
fn update_parent_references(&self, refs: &mut HashMap<String, Arc<Reference>>, updated_path: &str) -> Result<(), String> {
|
fn update_parent_references(&mut self, refs: &mut HashMap<String, Arc<Reference>>, updated_name: &str) {
|
||||||
// Find all references that have the updated reference as a dependent
|
// Find all references that have the updated reference as a dependent
|
||||||
let parent_paths: Vec<String> = refs
|
let parent_names: Vec<String> = refs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, r)| r.dependents.iter().any(|dep| dep.path == updated_path))
|
.filter(|(_, r)| r.dependents.iter().any(|dep| dep.name == updated_name))
|
||||||
.map(|(path, _)| path.clone())
|
.map(|(name, _)| name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for parent_path in parent_paths {
|
for parent_name in parent_names {
|
||||||
if let Some(parent_ref) = refs.get(&parent_path) {
|
if let Some(parent_ref) = refs.get(&parent_name) {
|
||||||
// Create a new list of dependents with the updated reference
|
// Create a new reference with the same content address and name
|
||||||
let mut new_dependents = Vec::new();
|
let mut updated_parent = Reference::new(
|
||||||
|
parent_ref.content_address.clone(),
|
||||||
|
parent_ref.name.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add all dependents, replacing the updated one
|
||||||
for dep in &parent_ref.dependents {
|
for dep in &parent_ref.dependents {
|
||||||
if dep.path == updated_path {
|
// Update the root reference if needed
|
||||||
|
if dep.name == updated_name {
|
||||||
// Use the updated reference
|
// Use the updated reference
|
||||||
new_dependents.push(refs.get(updated_path).unwrap().clone());
|
updated_parent = updated_parent.add_dep(refs.get(updated_name).unwrap().clone());
|
||||||
} else {
|
} else {
|
||||||
// Keep the existing dependent
|
// Keep the existing dependent
|
||||||
new_dependents.push(dep.clone());
|
updated_parent = updated_parent.add_dep(dep.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate new content address based on updated dependents
|
// The ID is automatically calculated in the add_dep method
|
||||||
let mut hasher = Blake2b512::new();
|
let updated_parent = Arc::new(updated_parent);
|
||||||
for dep in &new_dependents {
|
if updated_parent.name == self.root.name {
|
||||||
hasher.update(&dep.content_address);
|
self.root = updated_parent.clone();
|
||||||
}
|
}
|
||||||
let new_address = format!("{:x}", hasher.finalize());
|
|
||||||
|
|
||||||
// Create updated parent reference
|
|
||||||
let updated_parent = Arc::new(Reference {
|
|
||||||
object_id: parent_ref.object_id.clone(),
|
|
||||||
content_address: new_address,
|
|
||||||
path: parent_ref.path.clone(),
|
|
||||||
dependents: new_dependents,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the references map
|
// Update the references map
|
||||||
refs.insert(parent_path.clone(), updated_parent);
|
refs.insert(parent_name.clone(), updated_parent);
|
||||||
|
|
||||||
// Recursively update parents of this parent
|
// Recursively update parents of this parent
|
||||||
self.update_parent_references(refs, &parent_path)?;
|
self.update_parent_references(refs, &parent_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn random_graph() -> Graph {
|
pub fn random_graph() -> Graph {
|
||||||
let path_root = String::from("ref/0");
|
let root_name = String::from("ref/0");
|
||||||
let mut objects = HashMap::new();
|
let mut objects = HashMap::new();
|
||||||
let mut refs = HashMap::new();
|
let mut refs = HashMap::new();
|
||||||
|
|
||||||
|
// Create the root reference
|
||||||
let mut root_ref = Reference::new(
|
let mut root_ref = Reference::new(
|
||||||
"username:0".to_string(),
|
Some(String::from("root_content")),
|
||||||
String::from("0"),
|
root_name.clone(),
|
||||||
path_root.clone(),
|
|
||||||
);
|
);
|
||||||
let mut root_hasher = Blake2b512::new();
|
|
||||||
|
// Create 10 item references
|
||||||
for i in 1..=10 {
|
for i in 1..=10 {
|
||||||
|
let item_name = format!("/item/{}", i);
|
||||||
let mut item_ref = Reference::new(
|
let mut item_ref = Reference::new(
|
||||||
format!("item:{}", i),
|
Some(format!("item_content_{}", i)),
|
||||||
format!("0:{}", i),
|
item_name.clone(),
|
||||||
format!("/item/{}", i),
|
|
||||||
);
|
);
|
||||||
let mut hasher = Blake2b512::new();
|
|
||||||
|
// Create 10 subitems for each item
|
||||||
for j in 1..=10 {
|
for j in 1..=10 {
|
||||||
let (address, content) = random_object();
|
let (address, content) = random_object();
|
||||||
hasher.update(&content);
|
let subitem_name = format!("/item/{}/subitem/{}", i, j);
|
||||||
|
|
||||||
|
// Create a leaf reference
|
||||||
let leaf_ref = Reference::new(
|
let leaf_ref = Reference::new(
|
||||||
format!("item:{}:subitem:{}", i, j),
|
Some(address.clone()),
|
||||||
format!("{}", address),
|
subitem_name,
|
||||||
format!("/item/{}/subitem/{}", i, j),
|
).to_arc();
|
||||||
)
|
|
||||||
.to_arc();
|
// Add the leaf reference as a dependent to the item reference
|
||||||
item_ref = item_ref.add_dep(leaf_ref.clone());
|
item_ref = item_ref.add_dep(leaf_ref.clone());
|
||||||
|
|
||||||
|
// Store the content in the objects map
|
||||||
objects.insert(address.clone(), content);
|
objects.insert(address.clone(), content);
|
||||||
hasher.update(&leaf_ref.content_address);
|
|
||||||
refs.insert(leaf_ref.path.clone(), leaf_ref);
|
// Store the leaf reference in the refs map
|
||||||
|
refs.insert(leaf_ref.name.clone(), leaf_ref);
|
||||||
}
|
}
|
||||||
let hash = format!("{:x}", hasher.finalize());
|
|
||||||
item_ref.content_address = hash;
|
// Convert the item reference to Arc and add it to the root reference
|
||||||
root_hasher.update(&item_ref.content_address);
|
let arc_item_ref = item_ref.to_arc();
|
||||||
let rc_ref = item_ref.to_arc();
|
root_ref = root_ref.add_dep(arc_item_ref.clone());
|
||||||
root_ref = root_ref.add_dep(rc_ref.clone());
|
|
||||||
refs.insert(rc_ref.path.clone(), rc_ref);
|
// Store the item reference in the refs map
|
||||||
|
refs.insert(arc_item_ref.name.clone(), arc_item_ref);
|
||||||
}
|
}
|
||||||
root_ref.content_address = format!("{:x}", root_hasher.finalize());
|
|
||||||
let rc_root = root_ref.to_arc();
|
// Convert the root reference to Arc
|
||||||
refs.insert(rc_root.path.clone(), rc_root.clone());
|
let arc_root_ref = root_ref.to_arc();
|
||||||
dbg!(&objects);
|
|
||||||
|
// Store the root reference in the refs map
|
||||||
|
refs.insert(arc_root_ref.name.clone(), arc_root_ref.clone());
|
||||||
|
|
||||||
Graph {
|
Graph {
|
||||||
root: rc_root,
|
root: arc_root_ref,
|
||||||
refs: Arc::new(refs),
|
refs: Arc::new(refs),
|
||||||
objects: Arc::new(objects),
|
objects: Arc::new(objects),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
202
offline-web-model/src/test.rs
Normal file
202
offline-web-model/src/test.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rand::random_range;
|
||||||
|
|
||||||
|
use crate::{Graph, Reference, random_object};
|
||||||
|
|
||||||
|
fn get_random_candidate(graph: &Graph) -> Arc<Reference> {
|
||||||
|
// Pick a random leaf node to update
|
||||||
|
let refs: Vec<Arc<Reference>> =graph.refs.values().filter(|r| r.name != graph.root.name).map(|r| r.clone()).collect();
|
||||||
|
let random_index = random_range(0..refs.len());
|
||||||
|
refs[random_index].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that all dependencies are kept updated when new nodes are added
|
||||||
|
#[test]
|
||||||
|
fn test_dependencies_updated_when_nodes_added() {
|
||||||
|
// Create a simple graph
|
||||||
|
let mut graph = create_test_graph();
|
||||||
|
|
||||||
|
// Get the initial content address of the root
|
||||||
|
let initial_root_id = graph.root.id.clone();
|
||||||
|
|
||||||
|
let candidate = get_random_candidate(&graph);
|
||||||
|
|
||||||
|
// Update the leaf node
|
||||||
|
graph.update_object_reference(&candidate.name, random_object().1).unwrap();
|
||||||
|
|
||||||
|
// Verify that the leaf node's ID has changed
|
||||||
|
let updated_leaf = dbg!(graph.get_reference(&candidate.name).unwrap());
|
||||||
|
assert_ne!(updated_leaf.id, candidate.id,
|
||||||
|
"Leaf node ID should change when content is updated");
|
||||||
|
|
||||||
|
// Verify that the root's ID has changed
|
||||||
|
assert_ne!(graph.root.id, initial_root_id,
|
||||||
|
"Root ID should change when a dependent node is updated");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that the root of the graph is not itself a dependency of any other node
|
||||||
|
#[test]
|
||||||
|
fn test_root_not_a_dependency() {
|
||||||
|
let graph = create_test_graph();
|
||||||
|
let root_name = graph.root.name.clone();
|
||||||
|
|
||||||
|
// Check all references to ensure none have the root as a dependent
|
||||||
|
for (_, reference) in graph.refs.as_ref() {
|
||||||
|
for dep in &reference.dependents {
|
||||||
|
assert_ne!(dep.name, root_name,
|
||||||
|
"Root should not be a dependency of any other node");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that all nodes are dependents or transitive dependents of the root
|
||||||
|
#[test]
|
||||||
|
fn test_all_nodes_connected_to_root() {
|
||||||
|
let graph = create_test_graph();
|
||||||
|
|
||||||
|
// Collect all nodes reachable from the root
|
||||||
|
let mut reachable = HashSet::new();
|
||||||
|
|
||||||
|
fn collect_reachable(node: &Arc<Reference>, reachable: &mut HashSet<String>) {
|
||||||
|
reachable.insert(node.name.clone());
|
||||||
|
|
||||||
|
for dep in &node.dependents {
|
||||||
|
if !reachable.contains(&dep.name) {
|
||||||
|
collect_reachable(dep, reachable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_reachable(&graph.root, &mut reachable);
|
||||||
|
|
||||||
|
// Check that all nodes in the graph are reachable from the root
|
||||||
|
for (name, _) in graph.refs.as_ref() {
|
||||||
|
assert!(reachable.contains(name),
|
||||||
|
"All nodes should be reachable from the root: {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to create a test graph with a known structure
|
||||||
|
fn create_test_graph() -> Graph {
|
||||||
|
let root_name = String::from("/root");
|
||||||
|
let mut objects = HashMap::new();
|
||||||
|
let mut refs = HashMap::new();
|
||||||
|
|
||||||
|
// Create the root reference
|
||||||
|
let mut root_ref = Reference::new(
|
||||||
|
Some(String::from("root_content")),
|
||||||
|
root_name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create 3 item references
|
||||||
|
for i in 1..=3 {
|
||||||
|
let item_name = format!("/item/{}", i);
|
||||||
|
let mut item_ref = Reference::new(
|
||||||
|
Some(format!("item_content_{}", i)),
|
||||||
|
item_name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create 3 subitems for each item
|
||||||
|
for j in 1..=3 {
|
||||||
|
let (address, content) = random_object();
|
||||||
|
let subitem_name = format!("/item/{}/subitem/{}", i, j);
|
||||||
|
|
||||||
|
// Create a leaf reference
|
||||||
|
let leaf_ref = Reference::new(
|
||||||
|
Some(address.clone()),
|
||||||
|
subitem_name,
|
||||||
|
).to_arc();
|
||||||
|
|
||||||
|
// Add the leaf reference as a dependent to the item reference
|
||||||
|
item_ref = item_ref.add_dep(leaf_ref.clone());
|
||||||
|
|
||||||
|
// Store the content in the objects map
|
||||||
|
objects.insert(address.clone(), content);
|
||||||
|
|
||||||
|
// Store the leaf reference in the refs map
|
||||||
|
refs.insert(leaf_ref.name.clone(), leaf_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the item reference to Arc and add it to the root reference
|
||||||
|
let arc_item_ref = item_ref.to_arc();
|
||||||
|
root_ref = root_ref.add_dep(arc_item_ref.clone());
|
||||||
|
|
||||||
|
// Store the item reference in the refs map
|
||||||
|
refs.insert(arc_item_ref.name.clone(), arc_item_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the root reference to Arc
|
||||||
|
let arc_root_ref = root_ref.to_arc();
|
||||||
|
|
||||||
|
// Store the root reference in the refs map
|
||||||
|
refs.insert(arc_root_ref.name.clone(), arc_root_ref.clone());
|
||||||
|
|
||||||
|
Graph {
|
||||||
|
root: arc_root_ref,
|
||||||
|
refs: Arc::new(refs),
|
||||||
|
objects: Arc::new(objects),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that the graph correctly handles content-addressable properties
|
||||||
|
#[test]
|
||||||
|
fn test_content_addressable_properties() {
|
||||||
|
let mut graph = create_test_graph();
|
||||||
|
|
||||||
|
// Update a leaf node with the same content
|
||||||
|
let leaf_path = "/item/1/subitem/1".to_string();
|
||||||
|
let initial_leaf = graph.get_reference(&leaf_path).unwrap();
|
||||||
|
if let Some(content_address) = initial_leaf.content_address.clone() {
|
||||||
|
// Get the content for this address
|
||||||
|
let content = graph.get_object(&content_address).unwrap().clone();
|
||||||
|
|
||||||
|
// Update with the same content
|
||||||
|
graph.update_object_reference(&leaf_path, content).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that nothing changed since the content is the same
|
||||||
|
let updated_leaf = graph.get_reference(&leaf_path).unwrap();
|
||||||
|
assert_eq!(updated_leaf.content_address, initial_leaf.content_address,
|
||||||
|
"Content address should not change when content remains the same");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that the graph correctly handles ID calculation
|
||||||
|
#[test]
|
||||||
|
fn test_id_calculation() {
|
||||||
|
let mut graph = create_test_graph();
|
||||||
|
|
||||||
|
// Update a leaf node
|
||||||
|
let leaf_path = "/item/1/subitem/1".to_string();
|
||||||
|
let initial_leaf = graph.get_reference(&leaf_path).unwrap();
|
||||||
|
|
||||||
|
graph.update_object_reference(&leaf_path, "new content".as_bytes().to_vec()).unwrap();
|
||||||
|
|
||||||
|
// Verify that the ID changed
|
||||||
|
let updated_leaf = graph.get_reference(&leaf_path).unwrap();
|
||||||
|
assert_ne!(updated_leaf.id, initial_leaf.id,
|
||||||
|
"Reference ID should change when content changes");
|
||||||
|
|
||||||
|
// Verify that parent ID changed
|
||||||
|
let parent_path = "/item/1".to_string();
|
||||||
|
let parent = graph.get_reference(&parent_path).unwrap();
|
||||||
|
|
||||||
|
// Create a reference with the same properties to calculate expected ID
|
||||||
|
let mut test_ref = Reference::new(
|
||||||
|
parent.content_address.clone(),
|
||||||
|
parent.name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the same dependents
|
||||||
|
for dep in &parent.dependents {
|
||||||
|
test_ref = test_ref.add_dep(dep.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the ID calculation is consistent
|
||||||
|
assert_eq!(parent.id, test_ref.id,
|
||||||
|
"ID calculation should be consistent for the same reference properties");
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user