Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(bindings/ruby): add lister #5600

Merged
merged 3 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bindings/ruby/lib/opendal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@

require_relative "opendal_ruby/opendal_ruby"
require_relative "opendal_ruby/io"
require_relative "opendal_ruby/entry"
29 changes: 29 additions & 0 deletions bindings/ruby/lib/opendal_ruby/entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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

module OpenDAL
class Entry
def to_h
{
path: path,
metadata: metadata
}
end
end
end
2 changes: 2 additions & 0 deletions bindings/ruby/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub use ::opendal as ocore;

mod capability;
mod io;
mod lister;
mod metadata;
mod operator;

Expand All @@ -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(())
}
93 changes: 93 additions & 0 deletions bindings/ruby/src/lister.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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::cell::RefCell;

use magnus::class;
use magnus::method;
use magnus::prelude::*;
use magnus::Error;
use magnus::RModule;
use magnus::Ruby;
use magnus::Value;

use crate::metadata::Metadata;
use crate::*;

/// Represents the result when list a directory
#[magnus::wrap(class = "OpenDAL::Lister", free_immediately, size)]
pub struct Lister(RefCell<ocore::BlockingLister>);

/// 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()))
}
}

impl Lister {
/// Creates a new blocking Lister.
pub fn new(inner: ocore::BlockingLister) -> Self {
Self(RefCell::new(inner))
}

/// Returns the next element.
fn each(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
while let Some(Ok(entry)) = rb_self.0.borrow_mut().next() {
// we don't need the return value of the yield block
let _ = ruby.yield_value::<lister::Entry, Value>(Entry(entry));
}
Ok(())
}
}

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(())
}
27 changes: 27 additions & 0 deletions bindings/ruby/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use magnus::Ruby;

use crate::capability::Capability;
use crate::io::Io;
use crate::lister::Lister;
use crate::metadata::Metadata;
use crate::*;

Expand Down Expand Up @@ -141,6 +142,30 @@ impl Operator {
let operator = rb_self.0.clone();
Ok(Io::new(&ruby, operator, path, mode)?)
}

/// Lists the directory.
pub fn list(ruby: &Ruby, rb_self: &Self, path: String) -> Result<Lister, Error> {
let lister = rb_self
.0
.clone()
.lister(&path)
.map_err(|err| Error::new(ruby.exception_runtime_error(), err.to_string()))?;

Ok(Lister::new(lister))
}

/// Lists the directory recursively.
pub fn scan(ruby: &Ruby, rb_self: &Self, path: String) -> Result<Lister, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, scan has been deprecated, and bindings should not expose such an API. Instead, the list function should accept an argument that allows setting recursive: true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed.

let lister = rb_self
.0
.clone()
.lister_with(&path)
.recursive(true)
.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> {
Expand All @@ -157,6 +182,8 @@ 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))?;
class.define_method("scan", method!(Operator::scan, 1))?;

Ok(())
}
68 changes: 68 additions & 0 deletions bindings/ruby/test/lister_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 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_equal "sample", entry.name
end

test "entry has the metadata" do
metadata = @op.list("sample").first.metadata

assert metadata.file?
assert !metadata.dir?
end

test "scans the directory" do
lister = @op.scan("")

lists = lister.map(&:to_h).map { |e| e[:path] }.sort

assert_equal ["/", "sample", "sub/", "sub/sample"], lists
end

test "scan returns the entry" do
entry = @op.scan("sample").first

assert entry.is_a?(OpenDAL::Entry)
assert_equal "sample", entry.name
end
end