Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions document/format/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,14 +330,13 @@ impl<L: Layout> Gdd<L> {
&self.network_view_settings
}

/// Resolve each runtime `network_path` to its stable [`NetworkId`](document_graph_storage::NetworkId), so the
/// editor can key per-network, per-peer view state by a stable id. See [`Session::network_ids`].
/// The stable [`NetworkId`](document_graph_storage::NetworkId) of each runtime network location, to key per-network view state. See [`Session::network_ids`].
#[cfg(feature = "conversion")]
pub fn network_ids<M: NodeMetadataSource>(
&self,
network: &graph_craft::document::NodeNetwork,
metadata: &M,
) -> Result<std::collections::HashMap<Vec<core_types::uuid::NodeId>, document_graph_storage::NetworkId>, CommitError> {
) -> Result<std::collections::HashMap<graph_craft::document::OwnedNetworkLocation, document_graph_storage::NetworkId>, CommitError> {
self.session.network_ids(network, metadata)
}

Expand Down
382 changes: 201 additions & 181 deletions document/graph-storage/src/from_runtime.rs

Large diffs are not rendered by default.

44 changes: 21 additions & 23 deletions document/graph-storage/src/metadata_source.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
//! Lets `from_runtime` read editor-side per-node metadata without depending on the editor crate.
//! The editor implements this on `NodeNetworkInterface`; tests pass [`NoMetadata`].
//!
//! `network_path` is the chain of runtime local `NodeId`s from the root down to (but not including)
//! the queried node, matching `NodeNetworkInterface::node_metadata(node_id, network_path)`.
//! `network` is the location of the network holding the queried node, or of the queried network itself.

use std::collections::{BTreeMap, HashMap};

use core_types::uuid::NodeId as RuntimeNodeId;
use graph_craft::document::{NetworkLocation, OwnedNetworkLocation};

use crate::Value;

