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

erickguan pushed a commit to branch ruby-binding-io
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit d3df8ddf3f394f1cac828a5d08df506f6f1d7c45
Author: Erick Guan <297343+erickg...@users.noreply.github.com>
AuthorDate: Fri Aug 29 22:30:40 2025 +0200

    feat(bindings/ruby): support read with size and output buffer
---
 bindings/ruby/src/io.rs       | 43 +++++++++++++++++++++++++++++++++----------
 bindings/ruby/test/io_test.rb | 32 ++++++++++++++++++++++++++++++--
 2 files changed, 63 insertions(+), 12 deletions(-)

diff --git a/bindings/ruby/src/io.rs b/bindings/ruby/src/io.rs
index 082ff1b9e..c1a2d5a90 100644
--- a/bindings/ruby/src/io.rs
+++ b/bindings/ruby/src/io.rs
@@ -33,9 +33,11 @@ use magnus::class;
 use magnus::io::FMode;
 use magnus::method;
 use magnus::prelude::*;
+use magnus::scan_args::scan_args;
 use magnus::Error;
 use magnus::RHash;
 use magnus::RModule;
+use magnus::RString;
 use magnus::Value;
 
 use crate::*;
@@ -223,29 +225,50 @@ impl Io {
 impl Io {
     /// Reads data from the stream.
     /// TODO:
-    ///   - support default parameters
     ///   - support encoding
     ///
-    /// @param size The maximum number of bytes to read. Reads all data if 
`None`.
-    fn read(ruby: &Ruby, rb_self: &Self, size: Option<usize>) -> 
Result<bytes::Bytes, Error> {
+    /// @param size [Integer, nil] The maximum number of bytes to read. Reads 
all data when not provided.
+    /// @param buffer [String, nil] The output buffer to append to.
+    fn read(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> 
Result<Option<bytes::Bytes>, Error> {
+        let args = scan_args::<(), (Option<Option<i64>>, Option<RString>), (), 
(), (), ()>(args)?;
+        let (option_size, mut option_output_buffer) = args.optional;
+        let size = option_size.unwrap_or_default(); // allow nil
+
         let mut handle = rb_self.0.borrow_mut();
 
         if let FileState::Reader(reader) = &mut handle.state {
-            let buffer = match size {
+            let buffer: Option<bytes::Bytes> = match size {
                 Some(size) => {
-                    let mut bs = vec![0; size];
+                    if size <= 0 {
+                        return Err(Error::new(
+                            ruby.exception_arg_error(),
+                            format!("negative length {} given", size),
+                        ));
+                    }
+                    let mut bs = vec![0; size as usize];
                     let n = reader.read(&mut bs).map_err(format_io_error)?;
-                    bs.truncate(n);
-                    bs
+                    if n == 0 && size > 0 {
+                        // when called at end of file, read(positive_integer) 
returns nil.
+                        None
+                    } else {
+                        bs.truncate(n);
+                        Some(bs.into())
+                    }
                 }
                 None => {
                     let mut buffer = Vec::new();
                     reader.read_to_end(&mut buffer).map_err(format_io_error)?;
-                    buffer
+                    Some(buffer.into())
                 }
             };
 
-            Ok(buffer.into())
+            // when provided the buffer parameter, append read buffer
+            if let (Some(output_buffer), Some(inner)) =
+                (option_output_buffer.as_mut(), buffer.as_ref()) {
+                    output_buffer.cat(inner);
+            }
+
+            Ok(buffer)
         } else {
             Err(Error::new(
                 ruby.exception_runtime_error(),
@@ -417,7 +440,7 @@ pub fn include(gem_module: &RModule) -> Result<(), Error> {
     class.define_method("closed?", method!(Io::is_closed, 0))?;
     class.define_method("closed_read?", method!(Io::is_closed_read, 0))?;
     class.define_method("closed_write?", method!(Io::is_closed_write, 0))?;
-    class.define_method("read", method!(Io::read, 1))?;
+    class.define_method("read", method!(Io::read, -1))?;
     class.define_method("write", method!(Io::write, 1))?;
     class.define_method("readline", method!(Io::readline, 0))?;
     class.define_method("seek", method!(Io::seek, 2))?;
diff --git a/bindings/ruby/test/io_test.rb b/bindings/ruby/test/io_test.rb
index 8a6060a0b..851fa5726 100644
--- a/bindings/ruby/test/io_test.rb
+++ b/bindings/ruby/test/io_test.rb
@@ -82,12 +82,40 @@ class IOTest < ActiveSupport::TestCase
   end
 
   test "#read reads" do
-    result = @io_read.read(nil)
+    result = @io_read.read
 
     assert_equal "Sample data for testing\nA line after title\n", result
     # should be `assert_equal Encoding::UTF_8, result.encoding`
   end
 
+  test "#read reads with nil" do
+    result = @io_read.read(nil)
+
+    assert_equal "Sample data for testing\nA line after title\n", result
+  end
+
+  test "#read reads with size" do
+    result = @io_read.read(6)
+
+    assert_equal "Sample", result
+  end
+
+  test "#read raises ArgumentError with -1" do
+    assert_raise(ArgumentError) do |err|
+    @io_read.read(-1)
+
+    assert_equal "negative length -1 given", err.message
+    end
+  end
+
+  test "#read reads to a buffer" do
+    buffer = "hi ".dup # unfreezes string literal
+    result = @io_read.read(6, buffer)
+
+    assert_equal "Sample", result
+    assert_equal "hi Sample", buffer
+  end
+
   test "#write writes" do
     @io_write.write("This is a sentence.")
     @io_write.close
@@ -130,7 +158,7 @@ class IOTest < ActiveSupport::TestCase
   end
 
   test "#eof? returns" do
-    @io_read.read(nil)
+    @io_read.read
     assert @io_read.eof?
   end
 

Reply via email to