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