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 0840c3aee45dc9caf32b2cc366f19abbc40276a3
Author: Erick Guan <297343+erickg...@users.noreply.github.com>
AuthorDate: Sun Mar 30 14:54:09 2025 +0200

    feat(bindings/ruby): support Ruby's IO#new parameters
---
 bindings/ruby/src/io.rs       | 248 +++++++++++++++++++++++++-----------------
 bindings/ruby/src/operator.rs |  16 ++-
 2 files changed, 162 insertions(+), 102 deletions(-)

diff --git a/bindings/ruby/src/io.rs b/bindings/ruby/src/io.rs
index ca04c2545..082ff1b9e 100644
--- a/bindings/ruby/src/io.rs
+++ b/bindings/ruby/src/io.rs
@@ -23,7 +23,6 @@
 #![allow(rustdoc::bare_urls, reason = "YARD's syntax for documentation")]
 
 use std::cell::RefCell;
-use std::collections::HashSet;
 use std::io::BufRead;
 use std::io::Read;
 use std::io::Seek;
@@ -31,10 +30,13 @@ use std::io::SeekFrom;
 use std::io::Write;
 
 use magnus::class;
+use magnus::io::FMode;
 use magnus::method;
 use magnus::prelude::*;
 use magnus::Error;
+use magnus::RHash;
 use magnus::RModule;
+use magnus::Value;
 
 use crate::*;
 
