lib/standard/stream: Renamed streams for more explicit denomination
[nit.git] / lib / standard / stream.nit
index c63e38a..24681fe 100644 (file)
@@ -1,36 +1,55 @@
 # This file is part of NIT ( http://www.nitlanguage.org ).
 #
-# Copyright 2004-2008 Jean Privat <jean@pryen.org>
-#
-# This file is free software, which comes along with NIT.  This software is
+# This file is free software, which comes along with NIT. This software is
 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-# without  even  the implied warranty of  MERCHANTABILITY or  FITNESS FOR A 
-# PARTICULAR PURPOSE.  You can modify it is you want,  provided this header
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. You can modify it is you want, provided this header
 # is kept unaltered, and a notification of the changes is added.
-# You  are  allowed  to  redistribute it and sell it, alone or is a part of
+# You are allowed to redistribute it and sell it, alone or is a part of
 # another product.
 
 # Input and output streams of characters
 module stream
 
-import string
+intrude import ropes
+import error
+
+in "C" `{
+       #include <unistd.h>
+       #include <string.h>
+       #include <signal.h>
+`}
+
+# Any kind of error that could be produced by an operation on Streams
+class IOError
+       super Error
+end
 
 # Abstract stream class
-interface IOS
+abstract class Stream
+       # Error produced by the file stream
+       #
+       #     var ifs = new FileReader.open("donotmakethisfile.binx")
+       #     ifs.read_all
+       #     ifs.close
+       #     assert ifs.last_error != null
+       var last_error: nullable IOError = null
+
        # close the stream
        fun close is abstract
 end
 
 # Abstract input streams
-interface IStream
-       super IOS
+abstract class Reader
+       super Stream
        # Read a character. Return its ASCII value, -1 on EOF or timeout
        fun read_char: Int is abstract
 
        # Read at most i bytes
        fun read(i: Int): String
        do
-               var s = new Buffer.with_capacity(i)
+               if last_error != null then return ""
+               var s = new FlatBuffer.with_capacity(i)
                while i > 0 and not eof do
                        var c = read_char
                        if c >= 0 then
@@ -42,18 +61,107 @@ interface IStream
        end
 
        # Read a string until the end of the line.
+       #
+       # The line terminator '\n' and '\r\n', if any, is removed in each line.
+       #
+       # ~~~
+       # var txt = "Hello\n\nWorld\n"
+       # var i = new StringReader(txt)
+       # assert i.read_line == "Hello"
+       # assert i.read_line == ""
+       # assert i.read_line == "World"
+       # assert i.eof
+       # ~~~
+       #
+       # Only LINE FEED (`\n`), CARRIAGE RETURN & LINE FEED (`\r\n`), and
+       # the end or file (EOF) is considered to delimit the end of lines.
+       # CARRIAGE RETURN (`\r`) alone is not used for the end of line.
+       #
+       # ~~~
+       # var txt2 = "Hello\r\n\n\rWorld"
+       # var i2 = new StringReader(txt2)
+       # assert i2.read_line == "Hello"
+       # assert i2.read_line == ""
+       # assert i2.read_line == "\rWorld"
+       # assert i2.eof
+       # ~~~
+       #
+       # NOTE: Use `append_line_to` if the line terminator needs to be preserved.
        fun read_line: String
        do
-               assert not eof
-               var s = new Buffer
+               if last_error != null then return ""
+               if eof then return ""
+               var s = new FlatBuffer
                append_line_to(s)
-               return s.to_s
+               return s.to_s.chomp
        end
 
+       # Read all the lines until the eof.
+       #
+       # The line terminator '\n' and `\r\n` is removed in each line,
+       #
+       # ~~~
+       # var txt = "Hello\n\nWorld\n"
+       # var i = new StringReader(txt)
+       # assert i.read_lines == ["Hello", "", "World"]
+       # ~~~
+       #
+       # This method is more efficient that splitting
+       # the result of `read_all`.
+       #
+       # NOTE: SEE `read_line` for details.
+       fun read_lines: Array[String]
+       do
+               var res = new Array[String]
+               while not eof do
+                       res.add read_line
+               end
+               return res
+       end
+
+       # Return an iterator that read each line.
+       #
+       # The line terminator '\n' and `\r\n` is removed in each line,
+       # The line are read with `read_line`. See this method for details.
+       #
+       # ~~~
+       # var txt = "Hello\n\nWorld\n"
+       # var i = new StringReader(txt)
+       # assert i.each_line.to_a == ["Hello", "", "World"]
+       # ~~~
+       #
+       # Unlike `read_lines` that read all lines at the call, `each_line` is lazy.
+       # Therefore, the stream should no be closed until the end of the stream.
+       #
+       # ~~~
+       # i = new StringReader(txt)
+       # var el = i.each_line
+       #
+       # assert el.item == "Hello"
+       # el.next
+       # assert el.item == ""
+       # el.next
+       #
+       # i.close
+       #
+       # assert not el.is_ok
+       # # closed before "world" is read
+       # ~~~
+       fun each_line: LineIterator do return new LineIterator(self)
+
        # Read all the stream until the eof.