/// One node's editor-side metadata, produced by `Registry::to_runtime_with_metadata`. One entry per
/// node, since every node carries an identity to restore even when it carries no `ui::*` attribute.
#[derive(Clone, Debug, PartialEq)]
pub struct NodeMetadataEntry {
pub network_path: Vec<RuntimeNodeId>,
pub network: OwnedNetworkLocation,
pub local_id: RuntimeNodeId,
/// The node's storage identity. Restoring it pins the node to this identity, so a later conversion
/// keeps it instead of re-deriving one from the node's location.
Expand All @@ -35,9 +35,7 @@ pub struct NodeMetadataEntry {
/// properties of a network, not of any node.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NetworkMetadataEntry {
/// Owning-node chain from the root to (and including) the node containing this network.
/// Empty = root network.
pub network_path: Vec<RuntimeNodeId>,
pub network: OwnedNetworkLocation,
/// Stable storage id of this network. Lets the editor associate per-network, per-peer view state
/// (the node-graph nav, in `session.json`) with a network across reparenting.
pub network_id: crate::NetworkId,
Expand All @@ -64,69 +62,69 @@ pub struct InputMetadataEntry {
/// Editor-side metadata source. Methods default to "no data" so implementors only override what
/// they carry. Returns are type-erased as [`Value`] where the underlying types live editor-side (PTZ, etc.).
pub trait NodeMetadataSource {
fn position(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> Option<Position> {
fn position(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> Option<Position> {
None
}
fn is_layer(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> bool {
fn is_layer(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> bool {
false
}
fn display_name(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> Option<&str> {
fn display_name(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> Option<&str> {
None
}
fn locked(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> bool {
fn locked(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> bool {
false
}
fn pinned(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> bool {
fn pinned(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> bool {
false
}
/// Empty vec = no overrides. Stored as a single `ui::output_names` attribute (whole-vec LWW).
fn output_names(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> Vec<String> {
fn output_names(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> Vec<String> {
Vec::new()
}
/// Indexed like `output_names`, where a missing or empty entry is no description. Stored as one `ui::output_descriptions` attribute.
fn output_descriptions(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> Vec<String> {
fn output_descriptions(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> Vec<String> {
Vec::new()
}

fn input_name(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId, _input_index: usize) -> Option<&str> {
fn input_name(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId, _input_index: usize) -> Option<&str> {
None
}
fn input_description(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId, _input_index: usize) -> Option<&str> {
fn input_description(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId, _input_index: usize) -> Option<&str> {
None
}
fn widget_override(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId, _input_index: usize) -> Option<&str> {
fn widget_override(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId, _input_index: usize) -> Option<&str> {
None
}
/// Returns owned to stay object-safe. Each entry is stored as `ui::input_data::<key>` for per-key LWW.
fn input_data(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId, _input_index: usize) -> HashMap<String, Value> {
fn input_data(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId, _input_index: usize) -> HashMap<String, Value> {
HashMap::new()
}

/// The storage identity this node is pinned to, if it has one. A node the source does not name
/// falls back to a hash of its location.
fn storage_node_id(&self, _network_path: &[RuntimeNodeId], _local_id: RuntimeNodeId) -> Option<crate::NodeId> {
fn storage_node_id(&self, _network: NetworkLocation<'_>, _local_id: RuntimeNodeId) -> Option<crate::NodeId> {
None
}

/// The definition the network was instantiated from, if it still names one.
fn reference(&self, _network_path: &[RuntimeNodeId]) -> Option<&str> {
fn reference(&self, _network: NetworkLocation<'_>) -> Option<&str> {
None
}

/// The display order of the network's pinned nodes.
fn pinned_order(&self, _network_path: &[RuntimeNodeId]) -> Vec<RuntimeNodeId> {
fn pinned_order(&self, _network: NetworkLocation<'_>) -> Vec<RuntimeNodeId> {
Vec::new()
}

fn import_metadata(&self, _network_path: &[RuntimeNodeId]) -> Vec<InputMetadataEntry> {
fn import_metadata(&self, _network: NetworkLocation<'_>) -> Vec<InputMetadataEntry> {
Vec::new()
}

fn export_names(&self, _network_path: &[RuntimeNodeId]) -> Vec<String> {
fn export_names(&self, _network: NetworkLocation<'_>) -> Vec<String> {
Vec::new()
}

fn export_descriptions(&self, _network_path: &[RuntimeNodeId]) -> Vec<String> {
fn export_descriptions(&self, _network: NetworkLocation<'_>) -> Vec<String> {
Vec::new()
}
}
Expand Down
11 changes: 7 additions & 4 deletions document/graph-storage/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ impl Session {
Ok((hot_ops, conversion))
}

/// Resolve each runtime `network_path` to its stable [`NetworkId`] for this document's peer, so the
/// caller can key per-network, per-peer view state (`session.json`) by a stable id. Derived from the
/// network structure alone; resources/declarations are irrelevant to the ids.
/// The stable [`NetworkId`] of each runtime network location for this document's peer, to key per-network view state in `session.json`.
/// It depends only on the network structure, not on resources or declarations.
#[cfg(any(feature = "conversion", test))]
pub fn network_ids<M: NodeMetadataSource>(&self, network: &graph_craft::document::NodeNetwork, metadata: &M) -> Result<HashMap<Vec<core_types::uuid::NodeId>, NetworkId>, CommitError> {
pub fn network_ids<M: NodeMetadataSource>(
&self,
network: &graph_craft::document::NodeNetwork,
metadata: &M,
) -> Result<HashMap<graph_craft::document::OwnedNetworkLocation, NetworkId>, CommitError> {
let conversion = Registry::convert_from_runtime(network, metadata, &graphene_resource::ResourceRegistry::new(), self.document.peer)?;
Ok(conversion.network_ids)
}
Expand Down
66 changes: 18 additions & 48 deletions document/graph-storage/src/tests/projection.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use std::collections::BTreeSet;

use core_types::uuid::NodeId as RuntimeNodeId;
use graph_craft::ProtoNodeIdentifier;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NetworkId as RuntimeNetworkId, NodeInput, NodeNetwork, OwnedNetworkLocation};

use crate::to_runtime::ConversionError;
use crate::{Declarations, Network, NetworkId, NoMetadata, Node, NodeId, PeerId, ROOT_NETWORK, Registry};
Expand Down Expand Up @@ -70,67 +68,42 @@ fn projecting_a_node_matches_the_whole_conversion() {
let resources = graphene_resource::ResourceRegistry::new();
let conversion = Registry::convert_from_runtime(&nested_network(), &NoMetadata, &resources, PeerId(1)).expect("conversion");
let registry = &conversion.registry;
let (whole, node_entries, network_entries) = registry.to_runtime_with_full_metadata(&conversion.declarations).expect("whole conversion");
let (whole, node_entries, _) = registry.to_runtime_with_full_metadata(&conversion.declarations).expect("whole conversion");

let projection = registry.runtime_projection(&conversion.declarations);
for &id in registry.node_instances.keys() {
let projected = projection.node(id).expect("every stored node projects");
let (path, local_id) = projection.node_address(id).expect("every stored node has an address");
assert_eq!((path.as_slice(), local_id), (projected.network_path.as_slice(), projected.local_id));
let (network, local_id) = projection.node_address(id).expect("every stored node has an address");
assert_eq!((&network, local_id), (&projected.network, projected.local_id));

assert_eq!(
whole.nested_network(&path).and_then(|network| network.nodes.get(&local_id)),
whole.located_network(network.as_location()).and_then(|network| network.nodes.get(&local_id)),
Some(&projected.node),
"node {id} projected differently than the whole conversion converts it"
);

let own_entry = node_entries.iter().find(|entry| entry.storage_id == id).expect("the whole conversion emits an entry per node");
assert_eq!(projected.node_entries.first(), Some(own_entry), "the node's own entry comes first");

let nested_prefix: Vec<RuntimeNodeId> = [path.as_slice(), &[local_id]].concat();
let expected_nested: BTreeSet<_> = node_entries
.iter()
.filter(|entry| entry.network_path.starts_with(&nested_prefix))
.map(|entry| entry.storage_id)
.collect();
let projected_nested: BTreeSet<_> = projected.node_entries.iter().skip(1).map(|entry| entry.storage_id).collect();
assert_eq!(projected_nested, expected_nested, "nested node entries under {id}");

let expected_networks: BTreeSet<_> = network_entries
.iter()
.filter(|entry| entry.network_path.starts_with(&nested_prefix))
.map(|entry| entry.network_id)
.collect();
let projected_networks: BTreeSet<_> = projected.network_entries.iter().map(|entry| entry.network_id).collect();
assert_eq!(projected_networks, expected_networks, "nested network entries under {id}");
assert_eq!(&projected.metadata, own_entry);
}
}

#[test]
fn a_network_entry_projects_its_dense_exports_and_path() {
fn a_network_entry_projects_its_dense_exports_and_location() {
let resources = graphene_resource::ResourceRegistry::new();
let conversion = Registry::convert_from_runtime(&nested_network(), &NoMetadata, &resources, PeerId(1)).expect("conversion");
let registry = &conversion.registry;
let (whole, _, _) = registry.to_runtime_with_full_metadata(&conversion.declarations).expect("whole conversion");
let projection = registry.runtime_projection(&conversion.declarations);

let root = projection.network_entry(ROOT_NETWORK).expect("root");
assert!(root.network_path.is_empty());
assert_eq!(root.network, OwnedNetworkLocation::default());
assert_eq!(root.exports, whole.exports);

for &network_id in registry.networks.keys() {
let entry = projection.network_entry(network_id).expect("every stored network projects");
let nested = whole.nested_network(&entry.network_path).expect("the projected path exists in the whole conversion");
assert_eq!(entry.exports, nested.exports, "exports of the network at {:?}", entry.network_path);
assert_eq!(entry.scope_injections, nested.scope_injections);
if let Some(owner) = projection.owner(network_id) {
let (owner_path, owner_local) = projection.node_address(owner).expect("owner address");
assert_eq!(
entry.network_path,
[owner_path.as_slice(), &[owner_local]].concat(),
"a network's path ends in the node implementing it"
);
}
let located = whole.located_network(entry.network.as_location()).expect("the projected location exists in the whole conversion");
assert_eq!(entry.exports, located.exports, "exports of the network at {:?}", entry.network);
assert_eq!(entry.scope_injections, located.scope_injections);
}
}

Expand All @@ -145,29 +118,26 @@ fn an_unreachable_network_has_no_address() {

let declarations = Declarations::new();
let projection = registry.runtime_projection(&declarations);
assert_eq!(projection.network_path(NetworkId(5)), None);
assert_eq!(projection.network_location(NetworkId(5)), None);
assert_eq!(projection.node_address(NodeId(9)), None);
assert!(matches!(projection.node(NodeId(9)), Err(ConversionError::NetworkNotFound(_))));
}

/// The runtime holds a copy of a network under each node implementing it, so one several nodes implement has no single
/// address, and the root, which a node may name too, has no owner.
/// Nodes that implement the same network all name one table entry.
#[test]
fn a_network_several_nodes_implement_has_no_owner_and_neither_has_the_root() {
fn a_network_several_nodes_implement_has_one_location() {
let mut registry = Registry::default();
registry.networks.insert(ROOT_NETWORK, Network::default());
registry.networks.insert(NetworkId(5), Network::default());
for (id, implemented) in [(1, NetworkId(5)), (2, NetworkId(5)), (3, ROOT_NETWORK)] {
for id in [1, 2] {
let mut node = Node::dummy();
node.network = ROOT_NETWORK;
node.implementation = crate::Implementation::Network(implemented);
node.implementation = crate::Implementation::Network(NetworkId(5));
registry.node_instances.insert(NodeId(id), node);
}

let declarations = Declarations::new();
let projection = registry.runtime_projection(&declarations);
assert_eq!(projection.owner(NetworkId(5)), None);
assert_eq!(projection.network_path(NetworkId(5)), None);
assert_eq!(projection.owner(ROOT_NETWORK), None);
assert_eq!(projection.network_path(ROOT_NETWORK), Some(Vec::new()));
assert_eq!(projection.network_location(NetworkId(5)), Some(OwnedNetworkLocation::definition(RuntimeNetworkId(5))));
assert_eq!(projection.network_location(ROOT_NETWORK), Some(OwnedNetworkLocation::default()));
}
Loading
Loading