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/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 415a2887b feat(bindings/ruby): add lister (#5600)
415a2887b is described below

commit 415a2887b75bb558f49573e17a5dc69fd74bf63c
Author: Erick Guan <[email protected]>
AuthorDate: Mon Feb 10 15:02:13 2025 +0900

    feat(bindings/ruby): add lister (#5600)
    
    * feat(bindings/ruby): add lister
    
    * fixup! feat(bindings/ruby): add lister
    
    * fixup! feat(bindings/ruby): add lister
---
 bindings/ruby/lib/opendal.rb                       |   1 +
 .../ruby/lib/{opendal.rb => opendal_ruby/entry.rb} |  12 ++-
 bindings/ruby/src/lib.rs                           |   2 +
 bindings/ruby/src/lister.rs                        | 106 +++++++++++++++++++++
 bindings/ruby/src/operator.rs                      |  41 ++++++++
 bindings/ruby/test/lister_test.rb                  |  77 +++++++++++++++
 6 files changed, 237 insertions(+), 2 deletions(-)

diff --git a/bindings/ruby/lib/opendal.rb b/bindings/ruby/lib/opendal.rb
index 5dbb0404c..079721860 100644
--- a/bindings/ruby/lib/opendal.rb
+++ b/bindings/ruby/lib/opendal.rb
@@ -19,3 +19,4 @@
 
 require_relative "opendal_ruby/opendal_ruby"
 require_relative "opendal_ruby/io"
+require_relative "opendal_ruby/entry"
diff --git a/bindings/ruby/lib/opendal.rb 
b/bindings/ruby/lib/opendal_ruby/entry.rb
similarity index 86%
copy from bindings/ruby/lib/opendal.rb
copy to bindings/ruby/lib/opendal_ruby/entry.rb
index 5dbb0404c..58adc318d 100644
--- a/bindings/ruby/lib/opendal.rb
+++ b/bindings/ruby/lib/opendal_ruby/entry.rb
@@ -17,5 +17,13 @@
 
 # frozen_string_literal: true
 
-require_relative "opendal_ruby/opendal_ruby"
-require_relative "opendal_ruby/io"
+module OpenDAL
+  class Entry
+    def to_h
+      {
+        path: path,
+        metadata: metadata
+      }
+    end
+  end
+end
diff --git a/bindings/ruby/src/lib.rs b/bindings/ruby/src/lib.rs
index f47b964b4..9e4e88d84 100644
--- a/bindings/ruby/src/lib.rs
+++ b/bindings/ruby/src/lib.rs
@@ -26,6 +26,7 @@ pub use ::opendal as ocore;
 
 mod capability;
 mod io;
+mod lister;
 mod metadata;
 mod operator;
 
@@ -41,6 +42,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
     let _ = metadata::include(&gem_module);
     let _ = capability::include(&gem_module);
     let _ = io::include(&gem_module);
+    let _ = lister::include(&ruby, &gem_module);
 
     Ok(())
 }
diff --git a/bindings/ruby/src/lister.rs b/bindings/ruby/src/lister.rs
new file mode 100644
index 000000000..8229d8e71
--- /dev/null
+++ b/bindings/ruby/src/lister.rs
@@ -0,0 +1,106 @@
+// 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.
+
+use std::borrow::BorrowMut;
+use std::sync::Arc;
+use std::sync::Mutex;
+
+use magnus::block::Yield;
+use magnus::class;
+use magnus::method;
+use magnus::prelude::*;
+use magnus::Error;
+use magnus::RModule;
+use magnus::Ruby;
+
+use crate::metadata::Metadata;
+use crate::*;
+
+/// Entry returned by Lister to represent a path and it's relative metadata.
+#[magnus::wrap(class = "OpenDAL::Entry", free_immediately, size)]
+pub struct Entry(ocore::Entry);
+
+impl Entry {
+    /// Gets the path of entry. Path is relative to operator's root.
+    ///
+    /// Only valid in current operator.
+    ///
+    /// If this entry is a dir, `path` MUST end with `/`
+    /// Otherwise, `path` MUST NOT end with `/`.
+    fn path(&self) -> Result<&str, Error> {
+        Ok(self.0.path())
+    }
+
+    /// Gets the name of entry. Name is the last segment of path.
+    ///
+    /// If this entry is a dir, `name` MUST end with `/`
+    /// Otherwise, `name` MUST NOT end with `/`.
+    fn name(&self) -> Result<&str, Error> {
+        Ok(self.0.name())
+    }
+
+    /// Fetches the metadata of this entry.
+    fn metadata(&self) -> Result<Metadata, Error> {
+        Ok(Metadata::new(self.0.metadata().clone()))
+    }
+}
+
+/// Represents the result when list a directory
+#[magnus::wrap(class = "OpenDAL::Lister", free_immediately, size)]
+pub struct Lister(Arc<Mutex<ocore::BlockingLister>>);
+
+impl Iterator for Lister {
+    type Item = Entry;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Ok(mut inner) = self.0.borrow_mut().lock() {
+            match inner.next() {
+                Some(Ok(entry)) => Some(Entry(entry)),
+                _ => None,
+            }
+        } else {
+            None
+        }
+    }
+}
+
+impl Lister {
+    /// Creates a new blocking Lister.
+    pub fn new(inner: ocore::BlockingLister) -> Self {
+        Self(Arc::new(Mutex::new(inner)))
+    }
+
+    /// Returns the next element.
+    fn each(&self) -> Result<Yield<Lister>, Error> {
+        Ok(Yield::Iter(Lister(self.0.clone())))
+    }
+}
+
+pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
+    let entry_class = gem_module.define_class("Entry", class::object())?;
+    entry_class.define_method("path", method!(Entry::path, 0))?;
+    entry_class.define_method("name", method!(Entry::name, 0))?;
+    entry_class.define_method("metadata", method!(Entry::metadata, 0))?;
+
+    let lister_class = gem_module.define_class("Lister", class::object())?;
+    let _ = lister_class
+        .include_module(ruby.module_enumerable())
+        .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))?;
+    lister_class.define_method("each", method!(Lister::each, 0))?;
+
+    Ok(())
+}
diff --git a/bindings/ruby/src/operator.rs b/bindings/ruby/src/operator.rs
index 45b11392a..19c8a3721 100644
--- a/bindings/ruby/src/operator.rs
+++ b/bindings/ruby/src/operator.rs
@@ -21,13 +21,17 @@ use std::str::FromStr;
 use magnus::class;
 use magnus::method;
 use magnus::prelude::*;