+       #
+       # The content of the file is returned verbatim.
+       #
+       # ~~~
+       # var txt = "Hello\n\nWorld\n"
+       # var i = new StringReader(txt)
+       # assert i.read_all == txt
+       # ~~~
        fun read_all: String
        do
-               var s = new Buffer
+               if last_error != null then return ""
+               var s = new FlatBuffer
                while not eof do
                        var c = read_char
                        if c >= 0 then s.add(c.ascii)
@@ -62,15 +170,48 @@ interface IStream
        end
 
        # Read a string until the end of the line and append it to `s`.
+       #
+       # Unlike `read_line` and other related methods,
+       # the line terminator '\n', if any, is preserved in each line.
+       # Use the method `Text::chomp` to safely remove it.
+       #
+       # ~~~
+       # var txt = "Hello\n\nWorld\n"
+       # var i = new StringReader(txt)
+       # var b = new FlatBuffer
+       # i.append_line_to(b)
+       # assert b == "Hello\n"
+       # i.append_line_to(b)
+       # assert b == "Hello\n\n"
+       # i.append_line_to(b)
+       # assert b == txt
+       # assert i.eof
+       # ~~~
+       #
+       # If `\n` is not present at the end of the result, it means that
+       # a non-eol terminated last line was returned.
+       #
+       # ~~~
+       # var i2 = new StringReader("hello")
+       # assert not i2.eof
+       # var b2 = new FlatBuffer
+       # i2.append_line_to(b2)
+       # assert b2 == "hello"
+       # assert i2.eof
+       # ~~~
+       #
+       # NOTE: The single character LINE FEED (`\n`) delimits the end of lines.
+       # Therefore CARRIAGE RETURN & LINE FEED (`\r\n`) is also recognized.
        fun append_line_to(s: Buffer)
        do
+               if last_error != null then return
                loop
                        var x = read_char
                        if x == -1 then
                                if eof then return
                        else
                                var c = x.ascii
-                               s.push(c)
+                               s.chars.push(c)
                                if c == '\n' then return
                        end
                end
@@ -81,58 +222,138 @@ interface IStream
        fun eof: Bool is abstract
 end
 
+# Iterator returned by `Reader::each_line`.
+# See the aforementioned method for details.
+class LineIterator
+       super Iterator[String]
+
+       # The original stream
+       var stream: Reader
+
+       redef fun is_ok
+       do
+               var res = not stream.eof
+               if not res and close_on_finish then stream.close
+               return res
+       end
+
+       redef fun item
+       do
+               var line = self.line
+               if line == null then
+                       line = stream.read_line
+               end
+               self.line = line
+               return line
+       end
+
+       # The last line read (cache)
+       private var line: nullable String = null
+
+       redef fun next
+       do
+               # force the read
+               if line == null then item
+               # drop the line
+               line = null
+       end
+
+       # Close the stream when the stream is at the EOF.
+       #
+       # Default is false.
+       var close_on_finish = false is writable
+
+       redef fun finish
+       do
+               if close_on_finish then stream.close
+       end
+end
+
+# `ReadStream` capable of declaring if readable without blocking
+abstract class PollableReader
+       super Reader
+
+       # Is there something to read? (without blocking)
+       fun poll_in: Bool is abstract
+
+end
+
 # Abstract output stream
-interface OStream
-       super IOS
+abstract class Writer
+       super Stream
        # write a string
-       fun write(s: String) is abstract
+       fun write(s: Text) is abstract
 
        # Can the stream be used to write
        fun is_writable: Bool is abstract
 end
 
