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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1dfc7e6bd feat(bindings/cpp): expose all api returned by value (#3001)
1dfc7e6bd is described below

commit 1dfc7e6bddff054628d9d91d740af4ac02896016
Author: Mingzhuo Yin <[email protected]>
AuthorDate: Sat Sep 2 21:34:12 2023 +0800

    feat(bindings/cpp): expose all api returned by value (#3001)
    
    * feat(bindings/cpp): expose all api returned by value
    
    Signed-off-by: silver-ymz <[email protected]>
    
    * add docs-only option to avoid install dependency in docs ci
    
    Signed-off-by: silver-ymz <[email protected]>
    
    * typo
    
    Signed-off-by: silver-ymz <[email protected]>
    
    * update impl to optionalstring
    
    Signed-off-by: silver-ymz <[email protected]>
    
    ---------
    
    Signed-off-by: silver-ymz <[email protected]>
---
 .github/workflows/bindings_cpp.yml |  18 +++--
 .github/workflows/docs.yml         |  10 +--
 Cargo.lock                         |   1 +
 bindings/cpp/CMakeLists.txt        |  55 ++++++++-------
 bindings/cpp/CONTRIBUTING.md       |  18 ++---
 bindings/cpp/Cargo.toml            |   1 +
 bindings/cpp/include/opendal.hpp   |  97 +++++++++++++++++++++++++-
 bindings/cpp/src/lib.rs            | 136 +++++++++++++++++++++++++++++++++++--
 bindings/cpp/src/opendal.cpp       |  94 ++++++++++++++++++++-----
 bindings/cpp/tests/basic_test.cpp  |  50 ++++++++++++--
 10 files changed, 399 insertions(+), 81 deletions(-)

diff --git a/.github/workflows/bindings_cpp.yml 
b/.github/workflows/bindings_cpp.yml
index 03cad7633..f619e65eb 100644
--- a/.github/workflows/bindings_cpp.yml
+++ b/.github/workflows/bindings_cpp.yml
@@ -45,25 +45,23 @@ jobs:
       - uses: actions/checkout@v3
       - name: Install dependencies
         run: |
-          sudo apt-get install libgtest-dev ninja-build
-          cd /usr/src/gtest
-          sudo cmake CMakeLists.txt
-          sudo make
-          sudo cp lib/*.a /usr/lib
-          sudo ln -s /usr/lib/libgtest.a /usr/local/lib/libgtest.a
-          sudo ln -s /usr/lib/libgtest_main.a /usr/local/lib/libgtest_main.a
+          sudo apt-get install libgtest-dev ninja-build libboost-all-dev 
valgrind doxygen
 
       - name: Setup Rust toolchain
         uses: ./.github/actions/setup
 
-      - name: Build Cpp binding
+      - name: Build Cpp binding && Run tests
         working-directory: "bindings/cpp"
         run: |
           mkdir build
           cd build
           cmake -GNinja ..
           ninja
+          ./opendal_cpp_test
 
-      - name: Run tests
+      - name: Run tests with valgrind
         working-directory: "bindings/cpp/build"
-        run: ./opendal_cpp_test
+        run: |
+          cmake -GNinja -DENABLE_ADDRESS_SANITIZER=OFF ..
+          ninja
+          valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes 
--verbose ./opendal_cpp_test
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 35765dcaa..e4e46d42a 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -232,20 +232,14 @@ jobs:
 
       - name: Install dependencies
         run: | 
-          sudo apt-get install doxygen graphviz ninja-build libgtest-dev
-          cd /usr/src/gtest
-          sudo cmake CMakeLists.txt
-          sudo make
-          sudo cp lib/*.a /usr/lib
-          sudo ln -s /usr/lib/libgtest.a /usr/local/lib/libgtest.a
-          sudo ln -s /usr/lib/libgtest_main.a /usr/local/lib/libgtest_main.a
+          sudo apt-get install doxygen graphviz ninja-build
 
       - name: Build Cpp docs
         working-directory: "bindings/cpp"
         run: |
           mkdir build
           cd build
-          cmake -GNinja ..
+          cmake -GNinja -DDOCS_ONLY=ON ..
           ninja docs
 
       - name: Upload docs
diff --git a/Cargo.lock b/Cargo.lock
index 8fe3a49f3..6cf5fc04d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3599,6 +3599,7 @@ name = "opendal-cpp"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "chrono",
  "cxx",
  "cxx-build",
  "opendal",
diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt
index ebffe0d5a..06e89f993 100644
--- a/bindings/cpp/CMakeLists.txt
+++ b/bindings/cpp/CMakeLists.txt
@@ -25,6 +25,27 @@ if (NOT CMAKE_BUILD_TYPE)
     set(CMAKE_BUILD_TYPE Debug)
 endif()
 
+option(ENABLE_ADDRESS_SANITIZER "Enable address sanitizer" ON)
+option(DOCS_ONLY "Only build documentation" OFF)
+
+# Documentation
+set(PROJECT_DOCUMENT_SOURCE ${CMAKE_SOURCE_DIR}/include 
${CMAKE_SOURCE_DIR}/README.md)
+string(REPLACE ";" " " PROJECT_DOCUMENT_SOURCE "${PROJECT_DOCUMENT_SOURCE}")
+file(DOWNLOAD 
https://cdn.jsdelivr.net/gh/jothepro/[email protected]/doxygen-awesome.min.css
 ${CMAKE_BINARY_DIR}/doxygen-awesome.css)
+find_package(Doxygen REQUIRED)
+set(DOXYGEN_IN ${CMAKE_SOURCE_DIR}/Doxyfile)
+set(DOXYGEN_OUT ${CMAKE_BINARY_DIR}/Doxyfile.out)
+configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
+add_custom_target(docs
+    COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    COMMENT "Generating API documentation with Doxygen"
+    VERBATIM)
+
+if (DOCS_ONLY)
+    return()
+endif()
+
 # cargo target dir must be absolute, otherwise some build target cannot find it
 get_filename_component(CARGO_TARGET_DIR ${CMAKE_SOURCE_DIR}/../../target 
ABSOLUTE)
 set(CARGO_MANIFEST ${CMAKE_SOURCE_DIR}/Cargo.toml)
@@ -42,9 +63,12 @@ add_custom_command(
         COMMENT "Running cargo..."
 )
 
+find_package(Boost REQUIRED COMPONENTS date_time)
+
 add_library(opendal_cpp STATIC ${CPP_SOURCE_FILE} ${RUST_BRIDGE_CPP})
-target_include_directories(opendal_cpp PUBLIC ${CPP_INCLUDE_DIR})
-target_link_libraries(opendal_cpp PUBLIC ${RUST_LIB} ${CMAKE_DL_LIBS})
+target_include_directories(opendal_cpp PUBLIC ${CPP_INCLUDE_DIR} 
Boost::date_time)
+target_link_libraries(opendal_cpp PUBLIC ${RUST_LIB})
+target_link_libraries(opendal_cpp PRIVATE ${CMAKE_DL_LIBS} Boost::date_time)
 set_target_properties(opendal_cpp
         PROPERTIES ADDITIONAL_CLEAN_FILES ${CARGO_TARGET_DIR}
 )
@@ -71,7 +95,9 @@ target_link_libraries(opendal_cpp_test ${GTEST_LDFLAGS} 
GTest::gtest_main openda
 target_compile_options(opendal_cpp_test PRIVATE ${GTEST_CFLAGS})
 
 # enable address sanitizers
-if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES 
"GNU")
+if (ENABLE_ADDRESS_SANITIZER)
+    target_compile_options(opendal_cpp PRIVATE 
-fsanitize=leak,address,undefined -fno-omit-frame-pointer -fno-common -O1)
+    target_link_options(opendal_cpp PRIVATE -fsanitize=leak,address,undefined)
     target_compile_options(opendal_cpp_test PRIVATE 
-fsanitize=leak,address,undefined -fno-omit-frame-pointer -fno-common -O1)
     target_link_options(opendal_cpp_test PRIVATE 
-fsanitize=leak,address,undefined)
 endif()
@@ -85,25 +111,4 @@ if(APPLE)
 endif()
 
 include(GoogleTest)
-gtest_discover_tests(opendal_cpp_test)
-
-# Documentation
-set(PROJECT_DOCUMENT_SOURCE ${CMAKE_SOURCE_DIR}/include 
${CMAKE_SOURCE_DIR}/README.md)
-string(REPLACE ";" " " PROJECT_DOCUMENT_SOURCE "${PROJECT_DOCUMENT_SOURCE}")
-file(DOWNLOAD 
https://cdn.jsdelivr.net/gh/jothepro/[email protected]/doxygen-awesome.min.css
 ${CMAKE_BINARY_DIR}/doxygen-awesome.css)
-find_package(Doxygen)
-if (DOXYGEN_FOUND)
-    set(DOXYGEN_IN ${CMAKE_SOURCE_DIR}/Doxyfile)
-    set(DOXYGEN_OUT ${CMAKE_BINARY_DIR}/Doxyfile.out)
-
-    configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
-    message("Doxygen build started")
-
-    add_custom_target(docs
-        COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
-        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-        COMMENT "Generating API documentation with Doxygen"
-        VERBATIM )
-else (DOXYGEN_FOUND)
-    message("Doxygen need to be installed to generate the doxygen 
documentation")
-endif (DOXYGEN_FOUND)
\ No newline at end of file
+gtest_discover_tests(opendal_cpp_test)
\ No newline at end of file
diff --git a/bindings/cpp/CONTRIBUTING.md b/bindings/cpp/CONTRIBUTING.md
index a6238d7b5..c76dcda44 100644
--- a/bindings/cpp/CONTRIBUTING.md
+++ b/bindings/cpp/CONTRIBUTING.md
@@ -32,6 +32,8 @@ To build OpenDAL C++ binding, the following is all you need:
 
 - **GTest(Google Test)**. It is used to run the tests. To see how to build, 
check [here](https://github.com/google/googletest).
 
+- **Boost**. It is one dependency of this library. To see how to build, check 
[here](https://www.boost.org/doc/libs/1_76_0/more/getting_started/unix-variants.html).
+
 - **Doxygen**. It is used to generate the documentation. To see how to build, 
check [here](https://www.doxygen.nl/manual/install.html).
 
 - **Graphviz**. It is used to generate the documentation with graphs. To see 
how to build, check [here](https://graphviz.org/download/).
@@ -48,14 +50,11 @@ sudo apt install cmake ninja-build
 # install clang-format
 sudo apt install clang-format
 
-# install and build GTest library under /usr/lib and softlink to /usr/local/lib
+# install GTest library
 sudo apt-get install libgtest-dev
-cd /usr/src/gtest
-sudo cmake CMakeLists.txt
-sudo make
-sudo cp lib/*.a /usr/lib
-sudo ln -s /usr/lib/libgtest.a /usr/local/lib/libgtest.a
-sudo ln -s /usr/lib/libgtest_main.a /usr/local/lib/libgtest_main.a
+
+# install Boost library
+sudo apt install libboost-all-dev
 
 # install Doxygen and Graphviz
 sudo apt install doxygen graphviz
@@ -76,6 +75,9 @@ brew install clang-format
 # install GTest library
 brew install googletest
 
+# install Boost library
+brew install boost
+
 # install Doxygen and Graphviz
 brew install doxygen graphviz
 ```
@@ -89,7 +91,7 @@ mkdir build
 cd build
 
 # Add -DCMAKE_EXPORT_COMPILE_COMMANDS=1 to generate compile_commands.json for 
clangd
-cmake -DCMAKE_BUILD_TYPE=Debug -GNinja .. 
+cmake -GNinja .. 
 
 ninja
 ```
diff --git a/bindings/cpp/Cargo.toml b/bindings/cpp/Cargo.toml
index 55fcce62f..21f890f0c 100644
--- a/bindings/cpp/Cargo.toml
+++ b/bindings/cpp/Cargo.toml
@@ -34,6 +34,7 @@ crate-type = ["staticlib"]
 opendal.workspace = true
 cxx = "1.0"
 anyhow = "1.0"
+chrono = "0.4"
 
 [build-dependencies]
 cxx-build = "1.0"
diff --git a/bindings/cpp/include/opendal.hpp b/bindings/cpp/include/opendal.hpp
index 22efc5046..0a13030c5 100644
--- a/bindings/cpp/include/opendal.hpp
+++ b/bindings/cpp/include/opendal.hpp
@@ -20,6 +20,7 @@
 #pragma once
 #include "lib.rs.h"
 
+#include <boost/date_time/posix_time/posix_time.hpp>
 #include <memory>
 #include <optional>
 #include <string>
@@ -28,11 +29,48 @@
 
 namespace opendal {
 
+/**
+ * @enum EntryMode
+ * @brief The mode of the entry
+ */
+enum EntryMode {
+  FILE = 1,
+  DIR = 2,
+  UNKNOWN = 0,
+};
+
+/**
+ * @struct Metadata
+ * @brief The metadata of a file or directory
+ */
+struct Metadata {
+  EntryMode type;
+  std::uint64_t content_length;
+  std::optional<std::string> cache_control;
+  std::optional<std::string> content_disposition;
+  std::optional<std::string> content_md5;
+  std::optional<std::string> content_type;
+  std::optional<std::string> etag;
+  std::optional<boost::posix_time::ptime> last_modified;
+
+  Metadata(ffi::Metadata &&);
+};
+
+/**
+ * @struct Entry
+ * @brief The entry of a file or directory
+ */
+struct Entry {
+  std::string path;
+
+  Entry(ffi::Entry &&);
+};
+
 /**
  * @class Operator
  * @brief Operator is the entry for all public APIs.
  */
-class Operator : std::enable_shared_from_this<Operator> {
+class Operator {
 public:
   Operator() = default;
 
@@ -63,6 +101,8 @@ public:
 
   /**
    * @brief Read data from the operator
+   * @note The operation will make unnecessary copy. So we recommend to use the
+   * `reader` method.
    *
    * @param path The path of the data
    * @return The data read from the operator
@@ -77,6 +117,61 @@ public:
    */
   void write(std::string_view path, const std::vector<uint8_t> &data);
 
+  /**
+   * @brief Check if the path exists
+   *
+   * @param path The path to check
+   * @return true if the path exists, false otherwise
+   */
+  bool is_exist(std::string_view path);
+
+  /**
+   * @brief Create a directory
+   *
+   * @param path The path of the directory
+   */
+  void create_dir(std::string_view path);
+
+  /**
+   * @brief Copy a file from src to dst.
+   *
+   * @param src The source path
+   * @param dst The destination path
+   */
+  void copy(std::string_view src, std::string_view dst);
+
+  /**
+   * @brief Rename a file from src to dst.
+   *
+   * @param src The source path
+   * @param dst The destination path
+   */
+  void rename(std::string_view src, std::string_view dst);
+
+  /**
+   * @brief Remove a file or directory
+   *
+   * @param path The path of the file or directory
+   */
+  void remove(std::string_view path);
+
+  /**
+   * @brief Get the metadata of a file or directory
+   *
+   * @param path The path of the file or directory
+   * @return The metadata of the file or directory
+   */
+  Metadata stat(std::string_view path);
+
+  /**
+   * @brief List the entries of a directory
+   * @note The returned entries are sorted by name.
+   *
+   * @param path The path of the directory
+   * @return The entries of the directory
+   */
+  std::vector<Entry> list(std::string_view path);
+
 private:
   std::optional<rust::Box<opendal::ffi::Operator>> operator_;
 };
diff --git a/bindings/cpp/src/lib.rs b/bindings/cpp/src/lib.rs
index ff9f68e12..66498d9e7 100644
--- a/bindings/cpp/src/lib.rs
+++ b/bindings/cpp/src/lib.rs
@@ -17,7 +17,6 @@
 
 use anyhow::Result;
 use opendal as od;
-use std::collections::HashMap;
 use std::str::FromStr;
 
 #[cxx::bridge(namespace = "opendal::ffi")]
@@ -27,12 +26,45 @@ mod ffi {
         value: String,
     }
 
+    enum EntryMode {
+        File = 1,
+        Dir = 2,
+        Unknown = 0,
+    }
+
+    struct OptionalString {
+        has_value: bool,
+        value: String,
+    }
+
+    struct Metadata {
+        mode: EntryMode,
+        content_length: u64,
+        cache_control: OptionalString,
+        content_disposition: OptionalString,
+        content_md5: OptionalString,
+        content_type: OptionalString,
+        etag: OptionalString,
+        last_modified: OptionalString,
+    }
+
+    struct Entry {
+        path: String,
+    }
+
     extern "Rust" {
         type Operator;
 
         fn new_operator(scheme: &str, configs: Vec<HashMapValue>) -> 
Result<Box<Operator>>;
-        fn read(&self, path: &str) -> Result<Vec<u8>>;
-        fn write(&self, path: &str, bs: &[u8]) -> Result<()>;
+        fn read(self: &Operator, path: &str) -> Result<Vec<u8>>;
+        fn write(self: &Operator, path: &str, bs: &'static [u8]) -> Result<()>;
+        fn is_exist(self: &Operator, path: &str) -> Result<bool>;
+        fn create_dir(self: &Operator, path: &str) -> Result<()>;
+        fn copy(self: &Operator, src: &str, dst: &str) -> Result<()>;
+        fn rename(self: &Operator, src: &str, dst: &str) -> Result<()>;
+        fn remove(self: &Operator, path: &str) -> Result<()>;
+        fn stat(self: &Operator, path: &str) -> Result<Metadata>;
+        fn list(self: &Operator, path: &str) -> Result<Vec<Entry>>;
     }
 }
 
@@ -44,7 +76,7 @@ fn new_operator(scheme: &str, configs: 
Vec<ffi::HashMapValue>) -> Result<Box<Ope
     let map = configs
         .into_iter()
         .map(|value| (value.key, value.value))
-        .collect::<HashMap<_, _>>();
+        .collect();
 
     let op = Box::new(Operator(od::Operator::via_map(scheme, 
map)?.blocking()));
 
@@ -56,7 +88,99 @@ impl Operator {
         Ok(self.0.read(path)?)
     }
 
-    fn write(&self, path: &str, bs: &[u8]) -> Result<()> {
-        Ok(self.0.write(path, bs.to_owned())?)
+    // To avoid copying the bytes, we use &'static [u8] here.
+    //
+    // Safety: The bytes created from bs will be dropped after the function 
call.
+    // So it's safe to declare its lifetime as 'static.
+    fn write(&self, path: &str, bs: &'static [u8]) -> Result<()> {
+        Ok(self.0.write(path, bs)?)
+    }
+
+    fn is_exist(&self, path: &str) -> Result<bool> {
+        Ok(self.0.is_exist(path)?)
+    }
+
+    fn create_dir(&self, path: &str) -> Result<()> {
+        Ok(self.0.create_dir(path)?)
+    }
+
+    fn copy(&self, src: &str, dst: &str) -> Result<()> {
+        Ok(self.0.copy(src, dst)?)
+    }
+
+    fn rename(&self, src: &str, dst: &str) -> Result<()> {
+        Ok(self.0.rename(src, dst)?)
+    }
+
+    // We can't name it to delete because it's a keyword in C++
+    fn remove(&self, path: &str) -> Result<()> {
+        Ok(self.0.delete(path)?)
+    }
+
+    fn stat(&self, path: &str) -> Result<ffi::Metadata> {
+        Ok(self.0.stat(path)?.into())
+    }
+
+    fn list(&self, path: &str) -> Result<Vec<ffi::Entry>> {
+        Ok(self.0.list(path)?.into_iter().map(Into::into).collect())
+    }
+}
+
+impl From<od::Metadata> for ffi::Metadata {
+    fn from(meta: od::Metadata) -> Self {
+        let mode = meta.mode().into();
+        let content_length = meta.content_length();
+        let cache_control = meta.cache_control().map(ToOwned::to_owned).into();
+        let content_disposition = 
meta.content_disposition().map(ToOwned::to_owned).into();
+        let content_md5 = meta.content_md5().map(ToOwned::to_owned).into();
+        let content_type = meta.content_type().map(ToOwned::to_owned).into();
+        let etag = meta.etag().map(ToOwned::to_owned).into();
+        let last_modified = meta
+            .last_modified()
+            .map(|time| time.to_rfc3339_opts(chrono::SecondsFormat::Nanos, 
false))
+            .into();
+
+        Self {
+            mode,
+            content_length,
+            cache_control,
+            content_disposition,
+            content_md5,
+            content_type,
+            etag,
+            last_modified,
+        }
+    }
+}
+
+impl From<od::Entry> for ffi::Entry {
+    fn from(entry: od::Entry) -> Self {
+        let (path, _) = entry.into_parts();
+        Self { path }
+    }
+}
+
+impl From<od::EntryMode> for ffi::EntryMode {
+    fn from(mode: od::EntryMode) -> Self {
+        match mode {
+            od::EntryMode::FILE => Self::File,
+            od::EntryMode::DIR => Self::Dir,
+            _ => Self::Unknown,
+        }
+    }
+}
+
+impl From<Option<String>> for ffi::OptionalString {
+    fn from(s: Option<String>) -> Self {
+        match s {
+            Some(s) => Self {
+                has_value: true,
+                value: s,
+            },
+            None => Self {
+                has_value: false,
+                value: String::default(),
+            },
+        }
     }
 }
diff --git a/bindings/cpp/src/opendal.cpp b/bindings/cpp/src/opendal.cpp
index 49a1fa227..c0b4a3d90 100644
--- a/bindings/cpp/src/opendal.cpp
+++ b/bindings/cpp/src/opendal.cpp
@@ -21,37 +21,97 @@
 
 using namespace opendal;
 
+#define RUST_STR(s) rust::Str(s.data(), s.size())
+#define RUST_STRING(s) rust::String(s.data(), s.size())
+
 Operator::Operator(std::string_view scheme,
                    const std::unordered_map<std::string, std::string> &config) 
{
   auto rust_map = rust::Vec<ffi::HashMapValue>();
   rust_map.reserve(config.size());
-  for (const auto &[k, v] : config) {
-    rust_map.push_back(ffi::HashMapValue{
-        rust::String(k.data()),
-        rust::String(v.data()),
-    });
+  for (auto &[k, v] : config) {
+    rust_map.push_back({RUST_STRING(k), RUST_STRING(v)});
   }
 
-  operator_ = opendal::ffi::new_operator(rust::Str(scheme.data()), rust_map);
+  operator_ = opendal::ffi::new_operator(RUST_STR(scheme), rust_map);
 }
 
 bool Operator::available() const { return operator_.has_value(); }
 
+// We can't avoid copy, because std::vector hides the internal structure.
+// std::vector doesn't support init from a pointer without copy.
 std::vector<uint8_t> Operator::read(std::string_view path) {
-  auto rust_vec = operator_.value()->read(rust::Str(path.data()));
-
-  // Convert rust::Vec<uint8_t> to std::vector<uint8_t>
-  // This cannot use rust vector pointer to init std::vector because
-  // rust::Vec owns the memory and will free it when it goes out of scope.
-  std::vector<uint8_t> res;
-  res.reserve(rust_vec.size());
-  std::copy(rust_vec.cbegin(), rust_vec.cend(), std::back_inserter(res));
+  auto rust_vec = operator_.value()->read(RUST_STR(path));
 
-  return res;
+  return {rust_vec.data(), rust_vec.data() + rust_vec.size()};
 }
 
 void Operator::write(std::string_view path, const std::vector<uint8_t> &data) {
   operator_.value()->write(
-      rust::Str(path.data()),
-      rust::Slice<const uint8_t>(data.data(), data.size()));
+      RUST_STR(path), rust::Slice<const uint8_t>(data.data(), data.size()));
+}
+
+bool Operator::is_exist(std::string_view path) {
+  return operator_.value()->is_exist(RUST_STR(path));
+}
+
+void Operator::create_dir(std::string_view path) {
+  operator_.value()->create_dir(RUST_STR(path));
+}
+
+void Operator::copy(std::string_view src, std::string_view dst) {
+  operator_.value()->copy(RUST_STR(src), RUST_STR(dst));
+}
+
+void Operator::rename(std::string_view src, std::string_view dst) {
+  operator_.value()->rename(RUST_STR(src), RUST_STR(dst));
+}
+
+void Operator::remove(std::string_view path) {
+  operator_.value()->remove(RUST_STR(path));
+}
+
+Metadata Operator::stat(std::string_view path) {
+  return {operator_.value()->stat(RUST_STR(path))};
+}
+
+std::vector<Entry> Operator::list(std::string_view path) {
+  auto rust_vec = operator_.value()->list(RUST_STR(path));
+
+  std::vector<Entry> entries;
+  entries.reserve(rust_vec.size());
+  for (auto &&entry : rust_vec) {
+    entries.push_back(std::move(entry));
+  }
+  return entries;
+}
+
+std::optional<std::string> parse_optional_string(ffi::OptionalString &&s);
+
+Metadata::Metadata(ffi::Metadata &&other) {
+  type = static_cast<EntryMode>(other.mode);
+  content_length = other.content_length;
+  cache_control = parse_optional_string(std::move(other.cache_control));
+  content_disposition =
+      parse_optional_string(std::move(other.content_disposition));
+  content_type = parse_optional_string(std::move(other.content_type));
+  content_md5 = parse_optional_string(std::move(other.content_md5));
+  etag = parse_optional_string(std::move(other.etag));
+  auto last_modified_str =
+      parse_optional_string(std::move(other.last_modified));
+  if (last_modified_str.has_value()) {
+    last_modified =
+        boost::posix_time::from_iso_string(last_modified_str.value());
+  }
+}
+
+Entry::Entry(ffi::Entry &&other) : path(std::move(other.path)) {}
+
+// helper functions
+
+std::optional<std::string> parse_optional_string(ffi::OptionalString &&s) {
+  if (s.has_value) {
+    return std::string(std::move(s.value));
+  } else {
+    return std::nullopt;
+  }
 }
\ No newline at end of file
diff --git a/bindings/cpp/tests/basic_test.cpp 
b/bindings/cpp/tests/basic_test.cpp
index 32811b75f..d86cb4fd7 100644
--- a/bindings/cpp/tests/basic_test.cpp
+++ b/bindings/cpp/tests/basic_test.cpp
@@ -19,6 +19,7 @@
 
 #include "opendal.hpp"
 #include "gtest/gtest.h"
+#include <optional>
 #include <string>
 #include <unordered_map>
 
@@ -30,22 +31,59 @@ protected:
   std::unordered_map<std::string, std::string> config;
 
   void SetUp() override {
-    this->scheme = "memory";
-    op = opendal::Operator(this->scheme, this->config);
+    scheme = "memory";
+    op = opendal::Operator(scheme, config);
 
-    EXPECT_TRUE(this->op.available());
+    EXPECT_TRUE(op.available());
   }
 };
 
 // Scenario: OpenDAL Blocking Operations
 TEST_F(OpendalTest, BasicTest) {
-  std::string path = "test";
+  std::string file_path = "test";
+  std::string file_path_copied = "test_copied";
+  std::string file_path_renamed = "test_renamed";
+  std::string dir_path = "test_dir/";
   std::vector<uint8_t> data = {1, 2, 3, 4, 5};
 
-  op.write("test", data);
+  // write
+  op.write(file_path, data);
 
-  auto res = op.read("test");
+  // read
+  auto res = op.read(file_path);
   EXPECT_EQ(res, data);
+
+  // is_exist
+  EXPECT_TRUE(op.is_exist(file_path));
+
+  // create_dir
+  op.create_dir(dir_path);
+  EXPECT_TRUE(op.is_exist(dir_path));
+
+  // copy
+  op.copy(file_path, file_path_copied);
+  EXPECT_TRUE(op.is_exist(file_path_copied));
+
+  // rename
+  op.rename(file_path_copied, file_path_renamed);
+  EXPECT_TRUE(op.is_exist(file_path_renamed));
+
+  // stat
+  auto metadata = op.stat(file_path);
+  EXPECT_EQ(metadata.type, opendal::EntryMode::FILE);
+  EXPECT_EQ(metadata.content_length, data.size());
+
+  // list
+  auto list_file_path = dir_path + file_path;
+  op.write(list_file_path, data);
+  auto entries = op.list(dir_path);
+  EXPECT_EQ(entries.size(), 1);
+  EXPECT_EQ(entries[0].path, list_file_path);
+
+  // remove
+  op.remove(file_path_renamed);
+  op.remove(dir_path);
+  EXPECT_FALSE(op.is_exist(file_path_renamed));
 }
 
 int main(int argc, char **argv) {

Reply via email to