This is an automated email from the ASF dual-hosted git repository.

xiaokang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-graphar.git


The following commit(s) were added to refs/heads/main by this push:
     new 5bb4003b feat(Rust): add `GraphInfo` support (#883)
5bb4003b is described below

commit 5bb4003b9a90f07423da99f0bc4e9aabbb480ca9
Author: Jinye Wu <[email protected]>
AuthorDate: Mon Mar 2 13:51:38 2026 +0800

    feat(Rust): add `GraphInfo` support (#883)
    
    * update GraphInfo
    
    * refactor(rust): align info bindings with upstream usize APIs
    
    * update
    
    ---------
    
    Co-authored-by: Codex <codex@localhost>
---
 rust/include/graphar_rs.h    |  28 ++
 rust/src/ffi.rs              | 105 ++++++++
 rust/src/graphar_rs.cc       |  88 ++++++-
 rust/src/info/edge_info.rs   |   2 +
 rust/src/info/graph_info.rs  | 599 +++++++++++++++++++++++++++++++++++++++++++
 rust/src/info/mod.rs         |   2 +
 rust/src/info/vertex_info.rs |   3 +-
 7 files changed, 825 insertions(+), 2 deletions(-)

diff --git a/rust/include/graphar_rs.h b/rust/include/graphar_rs.h
index 230cd7e1..3d096e5a 100644
--- a/rust/include/graphar_rs.h
+++ b/rust/include/graphar_rs.h
@@ -32,6 +32,10 @@
 #include "rust/cxx.h"
 
 namespace graphar {
+struct MaybeIndex;
+
+using SharedVertexInfo = std::shared_ptr<VertexInfo>;
+using SharedEdgeInfo = std::shared_ptr<EdgeInfo>;
 using SharedPropertyGroup = std::shared_ptr<PropertyGroup>;
 using SharedAdjacentList = std::shared_ptr<AdjacentList>;
 using ConstInfoVersion = const InfoVersion;
@@ -89,6 +93,26 @@ std::shared_ptr<graphar::EdgeInfo> create_edge_info(
     const std::string& prefix,
     std::shared_ptr<graphar::ConstInfoVersion> version);
 
+std::shared_ptr<graphar::GraphInfo> load_graph_info(const std::string& path);
+std::shared_ptr<graphar::GraphInfo> create_graph_info(
+    const std::string& name,
+    const std::vector<graphar::SharedVertexInfo>& vertex_infos,
+    const std::vector<graphar::SharedEdgeInfo>& edge_infos,
+    const rust::Vec<rust::String>& labels, const std::string& prefix,
+    std::shared_ptr<graphar::ConstInfoVersion> version);
+graphar::MaybeIndex graph_info_vertex_info_index(
+    const graphar::GraphInfo& graph_info, const std::string& type);
+graphar::MaybeIndex graph_info_edge_info_index(
+    const graphar::GraphInfo& graph_info, const std::string& src_type,
+    const std::string& edge_type, const std::string& dst_type);
+
+void vertex_info_vec_push_vertex_info(
+    std::vector<graphar::SharedVertexInfo>& vertex_infos,
+    std::shared_ptr<graphar::VertexInfo> vertex_info);
+void edge_info_vec_push_edge_info(
+    std::vector<graphar::SharedEdgeInfo>& edge_infos,
+    std::shared_ptr<graphar::EdgeInfo> edge_info);
+
 void vertex_info_save(const graphar::VertexInfo& vertex_info,
                       const std::string& path);
 std::unique_ptr<std::string> vertex_info_dump(
@@ -101,4 +125,8 @@ void push_adjacent_list(graphar::AdjacentListVector& v,
 void edge_info_save(const graphar::EdgeInfo& edge_info,
                     const std::string& path);
 std::unique_ptr<std::string> edge_info_dump(const graphar::EdgeInfo& 
edge_info);
+void graph_info_save(const graphar::GraphInfo& graph_info,
+                     const std::string& path);
+std::unique_ptr<std::string> graph_info_dump(
+    const graphar::GraphInfo& graph_info);
 }  // namespace graphar_rs
diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs
index 9855a8ad..15c20584 100644
--- a/rust/src/ffi.rs
+++ b/rust/src/ffi.rs
@@ -17,6 +17,26 @@
 
 use cxx::{ExternType, SharedPtr};
 
+use crate::ffi::graphar::MaybeIndex;
+
+#[repr(transparent)]
+#[derive(Clone)]
+pub struct SharedVertexInfo(pub(crate) SharedPtr<graphar::VertexInfo>);
+
+unsafe impl ExternType for SharedVertexInfo {
+    type Id = cxx::type_id!("graphar::SharedVertexInfo");
+    type Kind = cxx::kind::Opaque;
+}
+
+#[repr(transparent)]
+#[derive(Clone)]
+pub struct SharedEdgeInfo(pub(crate) SharedPtr<graphar::EdgeInfo>);
+
+unsafe impl ExternType for SharedEdgeInfo {
+    type Id = cxx::type_id!("graphar::SharedEdgeInfo");
+    type Kind = cxx::kind::Opaque;
+}
+
 #[repr(transparent)]
 #[derive(Clone)]
 pub struct SharedPropertyGroup(pub(crate) SharedPtr<graphar::PropertyGroup>);
@@ -348,6 +368,81 @@ pub(crate) mod graphar {
         fn edge_info_dump(edge_info: &EdgeInfo) -> 
Result<UniquePtr<CxxString>>;
     }
 
+    struct MaybeIndex {
+        has_value: bool,
+        index: usize,
+    }
+
+    // `GraphInfo`
+    unsafe extern "C++" {
+        type GraphInfo;
+
+        fn GetName(&self) -> &CxxString;
+        fn GetLabels(&self) -> &CxxVector<CxxString>;
+        fn GetPrefix(&self) -> &CxxString;
+        fn version(&self) -> &SharedPtr<ConstInfoVersion>;
+        fn GetVertexInfo(&self, type_: &CxxString) -> SharedPtr<VertexInfo>;
+        fn GetEdgeInfo(
+            &self,
+            src_type: &CxxString,
+            edge_type: &CxxString,
+            dst_type: &CxxString,
+        ) -> SharedPtr<EdgeInfo>;
+        fn VertexInfoNum(&self) -> usize;
+        fn EdgeInfoNum(&self) -> usize;
+        fn GetVertexInfos(&self) -> &CxxVector<SharedVertexInfo>;
+        fn GetEdgeInfos(&self) -> &CxxVector<SharedEdgeInfo>;
+
+        #[namespace = "graphar_rs"]
+        fn load_graph_info(path: &CxxString) -> Result<SharedPtr<GraphInfo>>;
+        #[namespace = "graphar_rs"]
+        fn create_graph_info(
+            name: &CxxString,
+            vertex_infos: &CxxVector<SharedVertexInfo>,
+            edge_infos: &CxxVector<SharedEdgeInfo>,
+            labels: &Vec<String>,
+            prefix: &CxxString,
+            version: SharedPtr<ConstInfoVersion>,
+        ) -> Result<SharedPtr<GraphInfo>>;
+        #[namespace = "graphar_rs"]
+        fn graph_info_save(graph_info: &GraphInfo, path: &CxxString) -> 
Result<()>;
+        #[namespace = "graphar_rs"]
+        fn graph_info_dump(graph_info: &GraphInfo) -> 
Result<UniquePtr<CxxString>>;
+        #[namespace = "graphar_rs"]
+        fn graph_info_vertex_info_index(graph_info: &GraphInfo, type_: 
&CxxString) -> MaybeIndex;
+        #[namespace = "graphar_rs"]
+        fn graph_info_edge_info_index(
+            graph_info: &GraphInfo,
+            src_type: &CxxString,
+            edge_type: &CxxString,
+            dst_type: &CxxString,
+        ) -> MaybeIndex;
+    }
+
+    unsafe extern "C++" {
+        #[namespace = "graphar_rs"]
+        fn vertex_info_vec_push_vertex_info(
+            vertex_infos: Pin<&mut CxxVector<SharedVertexInfo>>,
+            vertex_info: SharedPtr<VertexInfo>,
+        );
+
+        #[namespace = "graphar_rs"]
+        fn edge_info_vec_push_edge_info(
+            edge_infos: Pin<&mut CxxVector<SharedEdgeInfo>>,
+            edge_info: SharedPtr<EdgeInfo>,
+        );
+    }
+
+    unsafe extern "C++" {
+        type SharedVertexInfo = crate::ffi::SharedVertexInfo;
+    }
+    impl CxxVector<SharedVertexInfo> {}
+
+    unsafe extern "C++" {
+        type SharedEdgeInfo = crate::ffi::SharedEdgeInfo;
+    }
+    impl CxxVector<SharedEdgeInfo> {}
+
     unsafe extern "C++" {
         type SharedPropertyGroup = crate::ffi::SharedPropertyGroup;
     }
@@ -358,3 +453,13 @@ pub(crate) mod graphar {
     }
     impl CxxVector<SharedAdjacentList> {}
 }
+
+impl From<MaybeIndex> for Option<usize> {
+    fn from(value: MaybeIndex) -> Self {
+        if value.has_value {
+            Some(value.index)
+        } else {
+            None
+        }
+    }
+}
diff --git a/rust/src/graphar_rs.cc b/rust/src/graphar_rs.cc
index 36c5c7da..0d9f68a9 100644
--- a/rust/src/graphar_rs.cc
+++ b/rust/src/graphar_rs.cc
@@ -17,8 +17,10 @@
  * under the License.
  */
 
-#include "graphar_rs.h"
+#include "graphar-rs/src/ffi.rs.h"
 
+#include <cstddef>
+#include <optional>
 #include <stdexcept>
 #include <utility>
 
@@ -157,6 +159,73 @@ std::shared_ptr<graphar::EdgeInfo> create_edge_info(
   return edge_info;
 }
 
+std::shared_ptr<graphar::GraphInfo> load_graph_info(const std::string& path) {
+  auto loaded = graphar::GraphInfo::Load(path);
+  if (!loaded) {
+    throw std::runtime_error(loaded.error().message());
+  }
+  return std::move(loaded).value();
+}
+
+std::shared_ptr<graphar::GraphInfo> create_graph_info(
+    const std::string& name,
+    const std::vector<graphar::SharedVertexInfo>& vertex_infos,
+    const std::vector<graphar::SharedEdgeInfo>& edge_infos,
+    const rust::Vec<rust::String>& labels, const std::string& prefix,
+    std::shared_ptr<graphar::ConstInfoVersion> version) {
+  if (name.empty()) {
+    throw std::runtime_error("CreateGraphInfo: name must not be empty");
+  }
+
+  std::vector<std::string> label_vec;
+  label_vec.reserve(labels.size());
+  for (size_t i = 0; i < labels.size(); ++i) {
+    label_vec.emplace_back(std::string(labels[i]));
+  }
+
+  auto graph_info = graphar::CreateGraphInfo(name, vertex_infos, edge_infos,
+                                             label_vec, prefix, version);
+  if (graph_info == nullptr) {
+    throw std::runtime_error("CreateGraphInfo: returned nullptr");
+  }
+  // if (!graph_info->IsValidated()) {
+  //   throw std::runtime_error("CreateGraphInfo: graph info is not 
validated");
+  // }
+  return graph_info;
+}
+
+static graphar::MaybeIndex optional_to_maybe_index(std::optional<size_t> opt) {
+  if (opt) {
+    return graphar::MaybeIndex{true, *opt};
+  } else {
+    return graphar::MaybeIndex{false, 0};
+  }
+}
+
+graphar::MaybeIndex graph_info_vertex_info_index(
+    const graphar::GraphInfo& graph_info, const std::string& type) {
+  return optional_to_maybe_index(graph_info.GetVertexInfoIndex(type));
+}
+
+graphar::MaybeIndex graph_info_edge_info_index(
+    const graphar::GraphInfo& graph_info, const std::string& src_type,
+    const std::string& edge_type, const std::string& dst_type) {
+  return optional_to_maybe_index(
+      graph_info.GetEdgeInfoIndex(src_type, edge_type, dst_type));
+}
+
+void vertex_info_vec_push_vertex_info(
+    std::vector<graphar::SharedVertexInfo>& vertex_infos,
+    std::shared_ptr<graphar::VertexInfo> vertex_info) {
+  vertex_infos.emplace_back(std::move(vertex_info));
+}
+
+void edge_info_vec_push_edge_info(
+    std::vector<graphar::SharedEdgeInfo>& edge_infos,
+    std::shared_ptr<graphar::EdgeInfo> edge_info) {
+  edge_infos.emplace_back(std::move(edge_info));
+}
+
 void vertex_info_save(const graphar::VertexInfo& vertex_info,
                       const std::string& path) {
   auto status = vertex_info.Save(path);
@@ -199,4 +268,21 @@ std::unique_ptr<std::string> edge_info_dump(
   }
   return std::make_unique<std::string>(std::move(r).value());
 }
+
+void graph_info_save(const graphar::GraphInfo& graph_info,
+                     const std::string& path) {
+  auto status = graph_info.Save(path);
+  if (!status.ok()) {
+    throw std::runtime_error(status.message());
+  }
+}
+
+std::unique_ptr<std::string> graph_info_dump(
+    const graphar::GraphInfo& graph_info) {
+  auto dumped = graph_info.Dump();
+  if (!dumped) {
+    throw std::runtime_error(dumped.error().message());
+  }
+  return std::make_unique<std::string>(std::move(dumped).value());
+}
 }  // namespace graphar_rs
diff --git a/rust/src/info/edge_info.rs b/rust/src/info/edge_info.rs
index 0ef70f53..af693c46 100644
--- a/rust/src/info/edge_info.rs
+++ b/rust/src/info/edge_info.rs
@@ -201,6 +201,8 @@ impl EdgeInfo {
 
     /// Return the number of property groups.
     ///
+    /// The upstream C++ API uses `int`; this binding returns `usize` for a
+    /// Rust-idiomatic count type.
     pub fn property_group_num(&self) -> usize {
         self.0.PropertyGroupNum()
     }
diff --git a/rust/src/info/graph_info.rs b/rust/src/info/graph_info.rs
new file mode 100644
index 00000000..e8c55ed7
--- /dev/null
+++ b/rust/src/info/graph_info.rs
@@ -0,0 +1,599 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! GraphAr graph-level metadata bindings.
+
+use std::path::Path;
+
+use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
+
+use crate::ffi;
+
+use super::{EdgeInfo, InfoVersion, VertexInfo};
+
+/// Graph-level metadata (`graphar::GraphInfo`).
+#[derive(Clone)]
+pub struct GraphInfo(pub(crate) SharedPtr<ffi::graphar::GraphInfo>);
+
+impl GraphInfo {
+    /// Create a builder for [`GraphInfo`].
+    pub fn builder(name: impl Into<String>) -> GraphInfoBuilder {
+        GraphInfoBuilder::new(name)
+    }
+
+    /// Create a new `GraphInfo`.
+    ///
+    /// Panics if GraphAr rejects the inputs. Prefer [`GraphInfo::try_new`] if
+    /// you want to handle errors.
+    pub fn new<N: AsRef<str>, P: AsRef<str>>(
+        name: N,
+        vertex_infos: Vec<VertexInfo>,
+        edge_infos: Vec<EdgeInfo>,
+        labels: Vec<String>,
+        prefix: P,
+        version: Option<InfoVersion>,
+    ) -> Self {
+        Self::try_new(name, vertex_infos, edge_infos, labels, prefix, 
version).unwrap()
+    }
+
+    /// Try to create a new `GraphInfo`.
+    pub fn try_new<N: AsRef<str>, P: AsRef<str>>(
+        name: N,
+        vertex_infos: Vec<VertexInfo>,
+        edge_infos: Vec<EdgeInfo>,
+        labels: Vec<String>,
+        prefix: P,
+        version: Option<InfoVersion>,
+    ) -> crate::Result<Self> {
+        let_cxx_string!(name = name.as_ref());
+        let_cxx_string!(prefix = prefix.as_ref());
+
+        let mut vertex_info_vec = CxxVector::new();
+        vertex_info_vec.pin_mut().reserve(vertex_infos.len());
+        for vertex_info in vertex_infos {
+            ffi::graphar::vertex_info_vec_push_vertex_info(
+                vertex_info_vec.pin_mut(),
+                vertex_info.0,
+            );
+        }
+        let vertex_info_vec_ref = vertex_info_vec
+            .as_ref()
+            .expect("vertex info vector should be valid");
+
+        let mut edge_info_vec: UniquePtr<CxxVector<ffi::SharedEdgeInfo>> = 
CxxVector::new();
+        edge_info_vec.pin_mut().reserve(edge_infos.len());
+        for edge_info in edge_infos {
+            
ffi::graphar::edge_info_vec_push_edge_info(edge_info_vec.pin_mut(), 
edge_info.0);
+        }
+        let edge_info_vec_ref = edge_info_vec
+            .as_ref()
+            .expect("edge info vector should be valid");
+
+        let version = version.map(|v| v.0).unwrap_or_else(SharedPtr::null);
+        let inner = ffi::graphar::create_graph_info(
+            &name,
+            vertex_info_vec_ref,
+            edge_info_vec_ref,
+            &labels,
+            &prefix,
+            version,
+        )?;
+        Ok(Self(inner))
+    }
+
+    /// Load graph metadata from a YAML file.
+    ///
+    /// Note: `path` must be valid UTF-8. On Unix, paths can contain arbitrary
+    /// bytes; such non-UTF8 paths return [`crate::Error::NonUtf8Path`].
+    pub fn load<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
+        let path_str = crate::path_to_utf8_str(path.as_ref())?;
+        let_cxx_string!(p = path_str);
+        Ok(Self(ffi::graphar::load_graph_info(&p)?))
+    }
+
+    /// Return the graph name.
+    pub fn name(&self) -> String {
+        self.0.GetName().to_string()
+    }
+
+    /// Return graph labels.
+    pub fn labels(&self) -> Vec<String> {
+        let labels = self.0.GetLabels();
+        let mut out = Vec::with_capacity(labels.len());
+        for label in labels {
+            out.push(label.to_string());
+        }
+        out
+    }
+
+    /// Return the logical prefix.
+    pub fn prefix(&self) -> String {
+        self.0.GetPrefix().to_string()
+    }
+
+    /// Return the optional info version.
+    pub fn version(&self) -> Option<InfoVersion> {
+        let sp = self.0.version();
+        if sp.is_null() {
+            None
+        } else {
+            Some(InfoVersion(sp.clone()))
+        }
+    }
+
+    /// Return the vertex info with the given type.
+    ///
+    /// Returns `None` if the type is not found.
+    pub fn vertex_info<S: AsRef<str>>(&self, r#type: S) -> Option<VertexInfo> {
+        let_cxx_string!(ty = r#type.as_ref());
+        let sp = self.0.GetVertexInfo(&ty);
+        if sp.is_null() {
+            None
+        } else {
+            Some(VertexInfo(sp))
+        }
+    }
+
+    /// Return the edge info with the given edge triplet.
+    ///
+    /// Returns `None` if the edge triplet is not found.
+    pub fn edge_info<S1: AsRef<str>, S2: AsRef<str>, S3: AsRef<str>>(
+        &self,
+        src_type: S1,
+        edge_type: S2,
+        dst_type: S3,
+    ) -> Option<EdgeInfo> {
+        let_cxx_string!(src = src_type.as_ref());
+        let_cxx_string!(edge = edge_type.as_ref());
+        let_cxx_string!(dst = dst_type.as_ref());
+        let sp = self.0.GetEdgeInfo(&src, &edge, &dst);
+        if sp.is_null() {
+            None
+        } else {
+            Some(EdgeInfo(sp))
+        }
+    }
+
+    /// Return the index of the vertex info with the given type.
+    ///
+    /// Returns `None` if the type is not found.
+    pub fn vertex_info_index<S: AsRef<str>>(&self, r#type: S) -> Option<usize> 
{
+        let_cxx_string!(ty = r#type.as_ref());
+        ffi::graphar::graph_info_vertex_info_index(&self.0, &ty).into()
+    }
+
+    /// Return the index of the edge info with the given edge triplet.
+    ///
+    /// Returns `None` if the edge triplet is not found.
+    pub fn edge_info_index<S1: AsRef<str>, S2: AsRef<str>, S3: AsRef<str>>(
+        &self,
+        src_type: S1,
+        edge_type: S2,
+        dst_type: S3,
+    ) -> Option<usize> {
+        let_cxx_string!(src = src_type.as_ref());
+        let_cxx_string!(edge = edge_type.as_ref());
+        let_cxx_string!(dst = dst_type.as_ref());
+        ffi::graphar::graph_info_edge_info_index(&self.0, &src, &edge, 
&dst).into()
+    }
+
+    /// Return the number of vertex infos.
+    pub fn vertex_info_num(&self) -> usize {
+        self.0.VertexInfoNum()
+    }
+
+    /// Return the number of edge infos.
+    pub fn edge_info_num(&self) -> usize {
+        self.0.EdgeInfoNum()
+    }
+
+    /// Return vertex infos.
+    ///
+    /// This allocates a new `Vec`. Prefer [`GraphInfo::vertex_infos_iter`]
+    /// if you only need to iterate.
+    pub fn vertex_infos(&self) -> Vec<VertexInfo> {
+        self.vertex_infos_iter().collect()
+    }
+
+    /// Iterate over vertex infos without allocating a `Vec`.
+    pub fn vertex_infos_iter(&self) -> impl Iterator<Item = VertexInfo> + '_ {
+        self.0
+            .GetVertexInfos()
+            .iter()
+            .map(|item| VertexInfo(item.0.clone()))
+    }
+
+    /// Return edge infos.
+    ///
+    /// This allocates a new `Vec`. Prefer [`GraphInfo::edge_infos_iter`]
+    /// if you only need to iterate.
+    pub fn edge_infos(&self) -> Vec<EdgeInfo> {
+        self.edge_infos_iter().collect()
+    }
+
+    /// Iterate over edge infos without allocating a `Vec`.
+    pub fn edge_infos_iter(&self) -> impl Iterator<Item = EdgeInfo> + '_ {
+        self.0
+            .GetEdgeInfos()
+            .iter()
+            .map(|item| EdgeInfo(item.0.clone()))
+    }
+
+    /// Save this `GraphInfo` to the given path.
+    ///
+    /// Note: `path` must be valid UTF-8. On Unix, paths can contain arbitrary
+    /// bytes; such non-UTF8 paths return [`crate::Error::NonUtf8Path`].
+    pub fn save<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
+        let path_str = crate::path_to_utf8_str(path.as_ref())?;
+        let_cxx_string!(p = path_str);
+        ffi::graphar::graph_info_save(&self.0, &p)?;
+        Ok(())
+    }
+
+    /// Dump this `GraphInfo` as a YAML string.
+    pub fn dump(&self) -> crate::Result<String> {
+        let dumped = ffi::graphar::graph_info_dump(&self.0)?;
+        let dumped = dumped.as_ref().expect("graph info dump should be valid");
+        Ok(dumped.to_string())
+    }
+}
+
+/// A builder for constructing a [`GraphInfo`].
+///
+/// Defaults:
+/// - `vertex_infos = []`
+/// - `edge_infos = []`
+/// - `labels = []`
+/// - `prefix = "./"` (matches upstream `graphar::GraphInfo` default and keeps 
the graph info validated; empty prefix is considered invalid upstream)
+/// - `version = None`
+pub struct GraphInfoBuilder {
+    name: String,
+    vertex_infos: Vec<VertexInfo>,
+    edge_infos: Vec<EdgeInfo>,
+    labels: Vec<String>,
+    prefix: String,
+    version: Option<InfoVersion>,
+}
+
+impl GraphInfoBuilder {
+    /// Create a new builder with the required graph name.
+    ///
+    /// The default `prefix` is `"./"` to match upstream GraphAr C++
+    /// `graphar::GraphInfo` behavior.
+    pub fn new(name: impl Into<String>) -> Self {
+        Self {
+            name: name.into(),
+            vertex_infos: Vec::new(),
+            edge_infos: Vec::new(),
+            labels: Vec::new(),
+            prefix: "./".to_string(),
+            version: None,
+        }
+    }
+
+    /// Push a single vertex info.
+    pub fn push_vertex_info(mut self, vertex_info: VertexInfo) -> Self {
+        self.vertex_infos.push(vertex_info);
+        self
+    }
+
+    /// Replace vertex infos with the given vector.
+    pub fn vertex_infos(mut self, vertex_infos: Vec<VertexInfo>) -> Self {
+        self.vertex_infos = vertex_infos;
+        self
+    }
+
+    /// Push a single edge info.
+    pub fn push_edge_info(mut self, edge_info: EdgeInfo) -> Self {
+        self.edge_infos.push(edge_info);
+        self
+    }
+
+    /// Replace edge infos with the given vector.
+    pub fn edge_infos(mut self, edge_infos: Vec<EdgeInfo>) -> Self {
+        self.edge_infos = edge_infos;
+        self
+    }
+
+    /// Set graph labels.
+    pub fn labels(mut self, labels: Vec<String>) -> Self {
+        self.labels = labels;
+        self
+    }
+
+    /// Set graph labels from a string iterator.
+    pub fn labels_from_iter<I, S>(mut self, labels: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<str>,
+    {
+        self.labels = labels.into_iter().map(|s| 
s.as_ref().to_string()).collect();
+        self
+    }
+
+    /// Push a single label.
+    pub fn push_label<S: Into<String>>(mut self, label: S) -> Self {
+        self.labels.push(label.into());
+        self
+    }
+
+    /// Set the logical prefix.
+    ///
+    /// If unset, [`GraphInfoBuilder`] uses `"./"` by default.
+    pub fn prefix<P: AsRef<str>>(mut self, prefix: P) -> Self {
+        self.prefix = prefix.as_ref().to_owned();
+        self
+    }
+
+    /// Set the info format version.
+    pub fn version(mut self, version: InfoVersion) -> Self {
+        self.version = Some(version);
+        self
+    }
+
+    /// Set the optional info format version.
+    pub fn version_opt(mut self, version: Option<InfoVersion>) -> Self {
+        self.version = version;
+        self
+    }
+
+    /// Build a [`GraphInfo`].
+    ///
+    /// Panics if GraphAr rejects the builder inputs. Prefer
+    /// [`GraphInfoBuilder::try_build`] if you want to handle errors.
+    pub fn build(self) -> GraphInfo {
+        self.try_build().unwrap()
+    }
+
+    /// Try to build a [`GraphInfo`].
+    pub fn try_build(self) -> crate::Result<GraphInfo> {
+        let Self {
+            name,
+            vertex_infos,
+            edge_infos,
+            labels,
+            prefix,
+            version,
+        } = self;
+        GraphInfo::try_new(name, vertex_infos, edge_infos, labels, prefix, 
version)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::info::{AdjListType, AdjacentList, AdjacentListVector};
+    use crate::property::{PropertyBuilder, PropertyGroup, PropertyGroupVector, 
PropertyVec};
+    use crate::types::{DataType, FileType};
+    use tempfile::tempdir;
+
+    fn make_vertex_info(r#type: &str) -> VertexInfo {
+        let mut props = PropertyVec::new();
+        props.emplace(PropertyBuilder::new("id", 
DataType::int64()).primary_key(true));
+        let group = PropertyGroup::new(props, FileType::Parquet, 
format!("{}_props/", r#type));
+        let mut groups = PropertyGroupVector::new();
+        groups.push(group);
+
+        VertexInfo::builder(r#type, 1024)
+            .property_groups(groups)
+            .labels_from_iter(["label"])
+            .prefix(format!("{}/", r#type))
+            .build()
+    }
+
+    fn make_edge_info(src_type: &str, edge_type: &str, dst_type: &str) -> 
EdgeInfo {
+        let mut adjacent_lists = AdjacentListVector::new();
+        adjacent_lists.push(AdjacentList::new(
+            AdjListType::OrderedBySource,
+            FileType::Parquet,
+            Some("ordered_by_source/"),
+        ));
+
+        let mut props = PropertyVec::new();
+        props.emplace(PropertyBuilder::new("weight", DataType::float64()));
+        let mut groups = PropertyGroupVector::new();
+        groups.push(PropertyGroup::new(props, FileType::Parquet, "weight/"));
+
+        EdgeInfo::builder(src_type, edge_type, dst_type, 1024, 100, 100)
+            .directed(true)
+            .adjacent_lists(adjacent_lists)
+            .property_groups(groups)
+            .prefix(format!("{src_type}_{edge_type}_{dst_type}/"))
+            .build()
+    }
+
+    #[test]
+    fn test_graph_info_builder_roundtrip_getters_and_lookups() {
+        let person = make_vertex_info("person");
+        let software = make_vertex_info("software");
+        let knows = make_edge_info("person", "knows", "person");
+
+        let graph_info = GraphInfo::builder("my_graph")
+            .push_vertex_info(person)
+            .push_vertex_info(software)
+            .push_edge_info(knows)
+            .labels(vec!["l0".to_string()])
+            .labels_from_iter(["l1", "l2"])
+            .push_label("l3")
+            .prefix("graph/")
+            .version(InfoVersion::new(1).unwrap())
+            .build();
+
+        assert_eq!(graph_info.name(), "my_graph");
+        assert_eq!(graph_info.prefix(), "graph/");
+        assert!(graph_info.version().is_some());
+
+        assert_eq!(
+            graph_info.labels(),
+            vec!["l1".to_string(), "l2".to_string(), "l3".to_string()]
+        );
+
+        assert_eq!(graph_info.vertex_info_num(), 2);
+        assert_eq!(graph_info.edge_info_num(), 1);
+        assert_eq!(graph_info.vertex_info_index("person"), Some(0));
+        assert_eq!(graph_info.vertex_info_index("software"), Some(1));
+        assert_eq!(graph_info.vertex_info_index("missing"), None);
+
+        assert_eq!(
+            graph_info.edge_info_index("person", "knows", "person"),
+            Some(0)
+        );
+        assert_eq!(
+            graph_info.edge_info_index("person", "unknown", "person"),
+            None
+        );
+
+        assert!(graph_info.vertex_info("person").is_some());
+        assert!(graph_info.vertex_info(String::from("software")).is_some());
+        assert!(graph_info.vertex_info("missing").is_none());
+
+        assert!(graph_info.edge_info("person", "knows", "person").is_some());
+        assert!(
+            graph_info
+                .edge_info("person", "missing", "person")
+                .is_none()
+        );
+
+        let vertex_infos = graph_info.vertex_infos();
+        assert_eq!(vertex_infos.len(), 2);
+        let vertex_infos_iter: Vec<_> = 
graph_info.vertex_infos_iter().collect();
+        assert_eq!(vertex_infos_iter.len(), 2);
+
+        let edge_infos = graph_info.edge_infos();
+        assert_eq!(edge_infos.len(), 1);
+        let edge_infos_iter: Vec<_> = graph_info.edge_infos_iter().collect();
+        assert_eq!(edge_infos_iter.len(), 1);
+    }
+
+    #[test]
+    fn test_graph_info_builder_replace_collections_and_version_opt() {
+        let person = make_vertex_info("person");
+        let software = make_vertex_info("software");
+        let knows = make_edge_info("person", "knows", "person");
+
+        let graph_info = GraphInfo::builder("my_graph")
+            .push_vertex_info(make_vertex_info("tmp"))
+            .vertex_infos(vec![person, software])
+            .push_edge_info(make_edge_info("tmp", "tmp", "tmp"))
+            .edge_infos(vec![knows])
+            .version_opt(None)
+            .build();
+
+        assert_eq!(graph_info.vertex_info_num(), 2);
+        assert_eq!(graph_info.edge_info_num(), 1);
+        assert!(graph_info.version().is_none());
+    }
+
+    #[test]
+    fn test_graph_info_builder_default_prefix() {
+        let graph_info = GraphInfo::builder("my_graph")
+            .push_vertex_info(make_vertex_info("person"))
+            .push_edge_info(make_edge_info("person", "knows", "person"))
+            .build();
+
+        assert_eq!(graph_info.prefix(), "./");
+    }
+
+    #[test]
+    fn test_graph_info_dump_save_and_load() {
+        let graph_info = GraphInfo::builder("my_graph")
+            .push_vertex_info(make_vertex_info("person"))
+            .push_edge_info(make_edge_info("person", "knows", "person"))
+            .prefix("graph/")
+            .labels_from_iter(["person"])
+            .build();
+
+        let dumped = graph_info.dump().unwrap();
+        assert!(!dumped.trim().is_empty(), "dumped={dumped:?}");
+        assert!(dumped.contains("name: my_graph"), "dumped={dumped:?}");
+        assert!(dumped.contains("prefix: graph/"), "dumped={dumped:?}");
+        assert!(
+            dumped.contains("person_knows_person.edge.yaml"),
+            "dumped={dumped:?}"
+        );
+        assert!(dumped.contains("person.vertex.yaml"), "dumped={dumped:?}");
+
+        let dir = tempdir().unwrap();
+        let graph_path = dir.path().join("my_graph.graph.yaml");
+        graph_info.save(&graph_path).unwrap();
+        graph_info
+            .vertex_info("person")
+            .unwrap()
+            .save(dir.path().join("person.vertex.yaml"))
+            .unwrap();
+        graph_info
+            .edge_info("person", "knows", "person")
+            .unwrap()
+            .save(dir.path().join("person_knows_person.edge.yaml"))
+            .unwrap();
+
+        let loaded = GraphInfo::load(&graph_path).unwrap();
+        assert_eq!(loaded.name(), "my_graph");
+        assert_eq!(loaded.prefix(), "graph/");
+        assert_eq!(loaded.vertex_info_num(), 1);
+        assert_eq!(loaded.edge_info_num(), 1);
+        assert!(loaded.vertex_info("person").is_some());
+        assert!(loaded.edge_info("person", "knows", "person").is_some());
+    }
+
+    #[test]
+    fn test_graph_info_try_new_error_paths() {
+        let err = GraphInfo::try_new("", vec![], vec![], vec![], "graph/", 
None)
+            .err()
+            .unwrap();
+        let msg = err.to_string();
+        assert!(
+            msg.contains("CreateGraphInfo") && msg.contains("name must not be 
empty"),
+            "unexpected error message: {msg:?}"
+        );
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_graph_info_new_panics_on_invalid_args() {
+        let _ = GraphInfo::new("", vec![], vec![], vec![], "graph/", None);
+    }
+
+    #[cfg(unix)]
+    #[test]
+    fn test_graph_info_save_and_load_non_utf8_path() {
+        use std::os::unix::ffi::OsStringExt;
+
+        let graph_info = GraphInfo::builder("my_graph")
+            .push_vertex_info(make_vertex_info("person"))
+            .push_edge_info(make_edge_info("person", "knows", "person"))
+            .build();
+
+        let dir = tempdir().unwrap();
+        let mut path = dir.path().to_path_buf();
+        path.push(std::ffi::OsString::from_vec(
+            b"graph_info_\xFF_non_utf8.yaml".to_vec(),
+        ));
+
+        let err = graph_info.save(&path).err().unwrap();
+        assert!(
+            matches!(err, crate::Error::NonUtf8Path(_)),
+            "unexpected error: {err:?}"
+        );
+
+        let err = GraphInfo::load(&path).err().unwrap();
+        assert!(
+            matches!(err, crate::Error::NonUtf8Path(_)),
+            "unexpected error: {err:?}"
+        );
+    }
+}
diff --git a/rust/src/info/mod.rs b/rust/src/info/mod.rs
index 3e032f9e..2a6cb072 100644
--- a/rust/src/info/mod.rs
+++ b/rust/src/info/mod.rs
@@ -19,6 +19,7 @@
 
 mod adjacent_list;
 mod edge_info;
+mod graph_info;
 mod version;
 mod vertex_info;
 
@@ -27,5 +28,6 @@ pub use crate::ffi::graphar::AdjListType;
 
 pub use adjacent_list::{AdjacentList, AdjacentListVector};
 pub use edge_info::{EdgeInfo, EdgeInfoBuilder};
+pub use graph_info::{GraphInfo, GraphInfoBuilder};
 pub use version::InfoVersion;
 pub use vertex_info::{VertexInfo, VertexInfoBuilder};
diff --git a/rust/src/info/vertex_info.rs b/rust/src/info/vertex_info.rs
index 10b93ad9..df098fb1 100644
--- a/rust/src/info/vertex_info.rs
+++ b/rust/src/info/vertex_info.rs
@@ -122,6 +122,8 @@ impl VertexInfo {
 
     /// Return the number of property groups.
     ///
+    /// The upstream C++ API uses `int`; this binding returns `usize` for a
+    /// Rust-idiomatic count type.
     pub fn property_group_num(&self) -> usize {
         self.0.PropertyGroupNum()
     }
@@ -169,7 +171,6 @@ impl VertexInfo {
     /// If you only need a borrowed reference and want bounds checking, prefer
     /// [`VertexInfo::property_groups_cxx`] and `cxx::CxxVector::get`, or
     /// [`VertexInfo::property_groups_iter`] with `nth`.
-    ///
     /// Returns `None` if the index is out of range.
     pub fn property_group_by_index(&self, index: usize) -> 
Option<PropertyGroup> {
         let sp = self.0.GetPropertyGroupByIndex(index);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to