+# Things that can be efficienlty writen to a `WriteStream`
+#
+# The point of this interface it to allow is instance to be efficenty
+# writen into a `WriteStream` without having to allocate a big String object
+#
+# ready-to-save documents usually provide this interface.
+interface Writable
+       # Write itself to a `stream`
+       # The specific logic it let to the concrete subclasses
+       fun write_to(stream: Writer) is abstract
+
+       # Like `write_to` but return a new String (may be quite large)
+       #
+       # This funtionnality is anectodical, since the point
+       # of streamable object to to be efficienlty written to a
+       # stream without having to allocate and concatenate strings
+       fun write_to_string: String
+       do
+               var stream = new StringWriter
+               write_to(stream)
+               return stream.to_s
+       end
+end
+
+redef class Text
+       super Writable
+       redef fun write_to(stream) do stream.write(self)
+end
+
 # Input streams with a buffer
-abstract class BufferedIStream
-       super IStream
+abstract class BufferedReader
+       super Reader
        redef fun read_char
        do
-               assert not eof
-               if _buffer_pos >= _buffer.length then
-                       fill_buffer
-               end
-               if _buffer_pos >= _buffer.length then
+               if last_error != null then return -1
+               if eof then
+                       last_error = new IOError("Stream has reached eof")
                        return -1
                end
-               var c = _buffer[_buffer_pos]
+               var c = _buffer.chars[_buffer_pos]
                _buffer_pos += 1
                return c.ascii
        end
 
        redef fun read(i)
        do
-               var s = new Buffer.with_capacity(i)
-               var j = _buffer_pos
-               var k = _buffer.length
-               while i > 0 do
-                       if j >= k then
-                               fill_buffer
-                               if eof then return s.to_s
-                               j = _buffer_pos
-                               k = _buffer.length
-                       end
-                       while j < k and i > 0 do
-                               s.add(_buffer[j])
-                               j +=  1
-                               i -= 1
+               if last_error != null then return ""
+               if _buffer.length == _buffer_pos then
+                       if not eof then
+                               return read(i)
                        end
+                       return ""
                end
-               _buffer_pos = j
-               return s.to_s
+               if _buffer_pos + i >= _buffer.length then
+                       var from = _buffer_pos
+                       _buffer_pos = _buffer.length
+                       return _buffer.substring_from(from).to_s
+               end
+               _buffer_pos += i
+               return _buffer.substring(_buffer_pos - i, i).to_s
        end
 
        redef fun read_all
        do
-               var s = new Buffer
+               if last_error != null then return ""
+               var s = new FlatBuffer
                while not eof do
                        var j = _buffer_pos
                        var k = _buffer.length
@@ -144,14 +365,23 @@ abstract class BufferedIStream
                        fill_buffer
                end
                return s.to_s
-       end   
+       end
 
        redef fun append_line_to(s)
        do
                loop
                        # First phase: look for a '\n'
                        var i = _buffer_pos
-                       while i < _buffer.length and _buffer[i] != '\n' do i += 1
+                       while i < _buffer.length and _buffer.chars[i] != '\n' do i += 1
+
+                       var eol
+                       if i < _buffer.length then
+                               assert _buffer.chars[i] == '\n'
+                               i += 1
+                               eol = true
+                       else
+                               eol = false
+                       end
 
                        # if there is something to append
                        if i > _buffer_pos then
@@ -161,149 +391,114 @@ abstract class BufferedIStream
                                # Copy from the buffer to the string
                                var j = _buffer_pos
                                while j < i do
-                                       s.add(_buffer[j])
+                                       s.add(_buffer.chars[j])
                                        j += 1
                                end
+                               _buffer_pos = i
+                       else
+                               assert end_reached
+                               return
                        end
 
-                       if i < _buffer.length then
-                               # so \n is in _buffer[i]
-                               _buffer_pos = i + 1 # skip \n
+                       if eol then
+                               # so \n is found
                                return
                        else
                                # so \n is not found
-                               _buffer_pos = i
-                               if end_reached then
-                                       return
-                               else
-                                       fill_buffer
-                               end
+                               if end_reached then return
+                               fill_buffer
                        end
                end
        end
 
-       redef fun eof do return _buffer_pos >= _buffer.length and end_reached
+       redef fun eof
+       do
+               if _buffer_pos < _buffer.length then return false
+               if end_reached then return true
+               fill_buffer
+               return _buffer_pos >= _buffer.length and end_reached
+       end
 
        # The buffer
-       var _buffer: nullable Buffer = null
+       private var buffer: nullable FlatBuffer = null
 
        # The current position in the buffer
-       var _buffer_pos: Int = 0
+       private var buffer_pos: Int = 0
 
        # Fill the buffer
        protected fun fill_buffer is abstract
 
