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

erickguan 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 6738df27f feat(bindings/ruby): support layers (#5874)
6738df27f is described below

commit 6738df27fd9d3636850aa9c98b3b309021c6f29b
Author: Erick Guan <[email protected]>
AuthorDate: Sat Apr 5 16:19:34 2025 +0200

    feat(bindings/ruby): support layers (#5874)
    
    * doc(bindings/ruby): doc updates and thread-safety notes
    
    Also various improvements over #inspect methods to be friendly within
    interactive consoles.
    
    * feat(bindings/ruby): support layers
    
    * feat(bindings/ruby): add mode to Metadata
    
    Also simplifies the implementation
    
    * Use the term middleware to refer layer
    
    * Use duck typing to apply layers
---
 bindings/ruby/Cargo.toml                           |   1 +
 bindings/ruby/lib/opendal.rb                       |   3 +
 bindings/ruby/lib/opendal_ruby/entry.rb            |   5 +
 .../lib/opendal_ruby/{entry.rb => metadata.rb}     |  28 +++-
 .../lib/opendal_ruby/{entry.rb => operator.rb}     |  13 +-
 .../opendal_ruby/{entry.rb => operator_info.rb}    |  10 +-
 bindings/ruby/src/lib.rs                           |   2 +
 bindings/ruby/src/lister.rs                        |  16 +-
 bindings/ruby/src/metadata.rs                      |  32 ++--
 bindings/ruby/src/middlewares.rs                   | 167 +++++++++++++++++++++
 bindings/ruby/src/operator.rs                      |  67 ++++++---
 bindings/ruby/test/blocking_op_test.rb             |   6 +
 .../entry.rb => test/middlewares_test.rb}          |  32 +++-
 13 files changed, 315 insertions(+), 67 deletions(-)

diff --git a/bindings/ruby/Cargo.toml b/bindings/ruby/Cargo.toml
index 133cc41bf..83e35a4bd 100644
--- a/bindings/ruby/Cargo.toml
+++ b/bindings/ruby/Cargo.toml
@@ -35,6 +35,7 @@ name = "opendal_ruby"
 magnus = { version = "0.7.1", features = ["bytes"] }
 # this crate won't be published, we always use the local version
 opendal = { version = ">=0", path = "../../core", features = [
+  "layers-throttle",
   # These are default features before v0.46. TODO: change to optional features
   "services-azblob",
   "services-azdls",
diff --git a/bindings/ruby/lib/opendal.rb b/bindings/ruby/lib/opendal.rb
index 079721860..78604e0ea 100644
--- a/bindings/ruby/lib/opendal.rb
+++ b/bindings/ruby/lib/opendal.rb
@@ -20,3 +20,6 @@
 require_relative "opendal_ruby/opendal_ruby"
 require_relative "opendal_ruby/io"
 require_relative "opendal_ruby/entry"
+require_relative "opendal_ruby/metadata"
+require_relative "opendal_ruby/operator"
+require_relative "opendal_ruby/operator_info"
diff --git a/bindings/ruby/lib/opendal_ruby/entry.rb 
b/bindings/ruby/lib/opendal_ruby/entry.rb
index 7dfc86d85..2bbd91ddb 100644
--- a/bindings/ruby/lib/opendal_ruby/entry.rb
+++ b/bindings/ruby/lib/opendal_ruby/entry.rb
@@ -20,11 +20,16 @@
 module OpenDAL
   class Entry
     # Returns the canonical data about an entry
+    # @return [Hash]
     def to_h
       {
         path: path,
         metadata: metadata
       }
     end
+
+    def inspect
+      "#<#{self.class.name} path: \"#{path}\", metadata: #{metadata.inspect}>"
+    end
   end
 end
diff --git a/bindings/ruby/lib/opendal_ruby/entry.rb 
b/bindings/ruby/lib/opendal_ruby/metadata.rb
similarity index 63%
copy from bindings/ruby/lib/opendal_ruby/entry.rb
copy to bindings/ruby/lib/opendal_ruby/metadata.rb
index 7dfc86d85..1bc576cee 100644
--- a/bindings/ruby/lib/opendal_ruby/entry.rb
+++ b/bindings/ruby/lib/opendal_ruby/metadata.rb
@@ -18,13 +18,27 @@
 # frozen_string_literal: true
 
 module OpenDAL
-  class Entry
-    # Returns the canonical data about an entry
-    def to_h
-      {
-        path: path,
-        metadata: metadata
-      }
+  class Metadata
+    FILE = "File"
+    DIRECTORY = "Directory"
+
+    # Returns `True` if this is a file.
+    # @return [Boolean]
+    def file?
+      mode == FILE
+    end
+
+    # Returns `True` if this is a directory.
+    # @return [Boolean]
+    def dir?
+      mode == DIRECTORY
+    end
+
+    def inspect
+      # Be concise to keep a few attributes
+      "#<#{self.class.name} mode: #{entry_mode}, \
+        content_type: #{content_type}, \
+        content_length: #{content_length}>"
     end
   end
 end
diff --git a/bindings/ruby/lib/opendal_ruby/entry.rb 
b/bindings/ruby/lib/opendal_ruby/operator.rb
similarity index 77%
copy from bindings/ruby/lib/opendal_ruby/entry.rb
copy to bindings/ruby/lib/opendal_ruby/operator.rb
index 7dfc86d85..416ecacf7 100644
--- a/bindings/ruby/lib/opendal_ruby/entry.rb
+++ b/bindings/ruby/lib/opendal_ruby/operator.rb
@@ -18,13 +18,12 @@
 # frozen_string_literal: true
 
 module OpenDAL
-  class Entry
-    # Returns the canonical data about an entry
-    def to_h
-      {
-        path: path,
-        metadata: metadata
-      }
+  class Operator
+    # Applies a middleware to the operator.
+    # @param middleware [Middleware]
+    # @return [Operator]
+    def middleware(middleware)
+      middleware.apply_to(self) # duck typing, expects a middleware to 
implement `#apply_to`
     end
   end
 end
diff --git a/bindings/ruby/lib/opendal_ruby/entry.rb 
b/bindings/ruby/lib/opendal_ruby/operator_info.rb
similarity index 86%
copy from bindings/ruby/lib/opendal_ruby/entry.rb
copy to bindings/ruby/lib/opendal_ruby/operator_info.rb
index 7dfc86d85..8951d9be6 100644
--- a/bindings/ruby/lib/opendal_ruby/entry.rb
+++ b/bindings/ruby/lib/opendal_ruby/operator_info.rb
@@ -18,13 +18,9 @@
 # frozen_string_literal: true
 
 module OpenDAL
-  class Entry
-    # Returns the canonical data about an entry
-    def to_h
-      {
-        path: path,
-        metadata: metadata
-      }
+  class OperatorInfo
+    def inspect
+      "#<#{self.class.name} scheme: \"#{scheme}\", root: \"#{root}\">"
     end
   end
 end
diff --git a/bindings/ruby/src/lib.rs b/bindings/ruby/src/lib.rs
index f9bb91af3..d2a3f8d67 100644
--- a/bindings/ruby/src/lib.rs
+++ b/bindings/ruby/src/lib.rs
@@ -28,6 +28,7 @@ mod capability;
 mod io;
 mod lister;
 mod metadata;
+mod middlewares;
 mod operator;
 mod operator_info;
 
@@ -45,6 +46,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
     let _ = io::include(&gem_module);
     let _ = lister::include(ruby, &gem_module);
     let _ = operator_info::include(&gem_module);
+    let _ = middlewares::include(&gem_module);
 
     Ok(())
 }
diff --git a/bindings/ruby/src/lister.rs b/bindings/ruby/src/lister.rs
index 0516c92ed..bc4d30e9a 100644
--- a/bindings/ruby/src/lister.rs
+++ b/bindings/ruby/src/lister.rs
@@ -22,7 +22,6 @@
 #![allow(rustdoc::invalid_html_tags, reason = "YARD's syntax for 
documentation")]
 #![allow(rustdoc::bare_urls, reason = "YARD's syntax for documentation")]
 
-use std::borrow::BorrowMut;
 use std::sync::Arc;
 use std::sync::Mutex;
 
@@ -80,6 +79,10 @@ impl Entry {
 /// Represents the result when list a directory
 ///
 /// This class is an enumerable.
+///
+/// # Safety
+///
+/// `Lister` is thread-safe.
 #[magnus::wrap(class = "OpenDAL::Lister", free_immediately, size)]
 pub struct Lister(Arc<Mutex<ocore::BlockingLister>>);
 
@@ -87,7 +90,8 @@ impl Iterator for Lister {
     type Item = Entry;
 
     fn next(&mut self) -> Option<Self::Item> {
-        if let Ok(mut inner) = self.0.borrow_mut().lock() {
+        // assumes low contention. also we want an entry eventually
+        if let Ok(mut inner) = self.0.lock() {
             match inner.next() {
                 Some(Ok(entry)) => Some(Entry(entry)),
                 _ => None,
@@ -109,6 +113,14 @@ impl Lister {
     /// Returns the next element.
     /// @return [Entry]
     fn each(&self) -> Result<Yield<Lister>, Error> {
+        // Magnus handles yielding to Ruby using an unsafe internal function,
+        // so we don’t manage the actual iteration loop ourselves.
+        //
+        // Since Ruby controls when values are pulled from the iterator,
+        // and could potentially call `each` from multiple threads or fibers,
+        // we wrap the underlying lister in `Arc<Mutex<_>>` to ensure thread 
safety.
+        //
+        // Multi-threaded iteration is rare in Ruby, but this design ensures 
thread safety.
         Ok(Yield::Iter(Lister(self.0.clone())))
     }
 }
diff --git a/bindings/ruby/src/metadata.rs b/bindings/ruby/src/metadata.rs
index dc1a7cf59..32ff89d51 100644
--- a/bindings/ruby/src/metadata.rs
+++ b/bindings/ruby/src/metadata.rs
@@ -42,6 +42,19 @@ impl Metadata {
 }
 
 impl Metadata {
+    /// @yard
+    /// @def mode
+    /// The entry mode of this Metadata.
+    /// @return [String] one of `File`, `Directory`, `Unknown`.
+    ///    `Unknown` means OpenDAL doesn't have actions to the entry.
+    pub fn mode(&self) -> &str {
+        match self.0.mode() {
+            ocore::EntryMode::FILE => "File",
+            ocore::EntryMode::DIR => "Directory",
+            ocore::EntryMode::Unknown => "Unknown",
+        }
+    }
+
     /// @yard
     /// @def content_disposition
     /// Content-Disposition of this object
@@ -81,26 +94,11 @@ impl Metadata {
     pub fn etag(&self) -> Option<&str> {
         self.0.etag()
     }
-
-    /// @yard
-    /// @def file?
-    /// Returns `True` if this is a file.
-    /// @return [Boolean]
-    pub fn is_file(&self) -> bool {
-        self.0.is_file()
-    }
-
-    /// @yard
-    /// @def dir?
-    /// Returns `True` if this is a directory.
-    /// @return [Boolean]
-    pub fn is_dir(&self) -> bool {
-        self.0.is_dir()
-    }
 }
 
 pub fn include(gem_module: &RModule) -> Result<(), Error> {
     let class = gem_module.define_class("Metadata", class::object())?;
+    class.define_method("mode", method!(Metadata::mode, 0))?;
     class.define_method(
         "content_disposition",
         method!(Metadata::content_disposition, 0),
@@ -109,8 +107,6 @@ pub fn include(gem_module: &RModule) -> Result<(), Error> {
     class.define_method("content_md5", method!(Metadata::content_md5, 0))?;
     class.define_method("content_type", method!(Metadata::content_type, 0))?;
     class.define_method("etag", method!(Metadata::etag, 0))?;
-    class.define_method("file?", method!(Metadata::is_file, 0))?;
-    class.define_method("dir?", method!(Metadata::is_dir, 0))?;
 
     Ok(())
 }
diff --git a/bindings/ruby/src/middlewares.rs b/bindings/ruby/src/middlewares.rs
new file mode 100644
index 000000000..0b271aece
--- /dev/null
+++ b/bindings/ruby/src/middlewares.rs
@@ -0,0 +1,167 @@
+// 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::sync::Arc;
+use std::sync::Mutex;
+use std::time::Duration;
+
+use magnus::class;
+use magnus::method;
+use magnus::prelude::*;
+use magnus::Error;
+use magnus::RModule;
+use magnus::Ruby;
+
+use opendal::raw::Accessor as OCoreAccessor;
+use opendal::raw::Layer as OCoreLayer;
+
+use crate::operator::Operator;
+use crate::*;
+
+// Applies an OpenDAL layer (treated as middleware in Ruby).
+//
+// Magnus provides `TypedData` and the `magnus::wrap` macro for strong typing 
between Ruby and Rust,
+// but we avoid exposing Rust types directly in Ruby to allow packaging layers 
in separate gems.
+//
+// Instead, we rely on duck typing: each Ruby-defined layer must implement 
`#apply_to(operator)`.
+// This internal function helps apply a Rust layer by locking the inner Layer 
and wrapping the result
+// back into an `Operator`.
+fn apply_layer<T>(
+    ruby: &Ruby,
+    inner: &Arc<Mutex<T>>,
+    operator: &Operator,
+    name: &'static str,
+) -> Result<Operator, Error>
+where
+    T: OCoreLayer<OCoreAccessor> + Clone + Send + Sync + 'static,
+{
+    let guard = inner.lock().map_err(|_| {
+        Error::new(
+            ruby.exception_runtime_error(),
+            format!("poisoned {} mutex", name),
+        )
+    })?;
+
+    let layered = operator.operator.clone().layer(guard.clone());
+
+    Ok(Operator::from_operator(layered))
+}
+
+/// @yard
+/// Adds retry for temporary failed operations.
+///
+/// See [`opendal::layers::RetryLayer`] for more information.
+#[magnus::wrap(class = "OpenDAL::RetryMiddleware")]
+struct RetryMiddleware(Arc<Mutex<ocore::layers::RetryLayer>>);
+
+impl RetryMiddleware {
+    fn new() -> Self {
+        Self(Arc::new(Mutex::new(ocore::layers::RetryLayer::default())))
+    }
+
+    fn apply_to(ruby: &Ruby, rb_self: &Self, operator: &Operator) -> 
Result<Operator, Error> {
+        apply_layer(ruby, &rb_self.0, operator, "RetryMiddleware")
+    }
+}
+
+/// @yard
+/// Adds concurrent request limit.
+///
+/// See [`opendal::layers::ConcurrentLimitLayer`] for more information.
+#[magnus::wrap(class = "OpenDAL::ConcurrentLimitMiddleware")]
+struct 
ConcurrentLimitMiddleware(Arc<Mutex<ocore::layers::ConcurrentLimitLayer>>);
+
+impl ConcurrentLimitMiddleware {
+    fn new(permits: usize) -> Self {
+        Self(Arc::new(Mutex::new(
+            ocore::layers::ConcurrentLimitLayer::new(permits),
+        )))
+    }
+
+    fn apply_to(ruby: &Ruby, rb_self: &Self, operator: &Operator) -> 
Result<Operator, Error> {
+        apply_layer(ruby, &rb_self.0, operator, "ConcurrentLimitMiddleware")
+    }
+}
+
+/// @yard
+/// Adds a bandwidth rate limiter to the underlying services.
+///
+/// See [`opendal::layers::ThrottleLayer`] for more information.
+#[magnus::wrap(class = "OpenDAL::ThrottleMiddleware")]
+struct ThrottleMiddleware(Arc<Mutex<ocore::layers::ThrottleLayer>>);
+
+impl ThrottleMiddleware {
+    fn new(bandwidth: u32, burst: u32) -> Self {
+        Self(Arc::new(Mutex::new(ocore::layers::ThrottleLayer::new(
+            bandwidth, burst,
+        ))))
+    }
+
+    fn apply_to(ruby: &Ruby, rb_self: &Self, operator: &Operator) -> 
Result<Operator, Error> {
+        apply_layer(ruby, &rb_self.0, operator, "ConcurrentLimitMiddleware")
+    }
+}
+
+fn parse_duration(ruby: &Ruby, val: f64) -> Result<Duration, Error> {
+    Duration::try_from_secs_f64(val).map_err(|e| {
+        Error::new(
+            ruby.exception_arg_error(),
+            format!("invalid float duration: {e}"),
+        )
+    })
+}
+
+/// @yard
+/// Adds timeout for every operation to avoid slow or unexpected hang 
operations.
+///
+/// See [`opendal::layers::TimeoutLayer`] for more information.
+#[magnus::wrap(class = "OpenDAL::TimeoutMiddleware")]
+struct TimeoutMiddleware(Arc<Mutex<ocore::layers::TimeoutLayer>>);
+
+impl TimeoutMiddleware {
+    fn new(ruby: &Ruby, timeout: f64, io_timeout: f64) -> Result<Self, Error> {
+        let layer = ocore::layers::TimeoutLayer::new()
+            .with_timeout(parse_duration(ruby, timeout)?)
+            .with_io_timeout(parse_duration(ruby, io_timeout)?);
+        Ok(Self(Arc::new(Mutex::new(layer))))
+    }
+
+    fn apply_to(ruby: &Ruby, rb_self: &Self, operator: &Operator) -> 
Result<Operator, Error> {
+        apply_layer(ruby, &rb_self.0, operator, "TimeoutMiddleware")
+    }
+}
+
+pub fn include(gem_module: &RModule) -> Result<(), Error> {
+    let retry = gem_module.define_class("RetryMiddleware", class::object())?;
+    retry.define_singleton_method("new", function!(RetryMiddleware::new, 0))?;
+    retry.define_method("apply_to", method!(RetryMiddleware::apply_to, 1))?;
+
+    let concurrent_limit = 
gem_module.define_class("ConcurrentLimitMiddleware", class::object())?;
+    concurrent_limit
+        .define_singleton_method("new", 
function!(ConcurrentLimitMiddleware::new, 1))?;
+    concurrent_limit.define_method("apply_to", 
method!(ConcurrentLimitMiddleware::apply_to, 1))?;
+
+    let throttle_middleware = gem_module.define_class("ThrottleMiddleware", 
class::object())?;
+    throttle_middleware.define_singleton_method("new", 
function!(ThrottleMiddleware::new, 2))?;
+    throttle_middleware.define_method("apply_to", 
method!(ThrottleMiddleware::apply_to, 1))?;
+
+    let timeout_middleware = gem_module.define_class("TimeoutMiddleware", 
class::object())?;
+    timeout_middleware.define_singleton_method("new", 
function!(TimeoutMiddleware::new, 2))?;
+    timeout_middleware.define_method("apply_to", 
method!(TimeoutMiddleware::apply_to, 1))?;
+
+    Ok(())
+}
diff --git a/bindings/ruby/src/operator.rs b/bindings/ruby/src/operator.rs
index f43a98b7b..75bc89d04 100644
--- a/bindings/ruby/src/operator.rs
+++ b/bindings/ruby/src/operator.rs
@@ -46,8 +46,36 @@ use crate::*;
 /// @yard
 /// The entrypoint for operating with file services and files.
 #[magnus::wrap(class = "OpenDAL::Operator", free_immediately, size)]
-#[derive(Clone, Debug)]
-pub struct Operator(ocore::BlockingOperator);
+pub struct Operator {
+    // We keep a reference to an `Operator` because:
+    // 1. Some builder functions exist only with the `Operator` struct.
+    // 2. Some builder functions don't exist in the `BlockingOperator`, e.g., 
`Operator::layer`, builder methods.
+    //
+    // We don't support async because:
+    // 1. Ractor and async is not stable.
+    // 2. magnus doesn't release GVL lock during operations yet.
+    // 3. Majority of use cases are still blocking.
+    //
+    // In practice, we will not use operator directly because of the async 
support.
+    pub operator: ocore::Operator,
+    // The cached blocking operator coming from `Operator::blocking`.
+    // Important to keep in sync after making changes to the `Operator`.
+    //
+    // We declare this `BlockingOperator` to state the intent of assumptions 
instead of
+    // getting a `BlockingOperator` for an operation dynamically every time.
+    pub blocking: ocore::BlockingOperator,
+}
+
+impl Operator {
+    // Convenience helper to construct operator
+    #[inline]
+    pub(crate) fn from_operator(operator: ocore::Operator) -> Operator {
+        // doesn't change `operator`'s state, but creates a new pointer to the 
inner accessor
+        let blocking = operator.blocking();
+
+        Operator { operator, blocking }
+    }
+}
 
 impl Operator {
     fn new(
@@ -64,9 +92,9 @@ impl Operator {
         let options = options.unwrap_or_default();
 
         let op = ocore::Operator::via_iter(scheme, options)
-            .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))?
-            .blocking();
-        Ok(Operator(op))
+            .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))?;
+
+        Ok(Operator::from_operator(op))
     }
 
     /// @yard
@@ -76,7 +104,7 @@ impl Operator {
     /// @return [String]
     fn read(ruby: &Ruby, rb_self: &Self, path: String) -> Result<bytes::Bytes, 
Error> {
         let buffer = rb_self
-            .0
+            .blocking
             .read(&path)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))?;
         Ok(buffer.to_bytes())
@@ -90,7 +118,7 @@ impl Operator {
     /// @return [nil]
     fn write(ruby: &Ruby, rb_self: &Self, path: String, bs: RString) -> 
Result<(), Error> {
         rb_self
-            .0
+            .blocking
             .write(&path, bs.to_bytes())
             .map(|_| ())
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
@@ -103,7 +131,7 @@ impl Operator {
     /// @return [Metadata]
     fn stat(ruby: &Ruby, rb_self: &Self, path: String) -> Result<Metadata, 
Error> {
         rb_self
-            .0
+            .blocking
             .stat(&path)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
             .map(Metadata::new)
@@ -114,7 +142,7 @@ impl Operator {
     /// Gets capabilities of the current operator
     /// @return [Capability]
     fn capability(&self) -> Result<Capability, Error> {
-        let capability = self.0.info().full_capability();
+        let capability = self.blocking.info().full_capability();
         Ok(Capability::new(capability))
     }
 
@@ -126,7 +154,7 @@ impl Operator {
     /// @return [nil]
     fn create_dir(ruby: &Ruby, rb_self: &Self, path: String) -> Result<(), 
Error> {
         rb_self
-            .0
+            .blocking
             .create_dir(&path)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
     }
@@ -138,7 +166,7 @@ impl Operator {
     /// @return [nil]
     fn delete(ruby: &Ruby, rb_self: &Self, path: String) -> Result<(), Error> {
         rb_self
-            .0
+            .blocking
             .delete(&path)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
     }
@@ -150,7 +178,7 @@ impl Operator {
     /// @return [Boolean]
     fn exists(ruby: &Ruby, rb_self: &Self, path: String) -> Result<bool, 
Error> {
         rb_self
-            .0
+            .blocking
             .exists(&path)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
     }
@@ -163,7 +191,7 @@ impl Operator {
     /// @return [nil]
     fn rename(ruby: &Ruby, rb_self: &Self, from: String, to: String) -> 
Result<(), Error> {
         rb_self
-            .0
+            .blocking
             .rename(&from, &to)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
     }
@@ -175,7 +203,7 @@ impl Operator {
     /// @return [nil]
     fn remove_all(ruby: &Ruby, rb_self: &Self, path: String) -> Result<(), 
Error> {
         rb_self
-            .0
+            .blocking
             .remove_all(&path)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
     }
@@ -188,7 +216,7 @@ impl Operator {
     /// @return [nil]
     fn copy(ruby: &Ruby, rb_self: &Self, from: String, to: String) -> 
Result<(), Error> {
         rb_self
-            .0
+            .blocking
             .copy(&from, &to)
             .map_err(|err| Error::new(ruby.exception_runtime_error(), 
err.to_string()))
     }
@@ -201,7 +229,7 @@ impl Operator {
     /// @raise [ArgumentError] invalid mode, or when the mode is not unique
     /// @return [OpenDAL::IO]
     fn open(ruby: &Ruby, rb_self: &Self, path: String, mode: String) -> 
Result<Io, Error> {
-        let operator = rb_self.0.clone();
+        let operator = rb_self.blocking.clone();
         Io::new(ruby, operator, path, mode)
     }
 
@@ -222,7 +250,7 @@ impl Operator {
         )?;
         let (limit, start_after, recursive) = kwargs.optional;
 
-        let mut builder = rb_self.0.clone().lister_with(&path);
+        let mut builder = rb_self.blocking.lister_with(&path);
 
         if let Some(limit) = limit {
             builder = builder.limit(limit);
@@ -243,9 +271,12 @@ impl Operator {
         Ok(Lister::new(lister))
     }
 
+    /// @yard
+    /// @def info
     /// Gets meta information of the underlying accessor.
+    /// @return [OperatorInfo]
     fn info(&self) -> Result<OperatorInfo, Error> {
-        Ok(OperatorInfo(self.0.info()))
+        Ok(OperatorInfo(self.blocking.info()))
     }
 }
 
diff --git a/bindings/ruby/test/blocking_op_test.rb 
b/bindings/ruby/test/blocking_op_test.rb
index 0bdb5bd85..f0c1d71d2 100644
--- a/bindings/ruby/test/blocking_op_test.rb
+++ b/bindings/ruby/test/blocking_op_test.rb
@@ -103,4 +103,10 @@ class OpenDalTest < ActiveSupport::TestCase
 
     io.close
   end
+
+  test "middleware applies a middleware" do
+    @op.middleware(OpenDAL::RetryMiddleware.new)
+
+    assert @op.is_a?(OpenDAL::Operator)
+  end
 end
diff --git a/bindings/ruby/lib/opendal_ruby/entry.rb 
b/bindings/ruby/test/middlewares_test.rb
similarity index 59%
copy from bindings/ruby/lib/opendal_ruby/entry.rb
copy to bindings/ruby/test/middlewares_test.rb
index 7dfc86d85..bf239819e 100644
--- a/bindings/ruby/lib/opendal_ruby/entry.rb
+++ b/bindings/ruby/test/middlewares_test.rb
@@ -17,14 +17,30 @@
 
 # frozen_string_literal: true
 
-module OpenDAL
-  class Entry
-    # Returns the canonical data about an entry
-    def to_h
-      {
-        path: path,
-        metadata: metadata
-      }
+require "test_helper"
+
+class MiddlewaresTest < ActiveSupport::TestCase
+  test "builds a retry middleware" do
+    assert_nothing_raised do
+      OpenDAL::RetryMiddleware.new
+    end
+  end
+
+  test "builds a concurrent limit middleware" do
+    assert_nothing_raised do
+      OpenDAL::ConcurrentLimitMiddleware.new(10)
+    end
+  end
+
+  test "builds a throttle middleware" do
+    assert_nothing_raised do
+      OpenDAL::ThrottleMiddleware.new(100, 10)
+    end
+  end
+
+  test "builds a timeout middleware" do
+    assert_nothing_raised do
+      OpenDAL::TimeoutMiddleware.new(1.23, 1.0)
     end
   end
 end

Reply via email to