Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

A first pass at the Reader interface.

  • Loading branch information...
commit 027847b5692bfe81e44c492a78aeda60c147bebf 1 parent 4daf93e
James Edward Gray II authored
Showing with 330 additions and 0 deletions.
  1. +1 −0  .gitignore
  2. +20 −0 Rakefile
  3. +178 −0 lib/mungr/reader.rb
  4. +131 −0 test/test_reader.rb
1  .gitignore
View
@@ -0,0 +1 @@
+doc
20 Rakefile
View
@@ -0,0 +1,20 @@
+# encoding: UTF-8
+
+require "rake/testtask"
+require "rake/rdoctask"
+
+task :default => :test
+
+Rake::TestTask.new do |test|
+ test.libs << "test"
+ test.test_files = FileList["test/test_*.rb"]
+ test.warning = true
+ test.verbose = true
+end
+
+Rake::RDocTask.new do |rdoc|
+ rdoc.title = "Mungr Documentation"
+ rdoc.main = "README"
+ rdoc.rdoc_dir = "doc"
+ rdoc.rdoc_files.include(*%w[README lib/])
+end
178 lib/mungr/reader.rb
View
@@ -0,0 +1,178 @@
+# encoding: UTF-8
+
+module Mungr
+ #
+ # Objects of this class are the basic unit of input for Mungr scripts.
+ # Reading using one of these objects is a four stage process:
+ #
+ # 1. An Reader is built and configured
+ # 2. The Reader is prepared just before the first read
+ # 3. Chunks of data are read from the Reader one by one until a +nil+ is
+ # returned to signal that the input is exhausted
+ # 4. The Reader is finished just after a +nil+ is read
+ #
+ class Reader
+ #
+ # Use the +init+ block to build a Reader by assigning code to each of the
+ # three code stages, something like:
+ #
+ # file_reader = Reader.new do |r|
+ # r.prepare { File.open("my_file.txt") }
+ # r.read { |f| f.gets }
+ # r.finish { |f| f.close }
+ # end
+ #
+ # All stages are optional, though a Reader isn't too handy without some
+ # read() code.
+ #
+ # Once built, you generally just call read() as long as it returns non-+nil+
+ # data and process the values it returns, like this:
+ #
+ # while line = file_reader.read
+ # # ... work with line here ...
+ # end
+ #
+ def initialize(&init)
+ @prepare_code = nil
+ @context = nil
+ @prepared = false
+ @read_code = nil
+ @read = false
+ @finish_code = nil
+ @finished = false
+
+ init[self] if init
+ end
+
+ # Returns +true+ if the prepare() code has been run, +false+ otherwise.
+ def prepared?
+ @prepared
+ end
+
+ # Returns +true+ if the read() code has been run, +false+ otherwise.
+ def read?
+ @read
+ end
+
+ # Returns +true+ if the finish() code has been run, +false+ otherwise.
+ def finished?
+ @finished
+ end
+
+ #
+ # :call-seq:
+ # prepare() { code_to_run_before_reading() }
+ # prepare()
+ #
+ # If passed a block, this method sets the code that will be used to prepare
+ # this Reader. Any value returned by this code will be forwarded to the
+ # read() and finish() code and thus essentially becomes the shared context
+ # of the reading process.
+ #
+ # When called without a block, this method actually runs the previously set
+ # code. This is generally done as needed when you call read() and it's not
+ # recommended to call this method yourself.
+ #
+ def prepare(&code)
+ load_or_run(:prepare, &code)
+ end
+
+ #
+ # :call-seq:
+ # read() { |context| code_to_read_one_chunk_of_data() }
+ # read()
+ #
+ # If passed a block, this method sets the code that will be used to read a
+ # single chunk of data. The block will be passed the context returned from
+ # prepare(). That code should return a +nil+ when input is exhausted.
+ #
+ # This method, called without block, is also the primary reading interface.
+ # You can just call it repeatedly until it returns +nil+ to indicate that
+ # input is exhausted.
+ #
+ def read(&code)
+ load_or_run(:read, &code)
+ end
+
+ #
+ # :call-seq:
+ # finish() { |context| code_to_run_after_reading() }
+ # finish()
+ #
+ # If passed a block, this method sets the code that will be used to finish
+ # this Reader. The block will be passed the context returned from
+ # prepare(). This code is called once after input is exhausted and it gives
+ # you a chance to do any needed cleanup.
+ #
+ # When called without a block, this method actually runs the previously set
+ # code. This is generally done as needed when you call read() and it's not
+ # recommended to call this method yourself.
+ #
+ def finish(&code)
+ load_or_run(:finish, &code)
+ end
+
+ #######
+ private
+ #######
+
+ #
+ # Provides the dual code setting and running behavior of all three stage
+ # methods. If +code+ is non-+nil+ it will be passed on to load_code(),
+ # otherwise +name+ code will be run.
+ #
+ def load_or_run(name, &code)
+ if code
+ load_code(name, code)
+ elsif instance_variable_get("@#{name}_code")
+ send("run_#{name}_code")
+ end
+ end
+
+ #
+ # Sets an instance variable based on +name+ to hold +code+ for later use.
+ # Also returns +self+ for method chaining.
+ #
+ def load_code(name, code)
+ instance_variable_set("@#{name}_code", code)
+ self
+ end
+
+ #
+ # Executes the prepare code, saving and returning the shared context that
+ # method returns. Also sets flips the prepared?() status to +true+.
+ #
+ def run_prepare_code
+ @context = @prepare_code[]
+ @prepared = true
+ @context
+ end
+
+ #
+ # This method is the primary interface for reading. It will:
+ #
+ # * Return +nil+ if read?() is now +true+
+ # * Run prepare() unless prepared?() is now +true+
+ # * Run the read code to generate one chunk of data and return that result
+ # * Run finish() just before returning the first +nil+
+ #
+ def run_read_code
+ return nil if read?
+ prepare unless prepared?
+ @read_code[@context].tap { |this_read|
+ if this_read.nil?
+ @read = true
+ finish
+ end
+ }
+ end
+
+ #
+ # Executes the finish code. Also sets flips the finished?() status to
+ # +true+.
+ #
+ def run_finish_code
+ @finish_code[@context].tap { @finished = true }
+ end
+ end
+end
131 test/test_reader.rb
View
@@ -0,0 +1,131 @@
+# encoding: UTF-8
+
+require "minitest/autorun"
+
+require "mungr/reader"
+
+class TestReader < MiniTest::Unit::TestCase
+ ##############
+ ### Status ###
+ ##############
+
+ def test_a_new_reader_is_not_prepared_read_or_finished
+ reader
+ assert(!@reader.prepared?, "A new Reader was already prepared?().")
+ assert(!@reader.read?, "A new Reader was already read?().")
+ assert(!@reader.finished?, "A new Reader was already finished?().")
+ end
+
+ def test_a_reader_is_prepared_before_the_first_read
+ order = Array.new
+ reader do |r|
+ r.prepare { order << :prepared }
+ r.read { order << :read }
+ end
+ assert(!@reader.prepared?, "The Reader was prepared?() before the read().")
+ @reader.read
+ assert( @reader.prepared?,
+ "The Reader was not prepared?() after the read()." )
+ assert_equal([:prepared, :read], order)
+ end
+
+ def test_exhausting_a_reader_sets_read_and_finished
+ order = Array.new
+ reader do |r|
+ r.read {
+ order << :read
+ nil # signal that we are exhausted
+ }
+ r.finish { order << :finished }
+ end
+ assert(!@reader.read?, "The Reader was read?() before being exhausted.")
+ assert( !@reader.finished?,
+ "The Reader was finished?() before being exhausted." )
+ @reader.read
+ assert(@reader.read?, "The Reader was read?() after being exhausted.")
+ assert( @reader.finished?,
+ "The Reader was finished?() after being exhausted." )
+ assert_equal([:read, :finished], order)
+ end
+
+ ###############
+ ### Context ###
+ ###############
+
+ def test_any_value_returned_from_prepare_is_forwarded_to_read_and_finish
+ object = Object.new
+ reader do |r|
+ r.prepare { object }
+ r.read { |context|
+ assert_same(object, context)
+ nil # signal that we are exhausted
+ }
+ r.finish { |context| assert_same(object, context) }
+ end.read
+ end
+
+ ###############
+ ### Reading ###
+ ###############
+
+ def test_calling_read_a_with_block_sets_the_code_and_further_calls_run_it
+ data = (1..3).to_a
+ expected = data.dup
+ reader do |r|
+ r.read { data.shift }
+ end
+ 4.times do |i|
+ assert_equal(expected[i], @reader.read)
+ end
+ end
+
+ def test_a_nil_signals_that_input_is_exhausted_and_no_more_reads_are_made
+ reader do |r|
+ r.prepare { [nil] + (1..3).to_a }
+ r.read { |data| data.shift }
+ end
+ 5.times do
+ assert_nil(@reader.read)
+ end
+ end
+
+ def test_prepare_is_called_once_before_the_first_read
+ calls = Array.new
+ reader do |r|
+ r.prepare {
+ calls << :prepare
+ (1..3).to_a
+ }
+ r.read { |data|
+ calls << :read
+ data.shift
+ }
+ end
+ @reader.read until @reader.read?
+ assert_equal([:prepare, :read, :read, :read, :read], calls)
+ end
+
+ def test_finish_is_called_once_when_the_input_is_exhausted
+ calls = Array.new
+ reader do |r|
+ r.prepare { (1..3).to_a }
+ r.read { |data|
+ calls << :read
+ data.shift
+ }
+ r.finish { calls << :finish }
+ end
+ 5.times do
+ @reader.read
+ end
+ assert_equal([:read, :read, :read, :read, :finish], calls)
+ end
+
+ #######
+ private
+ #######
+
+ def reader(&init)
+ @reader = Mungr::Reader.new(&init)
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.