-       # Is the last fill_buffer reach the end 
+       # Is the last fill_buffer reach the end
        protected fun end_reached: Bool is abstract
 
        # Allocate a `_buffer` for a given `capacity`.
        protected fun prepare_buffer(capacity: Int)
        do
-               _buffer = new Buffer.with_capacity(capacity)
+               _buffer = new FlatBuffer.with_capacity(capacity)
                _buffer_pos = 0 # need to read
        end
 end
 
-interface IOStream
-       super IStream
-       super OStream
+# An Input/Output Stream
+abstract class Duplex
+       super Reader
+       super Writer
 end
 
-##############################################################"
-
-abstract class FDStream
-       super IOS
-       # File description
-       var fd: Int
-
-       redef fun close do native_close(fd)
-
-       private fun native_close(i: Int): Int is extern "stream_FDStream_FDStream_native_close_1"
-       private fun native_read_char(i: Int): Int is extern "stream_FDStream_FDStream_native_read_char_1"
-       private fun native_read(i: Int, buf: NativeString, len: Int): Int is extern "stream_FDStream_FDStream_native_read_3"
-       private fun native_write(i: Int, buf: NativeString, len: Int): Int is extern "stream_FDStream_FDStream_native_write_3"
-       private fun native_write_char(i: Int, c: Char): Int is extern "stream_FDStream_FDStream_native_write_char_2"
-
-       init(fd: Int) do self.fd = fd
-end
+# Stream to a String.
+#
+# Mainly used for compatibility with Writer type and tests.
+class StringWriter
+       super Writer
 
-class FDIStream
-       super FDStream
-       super IStream
-       redef var eof: Bool = false
-       
-       redef fun read_char
+       private var content = new Array[String]
+       redef fun to_s do return content.to_s
+       redef fun is_writable do return not closed
+       redef fun write(str)
        do
-               var nb = native_read_char(fd)
-               if nb == -1 then eof = true
-               return nb
+               assert not closed
+               content.add(str.to_s)
        end
 
-       init(fd: Int) do end 
-end
-
-class FDOStream
-       super FDStream
-       super OStream
-       redef var is_writable: Bool
-
-       redef fun write(s)
-       do
-               var nb = native_write(fd, s.to_cstring, s.length)
-               if nb < s.length then is_writable = false
-       end
+       # Is the stream closed?
+       protected var closed = false
 
-       init(fd: Int)
-       do
-               is_writable = true
-       end
+       redef fun close do closed = true
 end
 
-class FDIOStream
-       super FDIStream
-       super FDOStream
-       super IOStream
-       init(fd: Int)
-       do
-               self.fd = fd
-               is_writable = true
-       end
-end
+# Stream from a String.
+#
+# Mainly used for compatibility with Reader type and tests.
+class StringReader
+       super Reader
 
-redef interface Object
-       # returns first available stream to read or write to
-       # return null on interruption (possibly a signal)
-       protected fun poll( streams : Sequence[FDStream] ) : nullable FDStream
-       do
-               var in_fds = new Array[Int]
-               var out_fds = new Array[Int]
-               var fd_to_stream = new HashMap[Int,FDStream]
-               for s in streams do
-                       var fd = s.fd
-                       if s isa FDIStream then in_fds.add( fd )
-                       if s isa FDOStream then out_fds.add( fd )
-
-                       fd_to_stream[fd] = s
-               end
+       # The string to read from.
+       var source: String
+
+       # The current position in the string.
+       private var cursor: Int = 0
 
-               var polled_fd = intern_poll( in_fds, out_fds )
+       redef fun read_char do
+               if cursor < source.length then
+                       var c = source[cursor].ascii
 
-               if polled_fd == null then
-                       return null
+                       cursor += 1
+                       return c
                else
-                       return fd_to_stream[polled_fd]
+                       return -1
                end
        end
 
-       private fun intern_poll( in_fds : Array[Int], out_fds : Array[Int] ) : nullable Int is extern import Array::length, Array::[], nullable Object as ( Int ), Int as nullable
-end
+       redef fun close do
+               source = ""
+       end
 
-# Stream to a String. Mainly used for compatibility with OStream type and tests.
-class StringOStream
-       super OStream
+       redef fun read_all do
+               var c = cursor
+               cursor = source.length
+               if c == 0 then return source
+               return source.substring_from(c)
+       end
 
-       private var content = new Array[String]
-       redef fun to_s do return content.to_s
-       redef fun is_writable do return true
-       redef fun write(str) do content.add(str)
+       redef fun eof do return cursor >= source.length
 end