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 878aa5c9 feat(Rust): add `VertexInfo` support (#835)
878aa5c9 is described below

commit 878aa5c9cbb244843b69f4820b7c648f89aa0a49
Author: Jinye Wu <[email protected]>
AuthorDate: Tue Jan 27 11:21:32 2026 +0800

    feat(Rust): add `VertexInfo` support (#835)
    
    * update
    
    * update
    
    * update
    
    * update
---
 rust/Cargo.lock                  | 106 +++++++++
 rust/Cargo.toml                  |   3 +
 rust/include/graphar_rs.h        |  22 ++
 rust/src/ffi.rs                  |  57 +++++
 rust/src/graphar_rs.cc           |  62 +++++
 rust/src/{lib.rs => info/mod.rs} |  19 +-
 rust/src/info/version.rs         |  53 +++++
 rust/src/info/vertex_info.rs     | 496 +++++++++++++++++++++++++++++++++++++++
 rust/src/lib.rs                  |   2 +
 rust/src/property.rs             |  93 +++++++-
 10 files changed, 896 insertions(+), 17 deletions(-)

diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 4317f99b..9faaed42 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -8,6 +8,12 @@ version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
 
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
 [[package]]
 name = "cc"
 version = "1.2.49"
@@ -18,6 +24,12 @@ dependencies = [
  "shlex",
 ]
 
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
 [[package]]
 name = "clap"
 version = "4.5.53"
@@ -132,6 +144,22 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
 [[package]]
 name = "find-msvc-tools"
 version = "0.1.5"
@@ -144,6 +172,18 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
 
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
 [[package]]
 name = "graphar-rs"
 version = "0.1.0"
@@ -151,6 +191,7 @@ dependencies = [
  "cmake",
  "cxx",
  "cxx-build",
+ "tempfile",
 ]
 
 [[package]]
@@ -169,6 +210,12 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "libc"
+version = "0.2.180"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
+
 [[package]]
 name = "link-cplusplus"
 version = "1.0.12"
@@ -178,6 +225,18 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.103"
@@ -196,6 +255,25 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rustix"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
 [[package]]
 name = "scratch"
 version = "1.0.9"
@@ -255,6 +333,19 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.4.1"
@@ -276,6 +367,15 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
 
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
 [[package]]
 name = "winapi-util"
 version = "0.1.11"
@@ -299,3 +399,9 @@ checksum = 
"ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
 dependencies = [
  "windows-link",
 ]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 71b9f07b..ba5c9108 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -28,3 +28,6 @@ cxx = "1.0.190"
 [build-dependencies]
 cxx-build = "1.0.190"
 cmake = "0.1.54"
+
+[dev-dependencies]
+tempfile = "3"
diff --git a/rust/include/graphar_rs.h b/rust/include/graphar_rs.h
index f04ae645..d7f313fb 100644
--- a/rust/include/graphar_rs.h
+++ b/rust/include/graphar_rs.h
@@ -19,6 +19,7 @@
 
 #pragma once
 
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <vector>
@@ -26,15 +27,19 @@
 #include "graphar/fwd.h"
 #include "graphar/graph_info.h"
 #include "graphar/types.h"
+#include "graphar/version_parser.h"
 #include "rust/cxx.h"
 
 namespace graphar {
 using SharedPropertyGroup = std::shared_ptr<PropertyGroup>;
+using ConstInfoVersion = const InfoVersion;
 }
 
 namespace graphar_rs {
 rust::String to_type_name(const graphar::DataType &type);
 
+std::shared_ptr<graphar::ConstInfoVersion> new_const_info_version(int32_t 
version);
+
 std::unique_ptr<graphar::Property>
 new_property(const std::string &name, std::shared_ptr<graphar::DataType> type,
              bool is_primary, bool is_nullable,
@@ -56,7 +61,24 @@ void 
property_vec_emplace_property(std::vector<graphar::Property> &properties,
                                    bool is_primary, bool is_nullable,
                                    graphar::Cardinality cardinality);
 
+std::unique_ptr<std::vector<graphar::Property>>
+property_vec_clone(const std::vector<graphar::Property> &properties);
+
 void property_group_vec_push_property_group(
     std::vector<graphar::SharedPropertyGroup> &property_groups,
     std::shared_ptr<graphar::PropertyGroup> property_group);
+
+std::unique_ptr<std::vector<graphar::SharedPropertyGroup>>
+property_group_vec_clone(
+    const std::vector<graphar::SharedPropertyGroup> &property_groups);
+
+std::shared_ptr<graphar::VertexInfo>
+create_vertex_info(const std::string &type, graphar::IdType chunk_size,
+                   const std::vector<graphar::SharedPropertyGroup> 
&property_groups,
+                   const rust::Vec<rust::String> &labels,
+                   const std::string &prefix,
+                   std::shared_ptr<graphar::ConstInfoVersion> version);
+
+void vertex_info_save(const graphar::VertexInfo &vertex_info, const 
std::string &path);
+std::unique_ptr<std::string> vertex_info_dump(const graphar::VertexInfo 
&vertex_info);
 } // namespace graphar_rs
diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs
index a649f3d5..d6b1e0a1 100644
--- a/rust/src/ffi.rs
+++ b/rust/src/ffi.rs
@@ -19,6 +19,7 @@ use cxx::{ExternType, SharedPtr};
 
 /// A shared pointer wrapper for `graphar::PropertyGroup`.
 #[repr(transparent)]
+#[derive(Clone)]
 pub struct SharedPropertyGroup(pub(crate) SharedPtr<graphar::PropertyGroup>);
 
 unsafe impl ExternType for SharedPropertyGroup {
@@ -173,6 +174,9 @@ pub(crate) mod graphar {
             is_nullable: bool,
             cardinality: Cardinality,
         );
+
+        #[namespace = "graphar_rs"]
+        fn property_vec_clone(properties: &CxxVector<Property>) -> 
UniquePtr<CxxVector<Property>>;
     }
 
     // `PropertyGroup`
@@ -193,6 +197,59 @@ pub(crate) mod graphar {
             property_groups: Pin<&mut CxxVector<SharedPropertyGroup>>,
             property_group: SharedPtr<PropertyGroup>,
         );
+
+        #[namespace = "graphar_rs"]
+        fn property_group_vec_clone(
+            property_groups: &CxxVector<SharedPropertyGroup>,
+        ) -> UniquePtr<CxxVector<SharedPropertyGroup>>;
+    }
+
+    // `InfoVersion`
+    //
+    // TODO: upstream C++ APIs still use `int` in a few places for versioning;
+    // prefer fixed-width integer types in the public C++ interface.
+    unsafe extern "C++" {
+        type InfoVersion;
+        type ConstInfoVersion;
+
+        #[namespace = "graphar_rs"]
+        fn new_const_info_version(version: i32) -> 
Result<SharedPtr<ConstInfoVersion>>;
+    }
+
+    // `VertexInfo`
+    unsafe extern "C++" {
+        type VertexInfo;
+
+        fn GetType(&self) -> &CxxString;
+        fn GetChunkSize(&self) -> i64;
+        fn GetPrefix(&self) -> &CxxString;
+        fn version(&self) -> &SharedPtr<ConstInfoVersion>;
+        fn GetLabels(&self) -> &CxxVector<CxxString>;
+
+        // TODO: upstream C++ uses `int` for this return type; prefer 
fixed-width.
+        fn PropertyGroupNum(&self) -> i32;
+
+        fn GetPropertyGroups(&self) -> &CxxVector<SharedPropertyGroup>;
+        fn GetPropertyGroup(&self, property_name: &CxxString) -> 
SharedPtr<PropertyGroup>;
+
+        // TODO: upstream C++ uses `int` for this parameter; prefer 
fixed-width.
+        fn GetPropertyGroupByIndex(&self, index: i32) -> 
SharedPtr<PropertyGroup>;
+
+        #[namespace = "graphar_rs"]
+        fn create_vertex_info(
+            type_: &CxxString,
+            chunk_size: i64,
+            property_groups: &CxxVector<SharedPropertyGroup>,
+            labels: &Vec<String>,
+            prefix: &CxxString,
+            version: SharedPtr<ConstInfoVersion>,
+        ) -> Result<SharedPtr<VertexInfo>>;
+
+        #[namespace = "graphar_rs"]
+        fn vertex_info_save(vertex_info: &VertexInfo, path: &CxxString) -> 
Result<()>;
+
+        #[namespace = "graphar_rs"]
+        fn vertex_info_dump(vertex_info: &VertexInfo) -> 
Result<UniquePtr<CxxString>>;
     }
 
     unsafe extern "C++" {
diff --git a/rust/src/graphar_rs.cc b/rust/src/graphar_rs.cc
index 15c138cc..d806ef99 100644
--- a/rust/src/graphar_rs.cc
+++ b/rust/src/graphar_rs.cc
@@ -19,6 +19,7 @@
 
 #include "graphar_rs.h"
 
+#include <stdexcept>
 #include <utility>
 
 namespace graphar_rs {
@@ -26,6 +27,12 @@ rust::String to_type_name(const graphar::DataType &type) {
   return rust::String(type.ToTypeName());
 }
 
+std::shared_ptr<graphar::ConstInfoVersion>
+new_const_info_version(int32_t version) {
+  // Let any upstream exceptions propagate to Rust via `cxx::Exception`.
+  return std::make_shared<graphar::InfoVersion>(static_cast<int>(version));
+}
+
 std::unique_ptr<graphar::Property>
 new_property(const std::string &name, std::shared_ptr<graphar::DataType> type,
              bool is_primary, bool is_nullable,
@@ -67,9 +74,64 @@ void 
property_vec_emplace_property(std::vector<graphar::Property> &properties,
   properties.emplace_back(name, type, is_primary, is_nullable, cardinality);
 }
 
+std::unique_ptr<std::vector<graphar::Property>>
+property_vec_clone(const std::vector<graphar::Property> &properties) {
+  return std::make_unique<std::vector<graphar::Property>>(properties);
+}
+
 void property_group_vec_push_property_group(
     std::vector<graphar::SharedPropertyGroup> &property_groups,
     std::shared_ptr<graphar::PropertyGroup> property_group) {
   property_groups.emplace_back(std::move(property_group));
 }
+
+std::unique_ptr<std::vector<graphar::SharedPropertyGroup>>
+property_group_vec_clone(
+    const std::vector<graphar::SharedPropertyGroup> &property_groups) {
+  return std::make_unique<std::vector<graphar::SharedPropertyGroup>>(
+      property_groups);
+}
+
+std::shared_ptr<graphar::VertexInfo> create_vertex_info(
+    const std::string &type, graphar::IdType chunk_size,
+    const std::vector<graphar::SharedPropertyGroup> &property_groups,
+    const rust::Vec<rust::String> &labels, const std::string &prefix,
+    std::shared_ptr<graphar::ConstInfoVersion> version) {
+  if (type.empty()) {
+    throw std::runtime_error("CreateVertexInfo: type must not be empty");
+  }
+  if (chunk_size <= 0) {
+    throw std::runtime_error("CreateVertexInfo: chunk_size must be > 0");
+  }
+
+  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 vertex_info = graphar::CreateVertexInfo(type, chunk_size, 
property_groups,
+                                               label_vec, prefix, 
std::move(version));
+  if (vertex_info == nullptr) {
+    throw std::runtime_error("CreateVertexInfo: returned nullptr");
+  }
+  return vertex_info;
+}
+
+void vertex_info_save(const graphar::VertexInfo &vertex_info,
+                      const std::string &path) {
+  auto status = vertex_info.Save(path);
+  if (!status.ok()) {
+    throw std::runtime_error(status.message());
+  }
+}
+
+std::unique_ptr<std::string>
+vertex_info_dump(const graphar::VertexInfo &vertex_info) {
+  auto dumped = vertex_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/lib.rs b/rust/src/info/mod.rs
similarity index 73%
copy from rust/src/lib.rs
copy to rust/src/info/mod.rs
index 6f6bcc73..e46a62a7 100644
--- a/rust/src/lib.rs
+++ b/rust/src/info/mod.rs
@@ -15,19 +15,10 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! Rust bindings for GraphAr.
+//! Graph metadata bindings.
 
-#![deny(missing_docs)]
+mod version;
+mod vertex_info;
 
-use cxx::CxxString;
-
-mod ffi;
-
-/// GraphAr property.
-pub mod property;
-/// GraphAr logical data types.
-pub mod types;
-
-fn cxx_string_to_string(value: &CxxString) -> String {
-    String::from_utf8_lossy(value.as_bytes()).into_owned()
-}
+pub use version::InfoVersion;
+pub use vertex_info::{VertexInfo, VertexInfoBuilder};
diff --git a/rust/src/info/version.rs b/rust/src/info/version.rs
new file mode 100644
index 00000000..b10b30f5
--- /dev/null
+++ b/rust/src/info/version.rs
@@ -0,0 +1,53 @@
+// 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 info version bindings.
+
+use crate::ffi;
+use cxx::SharedPtr;
+
+/// A GraphAr `InfoVersion` value.
+///
+/// This is a thin wrapper around `std::shared_ptr<const 
graphar::InfoVersion>`.
+#[derive(Clone)]
+pub struct InfoVersion(pub(crate) SharedPtr<ffi::graphar::ConstInfoVersion>);
+
+impl InfoVersion {
+    /// Create a new `InfoVersion` by version number.
+    ///
+    /// TODO: upstream C++ constructor takes `int`; prefer fixed-width integer 
types.
+    pub fn new(version: i32) -> Result<Self, cxx::Exception> {
+        Ok(Self(ffi::graphar::new_const_info_version(version)?))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_info_version_new_ok_and_err() {
+        let v1 = InfoVersion::new(1).unwrap();
+        let _v1_clone = v1.clone();
+
+        // Use an obviously invalid version to keep this test future-proof.
+        // Upstream may add support for version 2.
+        let err = InfoVersion::new(-1).err().unwrap();
+        let msg = err.to_string();
+        assert!(!msg.is_empty(), "unexpected empty error message");
+    }
+}
diff --git a/rust/src/info/vertex_info.rs b/rust/src/info/vertex_info.rs
new file mode 100644
index 00000000..11d9a714
--- /dev/null
+++ b/rust/src/info/vertex_info.rs
@@ -0,0 +1,496 @@
+// 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.
+
+//! Vertex metadata bindings.
+
+use super::version::InfoVersion;
+use crate::{cxx_string_to_string, ffi, property::PropertyGroup, 
property::PropertyGroupVector};
+use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
+use std::borrow::Cow;
+use std::path::Path;
+
+/// GraphAr vertex metadata (`graphar::VertexInfo`).
+#[derive(Clone)]
+pub struct VertexInfo(pub(crate) SharedPtr<ffi::graphar::VertexInfo>);
+
+impl VertexInfo {
+    /// Create a builder for [`VertexInfo`].
+    ///
+    /// This is the preferred API when constructing `VertexInfo` in Rust, since
+    /// the raw constructor has many parameters.
+    pub fn builder<S: Into<String>>(
+        r#type: S,
+        chunk_size: i64,
+        property_groups: PropertyGroupVector,
+    ) -> VertexInfoBuilder {
+        VertexInfoBuilder::new(r#type, chunk_size, property_groups)
+    }
+
+    /// Create a new `VertexInfo`.
+    ///
+    /// The `prefix` is a logical prefix string used by GraphAr (it is not a
+    /// filesystem path).
+    ///
+    /// Panics if GraphAr rejects the inputs (including, but not limited to,
+    /// `type` being empty or `chunk_size <= 0`). Prefer 
[`VertexInfo::try_new`]
+    /// if you want to handle errors.
+    pub fn new<S: AsRef<[u8]>, P: AsRef<[u8]>>(
+        r#type: S,
+        chunk_size: i64,
+        property_groups: PropertyGroupVector,
+        labels: Vec<String>,
+        prefix: P,
+        version: Option<InfoVersion>,
+    ) -> Self {
+        Self::try_new(r#type, chunk_size, property_groups, labels, prefix, 
version).unwrap()
+    }
+
+    /// Try to create a new `VertexInfo`.
+    ///
+    /// This returns an error if `type` is empty, `chunk_size <= 0`, or if the
+    /// upstream GraphAr implementation rejects the inputs.
+    pub fn try_new<S: AsRef<[u8]>, P: AsRef<[u8]>>(
+        r#type: S,
+        chunk_size: i64,
+        property_groups: PropertyGroupVector,
+        labels: Vec<String>,
+        prefix: P,
+        version: Option<InfoVersion>,
+    ) -> Result<Self, cxx::Exception> {
+        let_cxx_string!(ty = r#type.as_ref());
+        let_cxx_string!(prefix = prefix);
+
+        let groups_ref = property_groups
+            .as_ref()
+            .expect("property group vec should be valid");
+        let version = version.map(|v| v.0).unwrap_or_else(SharedPtr::null);
+
+        Ok(Self(ffi::graphar::create_vertex_info(
+            &ty, chunk_size, groups_ref, &labels, &prefix, version,
+        )?))
+    }
+
+    /// Return the vertex type name.
+    pub fn type_name(&self) -> String {
+        cxx_string_to_string(self.0.GetType())
+    }
+
+    /// Return the chunk size.
+    pub fn chunk_size(&self) -> i64 {
+        self.0.GetChunkSize()
+    }
+
+    /// Return the logical prefix.
+    pub fn prefix(&self) -> String {
+        cxx_string_to_string(self.0.GetPrefix())
+    }
+
+    /// Return the optional format version.
+    pub fn version(&self) -> Option<InfoVersion> {
+        let sp = self.0.version();
+        if sp.is_null() {
+            None
+        } else {
+            Some(InfoVersion(sp.clone()))
+        }
+    }
+
+    /// Return the labels of this vertex type.
+    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(cxx_string_to_string(label));
+        }
+        out
+    }
+
+    /// Return the underlying label vector.
+    ///
+    /// This is an advanced API that exposes `cxx` types and ties the returned
+    /// reference to the lifetime of `self`.
+    pub fn labels_cxx(&self) -> &CxxVector<cxx::CxxString> {
+        self.0.GetLabels()
+    }
+
+    /// Return the number of property groups.
+    ///
+    /// TODO: upstream C++ uses `int` for this return type; prefer fixed-width.
+    pub fn property_group_num(&self) -> i32 {
+        self.0.PropertyGroupNum()
+    }
+
+    /// Return property groups.
+    ///
+    /// This is an advanced API that exposes `cxx` types and ties the returned
+    /// reference to the lifetime of `self`.
+    pub fn property_groups_cxx(&self) -> &CxxVector<PropertyGroup> {
+        self.0.GetPropertyGroups()
+    }
+
+    /// Return property groups.
+    ///
+    /// This allocates a new `Vec`. Prefer [`VertexInfo::property_groups_iter`]
+    /// if you only need to iterate.
+    pub fn property_groups(&self) -> Vec<PropertyGroup> {
+        self.property_groups_iter().collect()
+    }
+
+    /// Iterate over property groups without allocating a `Vec`.
+    pub fn property_groups_iter(&self) -> impl Iterator<Item = PropertyGroup> 
+ '_ {
+        self.0.GetPropertyGroups().iter().cloned()
+    }
+
+    /// Return the property group containing the given property.
+    ///
+    /// Returns `None` if the property is not found.
+    pub fn property_group<S: AsRef<[u8]>>(&self, property_name: S) -> 
Option<PropertyGroup> {
+        let_cxx_string!(name = property_name);
+
+        let sp = self.0.GetPropertyGroup(&name);
+        if sp.is_null() {
+            None
+        } else {
+            Some(PropertyGroup::from_inner(sp))
+        }
+    }
+
+    /// Return the property group at the given index.
+    ///
+    /// This returns an owned [`PropertyGroup`] (backed by a C++ `shared_ptr`)
+    /// without allocating a `Vec`.
+    ///
+    /// 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`.
+    /// TODO: upstream C++ uses `int` for this parameter; prefer fixed-width.
+    ///
+    /// Returns `None` if the index is out of range.
+    pub fn property_group_by_index(&self, index: i32) -> Option<PropertyGroup> 
{
+        let sp = self.0.GetPropertyGroupByIndex(index);
+        if sp.is_null() {
+            None
+        } else {
+            Some(PropertyGroup::from_inner(sp))
+        }
+    }
+
+    /// Save this `VertexInfo` to the given path.
+    ///
+    /// On Unix, this passes the raw `OsStr` bytes to C++ to avoid lossy UTF-8
+    /// conversion. On non-Unix platforms, this falls back to converting the
+    /// path to UTF-8 using [`Path::to_string_lossy`].
+    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), cxx::Exception> {
+        let path = path.as_ref();
+
+        #[cfg(unix)]
+        let path_bytes: Cow<[u8]> = {
+            use std::os::unix::ffi::OsStrExt;
+            Cow::Borrowed(path.as_os_str().as_bytes())
+        };
+
+        #[cfg(not(unix))]
+        let path_bytes: Cow<[u8]> = 
Cow::Owned(path.to_string_lossy().into_owned().into_bytes());
+
+        let_cxx_string!(p = path_bytes.as_ref());
+        ffi::graphar::vertex_info_save(&self.0, &p)?;
+        Ok(())
+    }
+
+    /// Dump this `VertexInfo` as YAML string.
+    pub fn dump(&self) -> Result<String, cxx::Exception> {
+        let dumped: UniquePtr<cxx::CxxString> = 
ffi::graphar::vertex_info_dump(&self.0)?;
+        Ok(dumped.to_string())
+    }
+}
+
+/// A builder for constructing a [`VertexInfo`].
+///
+/// Defaults:
+/// - `labels = []`
+/// - `prefix = ""` (GraphAr may set a default prefix based on type)
+/// - `version = None`
+pub struct VertexInfoBuilder {
+    r#type: String,
+    chunk_size: i64,
+    property_groups: PropertyGroupVector,
+    labels: Vec<String>,
+    prefix: Vec<u8>,
+    version: Option<InfoVersion>,
+}
+
+impl VertexInfoBuilder {
+    /// Create a new builder with required fields.
+    pub fn new<S: Into<String>>(
+        r#type: S,
+        chunk_size: i64,
+        property_groups: PropertyGroupVector,
+    ) -> Self {
+        Self {
+            r#type: r#type.into(),
+            chunk_size,
+            property_groups,
+            labels: Vec::new(),
+            prefix: Vec::new(),
+            version: None,
+        }
+    }
+
+    /// Set vertex labels.
+    pub fn labels(mut self, labels: Vec<String>) -> Self {
+        self.labels = labels;
+        self
+    }
+
+    /// Set vertex 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.
+    pub fn prefix<P: AsRef<[u8]>>(mut self, prefix: P) -> Self {
+        self.prefix = prefix.as_ref().to_vec();
+        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 [`VertexInfo`].
+    ///
+    /// Panics if GraphAr rejects the builder inputs. Prefer 
[`VertexInfoBuilder::try_build`]
+    /// if you want to handle errors.
+    pub fn build(self) -> VertexInfo {
+        self.try_build().unwrap()
+    }
+
+    /// Try to build a [`VertexInfo`].
+    pub fn try_build(self) -> Result<VertexInfo, cxx::Exception> {
+        let Self {
+            r#type,
+            chunk_size,
+            property_groups,
+            labels,
+            prefix,
+            version,
+        } = self;
+
+        VertexInfo::try_new(r#type, chunk_size, property_groups, labels, 
prefix, version)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::property::{PropertyBuilder, PropertyGroup, PropertyVec};
+    use crate::types::{DataType, FileType};
+    use tempfile::tempdir;
+
+    fn cxx_string_to_string_for_test(s: &cxx::CxxString) -> String {
+        String::from_utf8_lossy(s.as_bytes()).into_owned()
+    }
+
+    fn make_property_groups() -> PropertyGroupVector {
+        let mut props = PropertyVec::new();
+        props.emplace(PropertyBuilder::new("id", 
DataType::int64()).primary_key(true));
+        props.emplace(PropertyBuilder::new("name", DataType::string()));
+
+        let pg = PropertyGroup::new(props, FileType::Parquet, "id_name/");
+        let mut groups = PropertyGroupVector::new();
+        groups.push(pg);
+        groups
+    }
+
+    #[test]
+    fn test_vertex_info_try_new_error_paths() {
+        let groups = PropertyGroupVector::new();
+
+        // type cannot be empty
+        let msg = VertexInfo::try_new("", 1, groups.clone(), vec![], "", None)
+            .err()
+            .unwrap()
+            .to_string();
+        assert!(
+            msg.contains("CreateVertexInfo") && msg.contains("type must not be 
empty"),
+            "unexpected error message: {msg:?}"
+        );
+
+        // `chunk_size` cannot be less than 1
+        let msg = VertexInfo::try_new("person", 0, groups.clone(), vec![], "", 
None)
+            .err()
+            .unwrap()
+            .to_string();
+        assert!(
+            msg.contains("CreateVertexInfo") && msg.contains("chunk_size must 
be > 0"),
+            "unexpected error message: {msg:?}"
+        );
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_vertex_info_new_panics_on_invalid_args() {
+        let groups = PropertyGroupVector::new();
+        // type cannot be empty
+        let _ = VertexInfo::new("", 1, groups, vec![], "", None);
+    }
+
+    #[test]
+    fn test_vertex_info_builder() {
+        let version = InfoVersion::new(1).unwrap();
+        let groups = make_property_groups();
+
+        // Create `VertexInfo` using builder API.
+        let vertex_info = VertexInfo::builder("person", 1024, groups)
+            .labels(vec!["l0".to_string()])
+            .labels_from_iter(["l1", "l2"])
+            .push_label("l3")
+            .prefix("person/")
+            .version(version)
+            .build();
+
+        assert_eq!(vertex_info.type_name(), "person");
+        assert_eq!(vertex_info.chunk_size(), 1024);
+        assert_eq!(vertex_info.prefix(), "person/");
+
+        assert!(vertex_info.version().is_some());
+
+        let labels = vertex_info.labels();
+        assert_eq!(
+            labels,
+            vec!["l1".to_string(), "l2".to_string(), "l3".to_string()]
+        );
+
+        let labels_cxx = vertex_info.labels_cxx();
+        assert_eq!(labels_cxx.len(), 3);
+        assert_eq!(
+            cxx_string_to_string_for_test(labels_cxx.get(0).unwrap()),
+            "l1"
+        );
+
+        assert_eq!(vertex_info.property_group_num(), 1);
+
+        let groups_cxx = vertex_info.property_groups_cxx();
+        assert_eq!(groups_cxx.len(), 1);
+        assert!(groups_cxx.get(0).unwrap().has_property("id"));
+
+        let groups_vec = vertex_info.property_groups();
+        assert_eq!(groups_vec.len(), 1);
+
+        let groups_iter: Vec<_> = vertex_info.property_groups_iter().collect();
+        assert_eq!(groups_iter.len(), 1);
+    }
+
+    #[test]
+    fn test_vertex_info_property_group_lookups() {
+        let groups = make_property_groups();
+
+        let vertex_info = VertexInfo::builder("person", 1024, groups)
+            .prefix("person/")
+            .version_opt(None)
+            .build();
+
+        assert!(vertex_info.version().is_none());
+
+        assert!(vertex_info.property_group("id").is_some());
+        assert!(vertex_info.property_group("missing").is_none());
+
+        let by_index = vertex_info.property_group_by_index(0).unwrap();
+        assert!(by_index.has_property("id"));
+
+        assert!(vertex_info.property_group_by_index(1).is_none());
+        assert!(vertex_info.property_group_by_index(-1).is_none());
+    }
+
+    #[test]
+    fn test_vertex_info_dump_and_save() {
+        let groups = make_property_groups();
+
+        let vertex_info = VertexInfo::builder("person", 1024, groups)
+            .labels_from_iter(["l1"])
+            .prefix("person/")
+            .build();
+
+        let dumped = vertex_info.dump().unwrap();
+        assert!(!dumped.trim().is_empty(), "dumped={dumped:?}");
+        assert!(dumped.contains("person"), "dumped={dumped:?}");
+        assert!(dumped.contains("person/"), "dumped={dumped:?}");
+        assert!(dumped.contains("l1"), "dumped={dumped:?}");
+        assert!(dumped.contains("1024"), "dumped={dumped:?}");
+
+        let dir = tempdir().unwrap();
+        let path = dir.path().join("vertex_info.yaml");
+        vertex_info.save(&path).unwrap();
+
+        let metadata = std::fs::metadata(&path).unwrap();
+        assert!(metadata.is_file());
+        assert!(metadata.len() > 0);
+
+        let saved = std::fs::read_to_string(&path).unwrap();
+        assert!(!saved.trim().is_empty(), "saved={saved:?}");
+        assert!(saved.contains("person"), "saved={saved:?}");
+        assert!(saved.contains("person/"), "saved={saved:?}");
+        assert!(saved.contains("l1"), "saved={saved:?}");
+        assert!(saved.contains("1024"), "saved={saved:?}");
+    }
+
+    #[cfg(unix)]
+    #[test]
+    fn test_vertex_info_save_non_utf8_path() {
+        use std::os::unix::ffi::OsStringExt;
+
+        let groups = make_property_groups();
+        let vertex_info = VertexInfo::builder("person", 1024, groups)
+            .labels_from_iter(["l1"])
+            .prefix("person/")
+            .build();
+
+        let dir = tempdir().unwrap();
+
+        let mut path = dir.path().to_path_buf();
+        path.push(std::ffi::OsString::from_vec(
+            b"vertex_info_\xFF_non_utf8.yaml".to_vec(),
+        ));
+
+        std::fs::File::create(&path).unwrap();
+        std::fs::remove_file(&path).unwrap();
+
+        vertex_info.save(&path).unwrap();
+        let metadata = std::fs::metadata(&path).unwrap();
+        assert!(metadata.is_file());
+        assert!(metadata.len() > 0);
+    }
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 6f6bcc73..2c2d21d3 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -23,6 +23,8 @@ use cxx::CxxString;
 
 mod ffi;
 
+/// GraphAr metadata.
+pub mod info;
 /// GraphAr property.
 pub mod property;
 /// GraphAr logical data types.
diff --git a/rust/src/property.rs b/rust/src/property.rs
index eae7cc9f..c33f8d86 100644
--- a/rust/src/property.rs
+++ b/rust/src/property.rs
@@ -162,6 +162,13 @@ impl Default for PropertyVec {
     }
 }
 
+impl Clone for PropertyVec {
+    fn clone(&self) -> Self {
+        let src = self.0.as_ref().expect("properties vec should be valid");
+        Self(ffi::graphar::property_vec_clone(src))
+    }
+}
+
 impl Deref for PropertyVec {
     type Target = UniquePtr<CxxVector<ffi::graphar::Property>>;
 
@@ -213,9 +220,13 @@ impl PropertyVec {
 }
 
 /// A group of properties stored in the same file(s).
-pub struct PropertyGroup(SharedPtr<ffi::graphar::PropertyGroup>);
+pub type PropertyGroup = ffi::SharedPropertyGroup;
 
 impl PropertyGroup {
+    pub(crate) fn from_inner(inner: SharedPtr<ffi::graphar::PropertyGroup>) -> 
Self {
+        Self(inner)
+    }
+
     /// Create a new property group.
     ///
     /// The `prefix` is a logical prefix string used by GraphAr (it is not a
@@ -250,7 +261,7 @@ impl PropertyGroup {
 }
 
 /// A vector of property groups.
-pub struct PropertyGroupVector(UniquePtr<CxxVector<ffi::SharedPropertyGroup>>);
+pub struct PropertyGroupVector(UniquePtr<CxxVector<PropertyGroup>>);
 
 impl Default for PropertyGroupVector {
     fn default() -> Self {
@@ -258,8 +269,15 @@ impl Default for PropertyGroupVector {
     }
 }
 
+impl Clone for PropertyGroupVector {
+    fn clone(&self) -> Self {
+        let src = self.0.as_ref().expect("property group vec should be valid");
+        Self(ffi::graphar::property_group_vec_clone(src))
+    }
+}
+
 impl Deref for PropertyGroupVector {
-    type Target = UniquePtr<CxxVector<ffi::SharedPropertyGroup>>;
+    type Target = UniquePtr<CxxVector<PropertyGroup>>;
 
     fn deref(&self) -> &Self::Target {
         &self.0
@@ -429,4 +447,73 @@ mod tests {
         vec.push(pg2);
         assert_eq!(vec.deref().len(), 2);
     }
+
+    fn make_property_vec_for_clone() -> PropertyVec {
+        let mut props = PropertyVec::new();
+        props.emplace(PropertyBuilder::new("id", 
DataType::int64()).primary_key(true));
+        props.push(Property::new(
+            "name",
+            DataType::string(),
+            false,
+            true,
+            Cardinality::Single,
+        ));
+        props
+    }
+
+    #[test]
+    fn test_property_vec_clone_independent_container() {
+        let mut original = make_property_vec_for_clone();
+        let cloned = original.clone();
+
+        assert_eq!(original.len(), 2);
+        assert_eq!(cloned.len(), 2);
+
+        let id_prop = Property::new("id2", DataType::int64(), true, true, 
Cardinality::Single);
+        original.push(id_prop);
+        assert_eq!(original.len(), 3);
+
+        // Mutating the original container should not affect the cloned one.
+        assert_eq!(cloned.len(), 2);
+
+        let pg = PropertyGroup::new(cloned, FileType::Parquet, "clone_check/");
+        let mut names: Vec<_> = pg.properties().into_iter().map(|p| 
p.name()).collect();
+        names.sort();
+        assert_eq!(names, vec!["id".to_string(), "name".to_string()]);
+    }
+
+    #[test]
+    fn test_property_group_vector_clone_independent_container() {
+        let mut props1 = PropertyVec::new();
+        props1.emplace(PropertyBuilder::new("id1", 
DataType::int64()).primary_key(true));
+        let pg1 = PropertyGroup::new(props1, FileType::Parquet, "pg1/");
+
+        let mut props2 = PropertyVec::new();
+        props2.emplace(PropertyBuilder::new("id2", 
DataType::int64()).primary_key(true));
+        let pg2 = PropertyGroup::new(props2, FileType::Parquet, "pg2/");
+
+        let mut groups = PropertyGroupVector::new();
+        groups.push(pg1);
+        groups.push(pg2);
+
+        let cloned = groups.clone();
+        assert_eq!(groups.len(), 2);
+        assert_eq!(cloned.len(), 2);
+
+        assert!(cloned.get(0).unwrap().has_property("id1"));
+        assert!(cloned.get(1).unwrap().has_property("id2"));
+
+        let mut props3 = PropertyVec::new();
+        props3.emplace(PropertyBuilder::new("id3", 
DataType::int64()).primary_key(true));
+        let pg3 = PropertyGroup::new(props3, FileType::Parquet, "pg3/");
+        groups.push(pg3);
+
+        assert_eq!(groups.len(), 3);
+        assert_eq!(cloned.len(), 2);
+
+        let cloned_props = cloned.get(0).unwrap().properties();
+        assert_eq!(cloned_props.len(), 1);
+        assert_eq!(cloned_props[0].name(), "id1");
+        assert_eq!(cloned_props[0].data_type().id(), Type::Int64);
+    }
 }


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


Reply via email to