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

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


The following commit(s) were added to refs/heads/main by this push:
     new d4cb4738b feat(binding/go): Add ListOptions with recursive support 
(#7605)
d4cb4738b is described below

commit d4cb4738b37eddbf4f1d005d2c1da591c7f0cf6c
Author: PoAn Yang <[email protected]>
AuthorDate: Thu May 28 21:58:37 2026 +0900

    feat(binding/go): Add ListOptions with recursive support (#7605)
---
 bindings/c/include/opendal.h                  |  70 ++++++++++++++
 bindings/c/src/lib.rs                         |   1 +
 bindings/c/src/operator.rs                    |  52 +++++++++++
 bindings/c/src/types.rs                       |  52 +++++++++++
 bindings/c/tests/test_framework.h             |   9 ++
 bindings/c/tests/test_suites_list.cpp         | 125 +++++++++++++++++++++++++
 bindings/go/lister.go                         | 126 +++++++++++++++++++-------
 bindings/go/string_ownership_test.go          |  42 +++++++++
 bindings/go/tests/behavior_tests/list_test.go |  69 +++++++++++++-
 bindings/go/types.go                          |   2 +
 10 files changed, 513 insertions(+), 35 deletions(-)

diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h
index 10e8642f3..493c62c12 100644
--- a/bindings/c/include/opendal.h
+++ b/bindings/c/include/opendal.h
@@ -439,6 +439,25 @@ typedef struct opendal_result_list {
   struct opendal_error *error;
 } opendal_result_list;
 
+/**
+ * \brief The options for the list operation.
+ *
+ * This struct carries the options for the list operation, including whether to
+ * list recursively. Use `opendal_list_options_new()` to construct and
+ * `opendal_list_options_free()` to free.
+ *
+ * @see opendal_operator_list_with
+ * @see opendal_list_options_new
+ * @see opendal_list_options_free
+ * @see opendal_list_options_set_recursive
+ */
+typedef struct opendal_list_options {
+  /**
+   * Whether to list recursively under the prefix; default false.
+   */
+  bool recursive;
+} opendal_list_options;
+
 /**
  * \brief Metadata for **operator**, users can use this metadata to get 
information
  * of operator.
@@ -1281,6 +1300,33 @@ struct opendal_result_stat opendal_operator_stat(const 
struct opendal_operator *
 struct opendal_result_list opendal_operator_list(const struct opendal_operator 
*op,
                                                  const char *path);
 
+/**
+ * \brief Blocking list the objects in `path` with options.
+ *
+ * List the objects in `path` with the provided `opendal_list_options`. This is
+ * similar to `opendal_operator_list` but allows passing options such as
+ * `recursive` to control the listing behavior.
+ *
+ * @param op The opendal_operator created previously
+ * @param path The designated path you want to list
+ * @param opts The options for the list operation; pass NULL to use defaults
+ * @see opendal_lister
+ * @see opendal_list_options
+ * @return Returns opendal_result_list, containing a lister and an 
opendal_error.
+ *
+ * # Safety
+ *
+ * * The memory pointed to by `path` must contain a valid null terminator at 
the end of
+ *   the string.
+ *
+ * # Panic
+ *
+ * * If the `path` points to NULL, this function panics, i.e. exits with 
information
+ */
+struct opendal_result_list opendal_operator_list_with(const struct 
opendal_operator *op,
+                                                      const char *path,
+                                                      const struct 
opendal_list_options *opts);
+
 /**
  * \brief Blocking create the directory in `path`.
  *
@@ -1537,6 +1583,30 @@ void opendal_string_free(char *ptr);
  */
 void opendal_bytes_free(struct opendal_bytes *ptr);
 
+/**
+ * \brief Construct a heap-allocated opendal_list_options with default values.
+ *
+ * @return A new opendal_list_options with all options set to their defaults.
+ *
+ * @see opendal_list_options_free
+ */
+struct opendal_list_options *opendal_list_options_new(void);
+
+/**
+ * \brief Set the recursive option.
+ *
+ * @param opts The opendal_list_options to modify.
+ * @param recursive Whether to list recursively.
+ */
+void opendal_list_options_set_recursive(struct opendal_list_options *opts, 
bool recursive);
+
+/**
+ * \brief Free the heap memory used by opendal_list_options.
+ *
+ * @param opts The opendal_list_options to free.
+ */
+void opendal_list_options_free(struct opendal_list_options *opts);
+
 /**
  * \brief Construct a heap-allocated opendal_operator_options
  *
diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs
index ede881e03..89fd461a3 100644
--- a/bindings/c/src/lib.rs
+++ b/bindings/c/src/lib.rs
@@ -70,6 +70,7 @@ pub use result::opendal_result_writer_write;
 
 mod types;
 pub use types::opendal_bytes;
+pub use types::opendal_list_options;
 pub use types::opendal_operator_options;
 
 mod entry;
diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs
index bb6de2f5a..4b3a058d1 100644
--- a/bindings/c/src/operator.rs
+++ b/bindings/c/src/operator.rs
@@ -767,6 +767,58 @@ pub unsafe extern "C" fn opendal_operator_list(
     }
 }
 
+/// \brief Blocking list the objects in `path` with options.
+///
+/// List the objects in `path` with the provided `opendal_list_options`. This 
is
+/// similar to `opendal_operator_list` but allows passing options such as
+/// `recursive` to control the listing behavior.
+///
+/// @param op The opendal_operator created previously
+/// @param path The designated path you want to list
+/// @param opts The options for the list operation; pass NULL to use defaults
+/// @see opendal_lister
+/// @see opendal_list_options
+/// @return Returns opendal_result_list, containing a lister and an 
opendal_error.
+///
+/// # Safety
+///
+/// * The memory pointed to by `path` must contain a valid null terminator at 
the end of
+///   the string.
+///
+/// # Panic
+///
+/// * If the `path` points to NULL, this function panics, i.e. exits with 
information
+#[no_mangle]
+pub unsafe extern "C" fn opendal_operator_list_with(
+    op: &opendal_operator,
+    path: *const c_char,
+    opts: *const opendal_list_options,
+) -> opendal_result_list {
+    assert!(!path.is_null());
+    let path = std::ffi::CStr::from_ptr(path)
+        .to_str()
+        .expect("malformed path");
+    let list_opts = if opts.is_null() {
+        core::options::ListOptions::default()
+    } else {
+        let o = &*opts;
+        core::options::ListOptions {
+            recursive: o.recursive,
+            ..Default::default()
+        }
+    };
+    match op.deref().lister_options(path, list_opts) {
+        Ok(lister) => opendal_result_list {
+            lister: Box::into_raw(Box::new(opendal_lister::new(lister))),
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => opendal_result_list {
+            lister: std::ptr::null_mut(),
+            error: opendal_error::new(e),
+        },
+    }
+}
+
 /// \brief Blocking create the directory in `path`.
 ///
 /// Create the directory in `path` blocking by `op_ptr`.
diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs
index 2ba5b6a5b..1b63f24ce 100644
--- a/bindings/c/src/types.rs
+++ b/bindings/c/src/types.rs
@@ -84,6 +84,58 @@ impl opendal_bytes {
     }
 }
 
+/// \brief The options for the list operation.
+///
+/// This struct carries the options for the list operation, including whether 
to
+/// list recursively. Use `opendal_list_options_new()` to construct and
+/// `opendal_list_options_free()` to free.
+///
+/// @see opendal_operator_list_with
+/// @see opendal_list_options_new
+/// @see opendal_list_options_free
+/// @see opendal_list_options_set_recursive
+#[repr(C)]
+pub struct opendal_list_options {
+    /// Whether to list recursively under the prefix; default false.
+    pub recursive: bool,
+}
+
+impl opendal_list_options {
+    /// \brief Construct a heap-allocated opendal_list_options with default 
values.
+    ///
+    /// @return A new opendal_list_options with all options set to their 
defaults.
+    ///
+    /// @see opendal_list_options_free
+    #[no_mangle]
+    pub extern "C" fn opendal_list_options_new() -> *mut Self {
+        Box::into_raw(Box::new(Self { recursive: false }))
+    }
+
+    /// \brief Set the recursive option.
+    ///
+    /// @param opts The opendal_list_options to modify.
+    /// @param recursive Whether to list recursively.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_list_options_set_recursive(
+        opts: *mut opendal_list_options,
+        recursive: bool,
+    ) {
+        if !opts.is_null() {
+            (*opts).recursive = recursive;
+        }
+    }
+
+    /// \brief Free the heap memory used by opendal_list_options.
+    ///
+    /// @param opts The opendal_list_options to free.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_list_options_free(opts: *mut 
opendal_list_options) {
+        if !opts.is_null() {
+            drop(Box::from_raw(opts));
+        }
+    }
+}
+
 impl Drop for opendal_bytes {
     fn drop(&mut self) {
         unsafe {
diff --git a/bindings/c/tests/test_framework.h 
b/bindings/c/tests/test_framework.h
index 06f60c27c..e7abe6284 100644
--- a/bindings/c/tests/test_framework.h
+++ b/bindings/c/tests/test_framework.h
@@ -212,6 +212,15 @@ inline opendal_required_capability 
make_capability_create_dir_list() {
     return cap;
 }
 
+inline opendal_required_capability 
make_capability_write_create_dir_list_recursive() {
+    opendal_required_capability cap = NO_CAPABILITY;
+    cap.write = true;
+    cap.create_dir = true;
+    cap.list = true;
+    cap.list_with_recursive = true;
+    return cap;
+}
+
 inline opendal_required_capability make_capability_presign() {
     opendal_required_capability cap = NO_CAPABILITY;
     cap.read = true;
diff --git a/bindings/c/tests/test_suites_list.cpp 
b/bindings/c/tests/test_suites_list.cpp
index 12c291462..33aebe7ab 100644
--- a/bindings/c/tests/test_suites_list.cpp
+++ b/bindings/c/tests/test_suites_list.cpp
@@ -20,6 +20,7 @@
 #include "test_framework.h"
 #include <set>
 #include <string>
+#include <unordered_set>
 
 // Test: Basic list operation
 void test_list_basic(opendal_test_context* ctx)
@@ -347,6 +348,126 @@ void test_entry_metadata(opendal_test_context* ctx)
     opendal_operator_delete(ctx->config->operator_instance, dir_path);
 }
 
+// Test: list_with default options (null opts behaves like list)
+void test_list_with_default_options(opendal_test_context* ctx)
+{
+    const char* dir_path = "test_list_with_default/";
+    const char* file_path = "test_list_with_default/file.txt";
+
+    opendal_error* error = 
opendal_operator_create_dir(ctx->config->operator_instance, dir_path);
+    OPENDAL_ASSERT_NO_ERROR(error, "Create dir should succeed");
+
+    opendal_bytes data;
+    data.data = (uint8_t*)"content";
+    data.len = 7;
+    data.capacity = 7;
+    error = opendal_operator_write(ctx->config->operator_instance, file_path, 
&data);
+    OPENDAL_ASSERT_NO_ERROR(error, "Write should succeed");
+
+    // Pass NULL opts — should behave identically to opendal_operator_list
+    opendal_result_list list_result = opendal_operator_list_with(
+        ctx->config->operator_instance, dir_path, NULL);
+    OPENDAL_ASSERT_NO_ERROR(list_result.error, "list_with(NULL opts) should 
succeed");
+    OPENDAL_ASSERT_NOT_NULL(list_result.lister, "Lister should not be null");
+
+    bool found_file = false;
+    while (true) {
+        opendal_result_lister_next next = 
opendal_lister_next(list_result.lister);
+        if (next.error) {
+            OPENDAL_ASSERT_NO_ERROR(next.error, "lister_next should not fail");
+            break;
+        }
+        if (!next.entry) break;
+
+        char* path = opendal_entry_path(next.entry);
+        if (strcmp(path, file_path) == 0) {
+            found_file = true;
+        }
+        opendal_string_free(path);
+        opendal_entry_free(next.entry);
+    }
+
+    OPENDAL_ASSERT(found_file, "Should find the file with null opts");
+
+    opendal_lister_free(list_result.lister);
+    opendal_operator_delete(ctx->config->operator_instance, file_path);
+    opendal_operator_delete(ctx->config->operator_instance, dir_path);
+}
+
+// Test: list_with recursive=true returns entries in nested directories
+void test_list_with_recursive(opendal_test_context* ctx)
+{
+    const char* base_dir = "test_list_recursive/";
+    const char* sub_dir = "test_list_recursive/sub/";
+    const char* deep_dir = "test_list_recursive/sub/deep/";
+    const char* file_top = "test_list_recursive/top.txt";
+    const char* file_sub = "test_list_recursive/sub/mid.txt";
+    const char* file_deep = "test_list_recursive/sub/deep/bottom.txt";
+
+    opendal_bytes data;
+    data.data = (uint8_t*)"x";
+    data.len = 1;
+    data.capacity = 1;
+
+    opendal_error* error;
+    error = opendal_operator_create_dir(ctx->config->operator_instance, 
base_dir);
+    OPENDAL_ASSERT_NO_ERROR(error, "Create base dir should succeed");
+    error = opendal_operator_create_dir(ctx->config->operator_instance, 
sub_dir);
+    OPENDAL_ASSERT_NO_ERROR(error, "Create sub dir should succeed");
+    error = opendal_operator_create_dir(ctx->config->operator_instance, 
deep_dir);
+    OPENDAL_ASSERT_NO_ERROR(error, "Create deep dir should succeed");
+    error = opendal_operator_write(ctx->config->operator_instance, file_top, 
&data);
+    OPENDAL_ASSERT_NO_ERROR(error, "Write top file should succeed");
+    error = opendal_operator_write(ctx->config->operator_instance, file_sub, 
&data);
+    OPENDAL_ASSERT_NO_ERROR(error, "Write sub file should succeed");
+    error = opendal_operator_write(ctx->config->operator_instance, file_deep, 
&data);
+    OPENDAL_ASSERT_NO_ERROR(error, "Write deep file should succeed");
+
+    // List recursively from base_dir
+    opendal_list_options* opts = opendal_list_options_new();
+    OPENDAL_ASSERT_NOT_NULL(opts, "list_options_new should not return NULL");
+    opendal_list_options_set_recursive(opts, true);
+
+    opendal_result_list list_result = opendal_operator_list_with(
+        ctx->config->operator_instance, base_dir, opts);
+    opendal_list_options_free(opts);
+
+    OPENDAL_ASSERT_NO_ERROR(list_result.error, "Recursive list should 
succeed");
+    OPENDAL_ASSERT_NOT_NULL(list_result.lister, "Lister should not be null");
+
+    std::unordered_set<std::string> found_paths;
+    while (true) {
+        opendal_result_lister_next next = 
opendal_lister_next(list_result.lister);
+        if (next.error) {
+            OPENDAL_ASSERT_NO_ERROR(next.error, "lister_next should not fail");
+            break;
+        }
+        if (!next.entry) break;
+
+        char* path = opendal_entry_path(next.entry);
+        found_paths.insert(std::string(path));
+        opendal_string_free(path);
+        opendal_entry_free(next.entry);
+    }
+    opendal_lister_free(list_result.lister);
+
+    // All three files must appear in a recursive listing
+    OPENDAL_ASSERT(found_paths.count(file_top) > 0,
+        "Recursive list must include top-level file");
+    OPENDAL_ASSERT(found_paths.count(file_sub) > 0,
+        "Recursive list must include file in sub directory");
+    OPENDAL_ASSERT(found_paths.count(file_deep) > 0,
+        "Recursive list must include file in deep directory");
+
+    // Cleanup
+    opendal_operator_delete(ctx->config->operator_instance, file_deep);
+    opendal_operator_delete(ctx->config->operator_instance, file_sub);
+    opendal_operator_delete(ctx->config->operator_instance, file_top);
+    opendal_operator_delete(ctx->config->operator_instance, deep_dir);
+    opendal_operator_delete(ctx->config->operator_instance, sub_dir);
+    opendal_operator_delete(ctx->config->operator_instance, base_dir);
+}
+
 // Define the list test suite
 opendal_test_case list_tests[] = {
     { "list_basic", test_list_basic, make_capability_write_create_dir_list() },
@@ -356,6 +477,10 @@ opendal_test_case list_tests[] = {
         make_capability_write_create_dir_list() },
     { "entry_metadata", test_entry_metadata,
         make_capability_write_create_dir_list() },
+    { "list_with_default_options", test_list_with_default_options,
+        make_capability_write_create_dir_list() },
+    { "list_with_recursive", test_list_with_recursive,
+        make_capability_write_create_dir_list_recursive() },
 };
 
 opendal_test_suite list_suite = {
diff --git a/bindings/go/lister.go b/bindings/go/lister.go
index 9d7943dea..fc6fd2b33 100644
--- a/bindings/go/lister.go
+++ b/bindings/go/lister.go
@@ -74,69 +74,86 @@ func (op *Operator) Check() (err error) {
 
 // List returns a Lister to iterate over entries that start with the given 
path in the parent directory.
 //
-// This function creates a new Lister to enumerate entries in the specified 
path.
+// WithListFn is a functional option for the List operation.
+type WithListFn func(*listOptions)
+
+// ListWithRecursive sets the recursive flag for the list operation.
+//
+// When recursive is true, the list operation will descend into 
sub-directories.
+func ListWithRecursive(recursive bool) WithListFn {
+       return func(o *listOptions) {
+               o.recursive = recursive
+       }
+}
+
+// listOptions holds the options for a list operation.
+type listOptions struct {
+       recursive bool
+}
+
+// List returns a Lister to iterate over entries that start with the given 
path.
 //
 // # Parameters
 //
 //   - path: The starting path for listing entries.
+//   - opts: Optional functional options to configure the list operation.
 //
 // # Returns
 //
 //   - *Lister: A new Lister instance for iterating over entries.
 //   - error: An error if the listing operation fails, or nil if successful.
 //
-// # Notes
-//
-//  1. List is a wrapper around the C-binding function 
`opendal_operator_list`. Recursive listing is not currently supported.
-//  2. Returned entries do not include metadata information. Use op.Stat to 
fetch metadata for individual entries.
-//
 // # Example
 //
 //     func exampleList(op *opendal.Operator) {
-//             lister, err := op.List("test")
+//             // List without options
+//             lister, err := op.List("test/")
 //             if err != nil {
 //                     log.Fatal(err)
 //             }
+//             defer lister.Close()
+//
+//             // List with recursive option
+//             lister, err = op.List("test/", opendal.ListWithRecursive(true))
+//             if err != nil {
+//                     log.Fatal(err)
+//             }
+//             defer lister.Close()
 //
 //             for lister.Next() {
 //                     entry := lister.Entry()
-//
-//                     fmt.Printf("Name: %s\n", entry.Name())
-//                     if meta := entry.Metadata(); meta != nil {
-//                             fmt.Printf("Length: %d\n", meta.ContentLength())
-//                             fmt.Printf("Last Modified: %s\n", 
meta.LastModified())
-//                             fmt.Printf("Is Directory: %v, Is File: %v\n", 
meta.IsDir(), meta.IsFile())
-//                     }
-//                     fmt.Println("---")
+//                     fmt.Printf("Path: %s\n", entry.Path())
 //             }
-//             if err := lister.Err(); err != nil {
+//             if err := lister.Error(); err != nil {
 //                     log.Printf("Error during listing: %v", err)
 //             }
 //     }
 //
-// Note: Always check lister.Err() after the loop to catch any errors that
+// Note: Always check lister.Error() after the loop to catch any errors that
 // occurred during iteration.
-func (op *Operator) List(path string) (*Lister, error) {
-       inner, err := ffiOperatorList.symbol(op.ctx)(op.inner, path)
+func (op *Operator) List(path string, opts ...WithListFn) (*Lister, error) {
+       o := &listOptions{}
+       for _, opt := range opts {
+               opt(o)
+       }
+       cOpts := ffiListOptionsNew.symbol(op.ctx)()
+       defer ffiListOptionsFree.symbol(op.ctx)(cOpts)
+       ffiListOptionsSetRecursive.symbol(op.ctx)(cOpts, o.recursive)
+       listerInner, err := ffiOperatorListWith.symbol(op.ctx)(op.inner, path, 
cOpts)
        if err != nil {
                return nil, err
        }
-       lister := &Lister{
-               inner: inner,
+       return &Lister{
+               inner: listerInner,
                ctx:   op.ctx,
-       }
-       return lister, nil
+       }, nil
 }
 
-// Lister provides an mechanism for listing entries at a specified path.
+// Lister provides a mechanism for listing entries at a specified path.
 //
 // Lister is a wrapper around the C-binding function `opendal_operator_list`. 
It allows
 // for efficient iteration over entries in a storage system.
 //
-// # Limitations
-//
-//   - The current implementation does not support the `list_with` 
functionality.
-//
 // # Usage
 //
 // Lister should be used in conjunction with its Next() and Entry() methods to
@@ -303,12 +320,54 @@ func (e *Entry) Metadata() *Metadata {
        return e.meta
 }
 
-var ffiOperatorList = newFFI(ffiOpts{
-       sym:    "opendal_operator_list",
+var ffiListOptionsNew = newFFI(ffiOpts{
+       sym:   "opendal_list_options_new",
+       rType: &ffi.TypePointer,
+}, func(_ context.Context, ffiCall ffiCall) func() *opendalListOptions {
+       return func() *opendalListOptions {
+               var opts *opendalListOptions
+               ffiCall(unsafe.Pointer(&opts))
+               return opts
+       }
+})
+
+var ffiListOptionsSetRecursive = newFFI(ffiOpts{
+       sym:    "opendal_list_options_set_recursive",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypeUint8},
+}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalListOptions, 
recursive bool) {
+       return func(opts *opendalListOptions, recursive bool) {
+               var r uint8
+               if recursive {
+                       r = 1
+               }
+               ffiCall(
+                       nil,
+                       unsafe.Pointer(&opts),
+                       unsafe.Pointer(&r),
+               )
+       }
+})
+
+var ffiListOptionsFree = newFFI(ffiOpts{
+       sym:    "opendal_list_options_free",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalListOptions) {
+       return func(opts *opendalListOptions) {
+               ffiCall(
+                       nil,
+                       unsafe.Pointer(&opts),
+               )
+       }
+})
+
+var ffiOperatorListWith = newFFI(ffiOpts{
+       sym:    "opendal_operator_list_with",
        rType:  &typeResultList,
-       aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
-}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, path 
string) (*opendalLister, error) {
-       return func(op *opendalOperator, path string) (*opendalLister, error) {
+       aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer, 
&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, path 
string, opts *opendalListOptions) (*opendalLister, error) {
+       return func(op *opendalOperator, path string, opts *opendalListOptions) 
(*opendalLister, error) {
                bytePath, err := BytePtrFromString(path)
                if err != nil {
                        return nil, err
@@ -318,6 +377,7 @@ var ffiOperatorList = newFFI(ffiOpts{
                        unsafe.Pointer(&result),
                        unsafe.Pointer(&op),
                        unsafe.Pointer(&bytePath),
+                       unsafe.Pointer(&opts),
                )
                if result.err != nil {
                        return nil, parseError(ctx, result.err)
diff --git a/bindings/go/string_ownership_test.go 
b/bindings/go/string_ownership_test.go
index 65da9ecbe..368efe037 100644
--- a/bindings/go/string_ownership_test.go
+++ b/bindings/go/string_ownership_test.go
@@ -276,3 +276,45 @@ func assertFreedPointers(t *testing.T, got []*byte, want 
...*byte) {
                }
        }
 }
+
+func TestListWithRecursiveDefaultNotRecursive(t *testing.T) {
+       o := &listOptions{}
+       if o.recursive {
+               t.Fatalf("default listOptions.recursive = true, want false")
+       }
+}
+
+func TestListWithRecursiveTrue(t *testing.T) {
+       o := &listOptions{}
+       ListWithRecursive(true)(o)
+       if !o.recursive {
+               t.Fatalf("ListWithRecursive(true): recursive = false, want 
true")
+       }
+}
+
+func TestListWithRecursiveFalse(t *testing.T) {
+       o := &listOptions{}
+       ListWithRecursive(true)(o)
+       ListWithRecursive(false)(o)
+       if o.recursive {
+               t.Fatalf("ListWithRecursive(false): recursive = true, want 
false")
+       }
+}
+
+func TestFfiOperatorListWithReturnType(t *testing.T) {
+       if ffiOperatorListWith.opts.rType != &typeResultList {
+               t.Fatalf("ffiOperatorListWith rType = %v, want typeResultList", 
ffiOperatorListWith.opts.rType)
+       }
+}
+
+func TestFfiOperatorListWithArgTypes(t *testing.T) {
+       aTypes := ffiOperatorListWith.opts.aTypes
+       if len(aTypes) != 3 {
+               t.Fatalf("ffiOperatorListWith aTypes len = %d, want 3", 
len(aTypes))
+       }
+       for i, at := range aTypes {
+               if at != &ffi.TypePointer {
+                       t.Fatalf("ffiOperatorListWith aTypes[%d] = %v, want 
TypePointer", i, at)
+               }
+       }
+}
diff --git a/bindings/go/tests/behavior_tests/list_test.go 
b/bindings/go/tests/behavior_tests/list_test.go
index 29f0436ec..5fc09e27e 100644
--- a/bindings/go/tests/behavior_tests/list_test.go
+++ b/bindings/go/tests/behavior_tests/list_test.go
@@ -24,7 +24,7 @@ import (
        "slices"
        "strings"
 
-       "github.com/apache/opendal/bindings/go"
+       opendal "github.com/apache/opendal/bindings/go"
        "github.com/google/uuid"
        "github.com/stretchr/testify/require"
 )
@@ -33,7 +33,7 @@ func testsList(cap *opendal.Capability) []behaviorTest {
        if !cap.Read() || !cap.Write() || !cap.List() || !cap.CreateDir() {
                return nil
        }
-       return []behaviorTest{
+       tests := []behaviorTest{
                testListCheck,
                testListDir,
                testListRichDir,
@@ -43,7 +43,12 @@ func testsList(cap *opendal.Capability) []behaviorTest {
                testListNestedDir,
                testListDirWithFilePath,
                testListEntryMetadata,
+               testListWithDefaultOptions,
        }
+       if cap.ListWithRecursive() {
+               tests = append(tests, testListWithRecursive)
+       }
+       return tests
 }
 
 func testListCheck(assert *require.Assertions, op *opendal.Operator, fixture 
*fixture) {
@@ -289,3 +294,63 @@ func testListDirWithFilePath(assert *require.Assertions, 
op *opendal.Operator, f
        }
        assert.Nil(obs.Error())
 }
+
+func testListWithDefaultOptions(assert *require.Assertions, op 
*opendal.Operator, fixture *fixture) {
+       parent := fixture.NewDirPath()
+       subDir := fixture.PushPath(fmt.Sprintf("%s%s/", parent, 
uuid.NewString()))
+       fileInParent, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", 
parent, uuid.NewString()))
+       fileInSub := fixture.PushPath(fmt.Sprintf("%s%s", subDir, 
uuid.NewString()))
+
+       assert.Nil(op.CreateDir(parent))
+       assert.Nil(op.CreateDir(subDir))
+       assert.Nil(op.Write(fileInParent, content))
+       assert.Nil(op.Write(fileInSub, content))
+
+       obs, err := op.List(parent)
+       assert.Nil(err)
+       defer obs.Close()
+
+       var paths []string
+       for obs.Next() {
+               paths = append(paths, obs.Entry().Path())
+       }
+       assert.Nil(obs.Error())
+
+       assert.NotContains(paths, fileInSub,
+               "List without options must not descend into sub-directories")
+       assert.Contains(paths, fileInParent, "direct child file must appear")
+       assert.Contains(paths, subDir, "direct child dir must appear")
+}
+
+func testListWithRecursive(assert *require.Assertions, op *opendal.Operator, 
fixture *fixture) {
+       parent := fixture.NewDirPath()
+       subDir := fixture.PushPath(fmt.Sprintf("%s%s/", parent, 
uuid.NewString()))
+       deepDir := fixture.PushPath(fmt.Sprintf("%s%s/", subDir, 
uuid.NewString()))
+
+       fileTop := fixture.PushPath(fmt.Sprintf("%s%s", parent, 
uuid.NewString()))
+       fileMid := fixture.PushPath(fmt.Sprintf("%s%s", subDir, 
uuid.NewString()))
+       fileDeep := fixture.PushPath(fmt.Sprintf("%s%s", deepDir, 
uuid.NewString()))
+
+       content := []byte("recursive test content")
+
+       assert.Nil(op.CreateDir(parent))
+       assert.Nil(op.CreateDir(subDir))
+       assert.Nil(op.CreateDir(deepDir))
+       assert.Nil(op.Write(fileTop, content))
+       assert.Nil(op.Write(fileMid, content))
+       assert.Nil(op.Write(fileDeep, content))
+
+       obs, err := op.List(parent, opendal.ListWithRecursive(true))
+       assert.Nil(err)
+       defer obs.Close()
+
+       var paths []string
+       for obs.Next() {
+               paths = append(paths, obs.Entry().Path())
+       }
+       assert.Nil(obs.Error())
+
+       assert.Contains(paths, fileTop, "recursive list must include top-level 
file")
+       assert.Contains(paths, fileMid, "recursive list must include file in 
sub-directory")
+       assert.Contains(paths, fileDeep, "recursive list must include file in 
deep directory")
+}
diff --git a/bindings/go/types.go b/bindings/go/types.go
index ead55958d..3d0239f19 100644
--- a/bindings/go/types.go
+++ b/bindings/go/types.go
@@ -306,6 +306,8 @@ type opendalResultList struct {
 
 type opendalLister struct{}
 
+type opendalListOptions struct{}
+
 type opendalResultListerNext struct {
        entry *opendalEntry
        err   *opendalError

Reply via email to