@@ -49,15 +51,30 @@ use crate::*;
 /// `OpenDAL::IO` is similar to Ruby's `IO` and `StringIO` for accessing files.
 ///
 /// You can't create an instance of `OpenDAL::IO` except using 
{OpenDAL::Operator#open}.
+///
+/// Constraints:
+/// - Only available for reading and writing
+/// - Writing doesn't support seek.
 #[magnus::wrap(class = "OpenDAL::IO", free_immediately, size)]
-pub struct Io(RefCell<FileState>);
+pub struct Io(RefCell<IoHandle>);
 
 enum FileState {
-    Reader(ocore::blocking::StdReader, bool), // bool indicates binary mode
-    Writer(ocore::blocking::StdWriter, bool), // bool indicates binary mode
+    Reader(ocore::blocking::StdReader),
+    Writer(ocore::blocking::StdWriter),
     Closed,
 }
 
+struct IoHandle {
+    state: FileState,
+    fmode: FMode,
+    #[allow(dead_code)]
+    external_encoding_name: Option<String>,
+    #[allow(dead_code)]
+    internal_encoding_name: Option<String>,
+    #[allow(dead_code)]
+    encoding_flags: i32,
+}
+
 pub fn format_io_error(err: std::io::Error) -> Error {
     Error::new(exception::runtime_error(), err.to_string())
 }
@@ -70,42 +87,44 @@ impl Io {
         ruby: &Ruby,
         operator: ocore::blocking::Operator,
         path: String,
-        mode: String,
+        mut mode: Value,
+        mut permission: Value,
+        kwargs: RHash,
     ) -> Result<Self, Error> {
-        let mut mode_flags = HashSet::new();
-        let is_unique = mode.chars().all(|c| mode_flags.insert(c));
-        if !is_unique {
-            return Err(Error::new(
-                ruby.exception_arg_error(),
-                format!("Invalid access mode {mode}"),
-            ));
-        }
-
-        let binary_mode = mode_flags.contains(&'b');
+        let (_open_flags, fmode, encoding) =
+            ruby.io_extract_modeenc(&mut mode, &mut permission, &kwargs)?;
 
-        if mode_flags.contains(&'r') {
-            Ok(Self(RefCell::new(FileState::Reader(
-                operator
-                    .reader(&path)
-                    .map_err(format_magnus_error)?
-                    .into_std_read(..)
-                    .map_err(format_magnus_error)?,
-                binary_mode,
-            ))))
-        } else if mode_flags.contains(&'w') {
-            Ok(Self(RefCell::new(FileState::Writer(
+        let state =
+            // Create reader if mode supports reading
+            if fmode.contains(FMode::READ) {
+                FileState::Reader(
+                    operator
+                        .reader(&path)
+                        .map_err(format_magnus_error)?
+                        .into_std_read(..)
+                        .map_err(format_magnus_error)?,
+                )
+            } else if fmode.contains(FMode::WRITE) {
+                FileState::Writer(
                 operator
                     .writer(&path)
                     .map_err(format_magnus_error)?
                     .into_std_write(),
-                binary_mode,
-            ))))
+            )
         } else {
-            Err(Error::new(
-                ruby.exception_runtime_error(),
-                format!("OpenDAL doesn't support mode: {mode}"),
-            ))
-        }
+            return Err(Error::new(
+                ruby.exception_arg_error(),
+                "Invalid mode: must open for reading or writing",
+            ));
+        };
+
+        Ok(Self(RefCell::new(IoHandle {
+            state,
+            fmode,
+            external_encoding_name: encoding.external.map(|e| e.to_string()),
+            internal_encoding_name: encoding.internal.map(|e| e.to_string()),
+            encoding_flags: encoding.flags,
+        })))
     }
 
     /// @yard
@@ -114,18 +133,12 @@ impl Io {
     /// @return [nil]
     /// @raise [IOError] when operate on a closed stream
     fn binary_mode(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
-        let mut cell = rb_self.0.borrow_mut();
-        match &mut *cell {
-            FileState::Reader(_, ref mut is_binary_mode) => {
-                *is_binary_mode = true;
-                Ok(())
-            }
-            FileState::Writer(_, ref mut is_binary_mode) => {
-                *is_binary_mode = true;
-                Ok(())
-            }
-            FileState::Closed => Err(Error::new(ruby.exception_io_error(), 
"closed stream")),
+        let mut handle = rb_self.0.borrow_mut();
+        if let FileState::Closed = handle.state {
+            return Err(Error::new(ruby.exception_io_error(), "closed stream"));
         }
+        handle.fmode = FMode::new(handle.fmode.bits() | FMode::BINARY_MODE);
+        Ok(())
     }
 
     /// @yard
@@ -134,11 +147,11 @@ impl Io {
     /// @return [Boolean]
     /// @raise [IOError] when operate on a closed stream
     fn is_binary_mode(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
-        match *rb_self.0.borrow() {
-            FileState::Reader(_, is_binary_mode) => Ok(is_binary_mode),
-            FileState::Writer(_, is_binary_mode) => Ok(is_binary_mode),
-            FileState::Closed => Err(Error::new(ruby.exception_io_error(), 
"closed stream")),
+        let handle = rb_self.0.borrow();
+        if let FileState::Closed = handle.state {
+            return Err(Error::new(ruby.exception_io_error(), "closed stream"));
         }
+        Ok(handle.fmode.contains(FMode::BINARY_MODE))
     }
 
     /// @yard
@@ -146,12 +159,11 @@ impl Io {
     /// Close streams.
     /// @return [nil]
     fn close(&self) -> Result<(), Error> {
-        // skips closing reader because `StdReader` doesn't have `close()`.
-        let mut cell = self.0.borrow_mut();
-        if let FileState::Writer(writer, _) = &mut *cell {
+        let mut handle = self.0.borrow_mut();
+        if let FileState::Writer(writer) = &mut handle.state {
             writer.close().map_err(format_io_error)?;
         }
-        *cell = FileState::Closed;
+        handle.state = FileState::Closed;
         Ok(())
     }
 
@@ -160,7 +172,10 @@ impl Io {
     /// Closes the read stream.
     /// @return [nil]
     fn close_read(&self) -> Result<(), Error> {
-        *self.0.borrow_mut() = FileState::Closed;
+        let mut handle = self.0.borrow_mut();
+        if let FileState::Reader(_) = &handle.state {
+            handle.state = FileState::Closed;
+        }
         Ok(())
     }
 
@@ -169,11 +184,11 @@ impl Io {
     /// Closes the write stream.
     /// @return [nil]
     fn close_write(&self) -> Result<(), Error> {
-        let mut cell = self.0.borrow_mut();
-        if let FileState::Writer(writer, _) = &mut *cell {
+        let mut handle = self.0.borrow_mut();
+        if let FileState::Writer(writer) = &mut handle.state {
             writer.close().map_err(format_io_error)?;
+            handle.state = FileState::Closed;
         }
-        *cell = FileState::Closed;
         Ok(())
     }
 
@@ -182,7 +197,8 @@ impl Io {
     /// Returns if streams are closed.
     /// @return [Boolean]
     fn is_closed(&self) -> Result<bool, Error> {
-        Ok(matches!(*self.0.borrow(), FileState::Closed))
+        let handle = self.0.borrow();
+        Ok(matches!(handle.state, FileState::Closed))
     }
 
     /// @yard
@@ -190,7 +206,8 @@ impl Io {
     /// Returns if the read stream is closed.
     /// @return [Boolean]
     fn is_closed_read(&self) -> Result<bool, Error> {
-        Ok(!matches!(*self.0.borrow(), FileState::Reader(_, _)))
+        let handle = self.0.borrow();
+        Ok(!matches!(handle.state, FileState::Reader(_)))
     }
 
     /// @yard
@@ -198,7 +215,8 @@ impl Io {
     /// Returns if the write stream is closed.
     /// @return [Boolean]
     fn is_closed_write(&self) -> Result<bool, Error> {
-        Ok(!matches!(*self.0.borrow(), FileState::Writer(_, _)))
+        let handle = self.0.borrow();
+        Ok(!matches!(handle.state, FileState::Writer(_)))
     }
 }
 
@@ -210,8 +228,9 @@ impl Io {
     ///
     /// @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> {
-        // FIXME: consider what to return exactly
-        if let FileState::Reader(reader, _) = &mut *rb_self.0.borrow_mut() {
+        let mut handle = rb_self.0.borrow_mut();
+
+        if let FileState::Reader(reader) = &mut handle.state {
             let buffer = match size {
                 Some(size) => {
                     let mut bs = vec![0; size];
@@ -230,7 +249,7 @@ impl Io {
         } else {
             Err(Error::new(
                 ruby.exception_runtime_error(),
-                "I/O operation failed for reading on closed file.",
+                "I/O operation failed for reading on write-only file.",
             ))
         }
     }
@@ -241,7 +260,9 @@ impl Io {
     /// @return [String]
     // TODO: extend readline with parameters
     fn readline(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
-        if let FileState::Reader(reader, _) = &mut *rb_self.0.borrow_mut() {
+        let mut handle = rb_self.0.borrow_mut();
+
+        if let FileState::Reader(reader) = &mut handle.state {
             let mut buffer = String::new();
             let size = reader.read_line(&mut buffer).map_err(format_io_error)?;
             if size == 0 {
@@ -255,7 +276,7 @@ impl Io {
         } else {
             Err(Error::new(
                 ruby.exception_runtime_error(),
-                "I/O operation failed for reading on closed file.",
+                "I/O operation failed for reading on write-only file.",
             ))
         }
     }
@@ -266,15 +287,16 @@ impl Io {
     /// @param buffer [String]
     /// @return [Integer] the written byte size
     fn write(ruby: &Ruby, rb_self: &Self, bs: String) -> Result<usize, Error> {
-        if let FileState::Writer(writer, _) = &mut *rb_self.0.borrow_mut() {
-            Ok(writer
-                .write_all(bs.as_bytes())
-                .map(|_| bs.len())
-                .map_err(format_io_error)?)
+        let mut handle = rb_self.0.borrow_mut();
+
+        if let FileState::Writer(writer) = &mut handle.state {
+            let bytes_written = bs.len();
+            writer.write_all(bs.as_bytes()).map_err(format_io_error)?;
+            Ok(bytes_written)
         } else {
             Err(Error::new(
                 ruby.exception_runtime_error(),
-                "I/O operation failed for reading on write only file.",
+                "I/O operation failed for writing on read-only file.",
             ))
         }
     }
@@ -289,49 +311,77 @@ impl Io {
     ///   - 0 = IO:SEEK_SET (Start)
     ///   - 1 = IO:SEEK_CUR (Current position)
     ///   - 2 = IO:SEEK_END (From the end)
-    ///     @return [Integer] always 0 if the seek operation is successful
+    ///
+    /// @return [Integer] always 0 if the seek operation is successful
     fn seek(ruby: &Ruby, rb_self: &Self, offset: i64, whence: u8) -> 
Result<u8, Error> {
-        match &mut *rb_self.0.borrow_mut() {
-            FileState::Reader(reader, _) => {
-                let whence = match whence {
-                    0 => SeekFrom::Start(offset as u64),
-                    1 => SeekFrom::Current(offset),
-                    2 => SeekFrom::End(offset),
-                    _ => return Err(Error::new(ruby.exception_arg_error(), 
"invalid whence")),
-                };
-
-                reader.seek(whence).map_err(format_io_error)?;
-
-                Ok(0)
-            }
-            FileState::Writer(_, _) => Err(Error::new(
-                ruby.exception_runtime_error(),
-                "I/O operation failed for reading on write only file.",
-            )),
-            FileState::Closed => Err(Error::new(
+        let mut handle = rb_self.0.borrow_mut();
+
+        if let FileState::Reader(reader) = &mut handle.state {
+            // Calculate the new position first
+            let new_reader_position = match whence {
+                0 => {
+                    // SEEK_SET - absolute position
+                    if offset < 0 {
+                        return Err(Error::new(
+                            ruby.exception_runtime_error(),
+                            "Cannot seek to negative reader_position.",
+                        ));
+                    }
+                    offset as u64
+                }
+                1 => {
+                    // SEEK_CUR - relative to current position
+                    let position = 
reader.stream_position().map_err(format_io_error)?;
+                    if offset < 0 && (-offset as u64) > position {
+                        return Err(Error::new(
+                            ruby.exception_runtime_error(),
+                            "Cannot seek before start of stream.",
+                        ));
+                    }
+                    (position as i64 + offset) as u64
+                }
+                2 => {
+                    // SEEK_END
+                    let end_pos = 
reader.seek(SeekFrom::End(0)).map_err(format_io_error)?;
+                    if offset < 0 && (-offset as u64) > end_pos {
+                        return Err(Error::new(
+                            ruby.exception_runtime_error(),
+                            "Cannot seek before start of stream.",
+                        ));
+                    }
+                    (end_pos as i64 + offset) as u64
+                }
+                _ => return Err(Error::new(ruby.exception_arg_error(), 
"invalid whence")),
+            };
+
+            let _ = reader.seek(std::io::SeekFrom::Start(new_reader_position));
+        } else {
+            return Err(Error::new(
                 ruby.exception_runtime_error(),
-                "I/O operation failed for reading on closed file.",
-            )),
+                "Cannot seek from end on write-only stream.",
+            ));
         }
+
+        Ok(0)
     }
 
     /// @yard
     /// @def tell
-    /// Returns the current position of the file pointer in the stream.
-    /// @return [Integer] the current position in bytes
+    /// Returns the current reader_position of the file pointer in the stream.
+    /// @return [Integer] the current reader_position in bytes
     /// @raise [IOError] when cannot operate on the operation mode
     fn tell(ruby: &Ruby, rb_self: &Self) -> Result<u64, Error> {
-        match &mut *rb_self.0.borrow_mut() {
-            FileState::Reader(reader, _) => {
-                Ok(reader.stream_position().map_err(format_io_error)?)
-            }
-            FileState::Writer(_, _) => Err(Error::new(
+        let mut handle = rb_self.0.borrow_mut();
+
+        match &mut handle.state {
+            FileState::Reader(reader) => 
Ok(reader.stream_position().map_err(format_io_error)?),
+            FileState::Writer(_) => Err(Error::new(
                 ruby.exception_runtime_error(),
                 "I/O operation failed for reading on write only file.",
             )),
             FileState::Closed => Err(Error::new(
                 ruby.exception_runtime_error(),
-                "I/O operation failed for reading on closed file.",
+                "I/O operation failed for tell on closed stream.",
             )),
         }
     }
diff --git a/bindings/ruby/src/operator.rs b/bindings/ruby/src/operator.rs
index a75082bbc..5d91f74a9 100644
--- a/bindings/ruby/src/operator.rs
+++ b/bindings/ruby/src/operator.rs
@@ -31,6 +31,7 @@ use magnus::prelude::*;
 use magnus::scan_args::get_kwargs;
 use magnus::scan_args::scan_args;
 use magnus::Error;
+use magnus::RHash;
 use magnus::RModule;
 use magnus::RString;
 use magnus::Ruby;
@@ -233,9 +234,18 @@ impl Operator {
     /// @param mode [String] operation mode, e.g., `r`, `w`, or `rb`.
     /// @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> {
+    fn open(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Io, Error> {
+        let args = scan_args::<(String,), (Option<Value>, Option<Value>), (), 
(), RHash, ()>(args)?;
+        let (path,) = args.required;
+        let (option_mode, option_permission) = args.optional;
+        let kwargs = args.keywords;
+
+        // Ruby handles Qnil safely (will not assign to Qnil)
+        let mode = option_mode.unwrap_or(ruby.str_new("r").as_value());
+        let permission = option_permission.unwrap_or(ruby.qnil().as_value());
+
         let operator = rb_self.blocking_op.clone();
-        Io::new(ruby, operator, path, mode)
+        Io::new(ruby, operator, path, mode, permission, kwargs)
     }
 
     /// @yard
@@ -293,7 +303,7 @@ pub fn include(gem_module: &RModule) -> Result<(), Error> {
     class.define_method("rename", method!(Operator::rename, 2))?;
     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("open", method!(Operator::open, -1))?;
     class.define_method("list", method!(Operator::list, -1))?;
     class.define_method("info", method!(Operator::info, 0))?;
 

Reply via email to