+use magnus::scan_args::get_kwargs;
+use magnus::scan_args::scan_args;
 use magnus::Error;
 use magnus::RModule;
 use magnus::RString;
 use magnus::Ruby;
+use magnus::Value;
 
 use crate::capability::Capability;
 use crate::io::Io;
+use crate::lister::Lister;
 use crate::metadata::Metadata;
 use crate::*;
 
@@ -141,6 +145,42 @@ impl Operator {
         let operator = rb_self.0.clone();
         Ok(Io::new(&ruby, operator, path, mode)?)
     }
+
+    /// Lists the directory.
+    ///
+    /// @param limit [usize, nil] per-request max results
+    /// @param start_after [String, nil] the specified key to start listing 
from.
+    /// @param recursive [Boolean, nil] lists the directory recursively.
+    pub fn list(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Lister, 
Error> {
+        let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
+        let (path,) = args.required;
+        let kwargs = get_kwargs::<_, (), (Option<usize>, Option<String>, 
Option<bool>), ()>(
+            args.keywords,
+            &[],
+            &["limit", "start_after", "recursive"],
+        )?;
+        let (limit, start_after, recursive) = kwargs.optional;
+
+        let mut builder = rb_self.0.clone().lister_with(&path);
+
+        if let Some(limit) = limit {
+            builder = builder.limit(limit);
+        }
+
+        if let Some(start_after) = start_after {
+            builder = builder.start_after(start_after.as_str());
+        }
+
+        if let Some(true) = recursive {
+            builder = builder.recursive(true);
+        }
+
+        let lister = builder
+            .call()
+            .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))?;
+
+        Ok(Lister::new(lister))
+    }
 }
 
 pub fn include(gem_module: &RModule) -> Result<(), Error> {
@@ -157,6 +197,7 @@ pub fn include(gem_module: &RModule) -> Result<(), Error> {
     class.define_method("remove_all", method!(Operator::remove_all, 1))?;
     class.define_method("copy", method!(Operator::copy, 2))?;
     class.define_method("open", method!(Operator::open, 2))?;
+    class.define_method("list", method!(Operator::list, -1))?;
 
     Ok(())
 }
diff --git a/bindings/ruby/test/lister_test.rb 
b/bindings/ruby/test/lister_test.rb
new file mode 100644
index 000000000..fbac53772
--- /dev/null
+++ b/bindings/ruby/test/lister_test.rb
@@ -0,0 +1,77 @@
+# 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.
+
+# frozen_string_literal: true
+
+require "test_helper"
+require "tmpdir"
+
+class ListerTest < ActiveSupport::TestCase
+  setup do
+    @root = Dir.mktmpdir
+    File.write("#{@root}/sample", "Sample data for testing")
+    Dir.mkdir("#{@root}/sub")
+    File.write("#{@root}/sub/sample", "Sample data for testing")
+    @op = OpenDAL::Operator.new("fs", {"root" => @root})
+  end
+
+  test "lists the directory" do
+    lister = @op.list("")
+
+    lists = lister.map(&:to_h).map { |e| e[:path] }.sort
+
+    assert_equal ["/", "sample", "sub/"], lists
+  end
+
+  test "list returns the entry" do
+    entry = @op.list("/").first
+
+    assert entry.is_a?(OpenDAL::Entry)
+    assert entry.name.length > 0
+  end
+
+  test "entry has the metadata" do
+    metadata = @op.list("sample").first.metadata
+
+    assert metadata.file?
+    assert !metadata.dir?
+  end
+
+  test "lists the directory recursively" do
+    lister = @op.list("", recursive: true)
+
+    lists = lister.map(&:to_h).map { |e| e[:path] }.sort
+
+    assert_equal ["/", "sample", "sub/", "sub/sample"], lists
+  end
+
+  test "lists the directory with limit" do
+    lister = @op.list("", limit: 1)
+
+    lists = lister.map(&:to_h).map { |e| e[:path] }.sort
+
+    assert_equal ["/", "sample", "sub/"], lists
+  end
+
+  test "lists the directory with start_after" do
+    lister = @op.list("", start_after: "sub/")
+
+    lists = lister.map(&:to_h).map { |e| e[:path] }.sort
+
+    assert_equal ["/", "sample", "sub/"], lists # fs backend doesn't support 
start_after
+  end
+end

Reply via email to