Files
galaxy/crates/remote_server/src/repo_metadata_proto.rs
T

292 lines
10 KiB
Rust

//! Conversion between `repo_metadata` Rust types and proto-generated types.
//!
//! The Rust types in `repo_metadata::file_tree_update` were designed to mirror the
//! proto schema 1:1, so these conversions are straightforward field mappings.
use galaxy_util::standardized_path::StandardizedPath;
use repo_metadata::file_tree_store::{FileTreeEntry, FileTreeEntryState};
use repo_metadata::file_tree_update::{
DirectoryNodeMetadata, FileNodeMetadata, FileTreeEntryUpdate, RepoMetadataUpdate,
RepoNodeMetadata,
};
use crate::proto;
// ── Rust → Proto ────────────────────────────────────────────
impl From<&RepoMetadataUpdate> for proto::RepoMetadataUpdatePush {
fn from(update: &RepoMetadataUpdate) -> Self {
Self {
repo_path: update.repo_path.to_string(),
remove_entries: update
.remove_entries
.iter()
.map(|p| p.to_string())
.collect(),
update_entries: update
.update_entries
.iter()
.map(proto::RepoMetadataEntryUpdate::from)
.collect(),
}
}
}
impl From<&FileTreeEntryUpdate> for proto::RepoMetadataEntryUpdate {
fn from(update: &FileTreeEntryUpdate) -> Self {
Self {
parent_path_to_replace: update.parent_path_to_replace.to_string(),
subtree_metadata: update
.subtree_metadata
.iter()
.map(proto::RepoNodeMetadata::from)
.collect(),
}
}
}
impl From<&RepoNodeMetadata> for proto::RepoNodeMetadata {
fn from(node: &RepoNodeMetadata) -> Self {
let node_oneof = match node {
RepoNodeMetadata::Directory(dir) => {
proto::repo_node_metadata::Node::Directory(proto::DirectoryNodeMetadata {
path: dir.path.to_string(),
ignored: dir.ignored,
loaded: dir.loaded,
})
}
RepoNodeMetadata::File(file) => {
proto::repo_node_metadata::Node::File(proto::FileNodeMetadata {
path: file.path.to_string(),
extension: file.extension.clone(),
ignored: file.ignored,
})
}
};
Self {
node: Some(node_oneof),
}
}
}
/// Serializes a full `FileTreeEntry`
/// messages suitable for a `RepoMetadataSnapshot`.
///
/// Walks the tree breadth-first from the root, producing one `RepoMetadataEntryUpdate`
/// per parent directory containing its immediate children as `RepoNodeMetadata`.
pub fn file_tree_entry_to_snapshot_proto(
entry: &FileTreeEntry,
) -> Vec<proto::RepoMetadataEntryUpdate> {
let mut result = Vec::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(entry.root_directory().clone());
while let Some(current_path) = queue.pop_front() {
let children: Vec<_> = entry.child_paths(&current_path).cloned().collect();
if children.is_empty() {
continue;
}
let mut subtree_metadata = Vec::with_capacity(children.len());
for child_path in &children {
match entry.get(child_path) {
Some(FileTreeEntryState::Directory(dir)) => {
subtree_metadata.push(proto::RepoNodeMetadata {
node: Some(proto::repo_node_metadata::Node::Directory(
proto::DirectoryNodeMetadata {
path: dir.path.to_string(),
ignored: dir.ignored,
loaded: dir.loaded,
},
)),
});
// Enqueue for breadth-first traversal.
queue.push_back(child_path.clone());
}
Some(FileTreeEntryState::File(file)) => {
subtree_metadata.push(proto::RepoNodeMetadata {
node: Some(proto::repo_node_metadata::Node::File(
proto::FileNodeMetadata {
path: file.path.to_string(),
extension: file.extension.clone(),
ignored: file.ignored,
},
)),
});
}
None => {}
}
}
if !subtree_metadata.is_empty() {
result.push(proto::RepoMetadataEntryUpdate {
parent_path_to_replace: current_path.to_string(),
subtree_metadata,
});
}
}
result
}
// ── Proto → Rust ──────────────────────────────────────────────────
/// Converts a `RepoMetadataUpdatePush` proto message into a `RepoMetadataUpdate`.
pub fn proto_to_repo_metadata_update(
push: &proto::RepoMetadataUpdatePush,
) -> Option<RepoMetadataUpdate> {
let repo_path = StandardizedPath::try_new(&push.repo_path).ok()?;
let remove_entries: Vec<StandardizedPath> = push
.remove_entries
.iter()
.filter_map(|p| match StandardizedPath::try_new(p) {
Ok(path) => Some(path),
Err(e) => {
log::warn!("Skipping invalid remove_entry path {p:?}: {e}");
None
}
})
.collect();
let update_entries: Vec<FileTreeEntryUpdate> = push
.update_entries
.iter()
.filter_map(proto_to_entry_update)
.collect();
Some(RepoMetadataUpdate {
repo_path,
remove_entries,
update_entries,
})
}
/// Converts a `RepoMetadataSnapshot` proto into a `RepoMetadataUpdate`
/// (with no removals) that can be applied to a `RemoteRepoMetadataModel`.
pub fn proto_snapshot_to_update(
snapshot: &proto::RepoMetadataSnapshot,
) -> Option<RepoMetadataUpdate> {
let repo_path = StandardizedPath::try_new(&snapshot.repo_path).ok()?;
let update_entries: Vec<FileTreeEntryUpdate> = snapshot
.entries
.iter()
.filter_map(proto_to_entry_update)
.collect();
Some(RepoMetadataUpdate {
repo_path,
remove_entries: Vec::new(),
update_entries,
})
}
fn proto_to_entry_update(
proto_update: &proto::RepoMetadataEntryUpdate,
) -> Option<FileTreeEntryUpdate> {
let parent_path = StandardizedPath::try_new(&proto_update.parent_path_to_replace).ok()?;
let subtree_metadata: Vec<RepoNodeMetadata> = proto_update
.subtree_metadata
.iter()
.filter_map(proto_to_repo_node_metadata)
.collect();
Some(FileTreeEntryUpdate {
parent_path_to_replace: parent_path,
subtree_metadata,
})
}
/// Converts a `LoadRepoMetadataDirectoryResponse` proto into a `RepoMetadataUpdate`
/// (with no removals) that can be applied to a `RemoteRepoMetadataModel`.
pub fn proto_load_repo_metadata_directory_response_to_update(
resp: &proto::LoadRepoMetadataDirectoryResponse,
) -> Option<RepoMetadataUpdate> {
let repo_path = StandardizedPath::try_new(&resp.repo_path).ok()?;
let update_entries: Vec<FileTreeEntryUpdate> = resp
.entries
.iter()
.filter_map(proto_to_entry_update)
.collect();
Some(RepoMetadataUpdate {
repo_path,
remove_entries: Vec::new(),
update_entries,
})
}
/// Serializes the immediate children of a directory in a `FileTreeEntry` as
/// `RepoMetadataEntryUpdate` protos. Used to build a `LoadRepoMetadataDirectoryResponse`.
pub fn file_tree_children_to_proto_entries(
entry: &FileTreeEntry,
dir_path: &StandardizedPath,
) -> Vec<proto::RepoMetadataEntryUpdate> {
let children: Vec<_> = entry.child_paths(dir_path).cloned().collect();
if children.is_empty() {
return Vec::new();
}
let mut subtree_metadata = Vec::with_capacity(children.len());
for child_path in &children {
match entry.get(child_path) {
Some(FileTreeEntryState::Directory(dir)) => {
subtree_metadata.push(proto::RepoNodeMetadata {
node: Some(proto::repo_node_metadata::Node::Directory(
proto::DirectoryNodeMetadata {
path: dir.path.to_string(),
ignored: dir.ignored,
loaded: dir.loaded,
},
)),
});
}
Some(FileTreeEntryState::File(file)) => {
subtree_metadata.push(proto::RepoNodeMetadata {
node: Some(proto::repo_node_metadata::Node::File(
proto::FileNodeMetadata {
path: file.path.to_string(),
extension: file.extension.clone(),
ignored: file.ignored,
},
)),
});
}
None => {}
}
}
if subtree_metadata.is_empty() {
return Vec::new();
}
vec![proto::RepoMetadataEntryUpdate {
parent_path_to_replace: dir_path.to_string(),
subtree_metadata,
}]
}
fn proto_to_repo_node_metadata(proto_node: &proto::RepoNodeMetadata) -> Option<RepoNodeMetadata> {
match proto_node.node.as_ref()? {
proto::repo_node_metadata::Node::Directory(dir) => {
let path = StandardizedPath::try_new(&dir.path).ok()?;
Some(RepoNodeMetadata::Directory(DirectoryNodeMetadata {
path,
ignored: dir.ignored,
loaded: dir.loaded,
}))
}
proto::repo_node_metadata::Node::File(file) => {
let path = StandardizedPath::try_new(&file.path).ok()?;
Some(RepoNodeMetadata::File(FileNodeMetadata {
path,
extension: file.extension.clone(),
ignored: file.ignored,
}))
}
}
}