Examples

Contents of linefeed:examples/, concatenated here for easy consumption.

01_logger.rb

# frozen_string_literal: true

require_relative 'demo'
require 'linefeed'

# Simplest possible example
module Demo
  class Logger
    include Linefeed

    def initialize(output)
      line_no = 0
      linefeed do |line|
        line_no += 1
        output << format('%<line_no>03d => %<line>s', line_no: line_no, line: line)
      end
    end
  end
end

02_canonicalize.rb

# frozen_string_literal: true

require_relative 'demo'
require 'linefeed'

# Per-line processing with headers & trailers
module Demo
  class Canonicalize
    include Linefeed

    def initialize(output)
      @output = output
      @output << "---------- START\r\n"
      @output << "Canonicalized: yes\r\n"
      @output << "\r\n"

      linefeed do |line|
        output << process_line(line)
      end
    end

    def process_line(line)
      canonicalize(line)
    end

    def canonicalize(line)
      "#{line.chomp.sub(/[ \t]+$/, '')}\r\n"
    end

    def close
      super
      @output << "---------- END\r\n"
      @output.close
    end
  end
end

03_escaped.rb

# frozen_string_literal: true

require_relative 'demo'
require 'linefeed'

# Handling the protocol via super
module Demo
  class Escaped
    include Linefeed

    def initialize(output)
      @output = output
    end

    def escape(line)
      line.sub(/^(-|From )/, '- \\1')
    end

    def <<(chunk)
      super do |line|
        @output << escape(line)
      end
    end

    def close
      super do |line|
        @output << "#{escape(line)}\n"
      end
    end
  end
end

04_line_digest.rb

# frozen_string_literal: true

require_relative 'demo'
require 'linefeed'
require 'digest'

# Only outputs at close
module Demo
  class LineDigest
    include Linefeed

    def initialize(output)
      @output = output
      @line_digest = Digest('SHA256').new

      linefeed do |line|
        @line_digest.update(line)
      end
    end

    def close
      super
      @output << @line_digest.hexdigest
    end
  end
end

05_chunk_digest.rb

# frozen_string_literal: true

require_relative 'demo'
require 'linefeed'
require 'digest'

# Not actually using Linefeed, but speaking the same protocol,
# consuming entire chunks.
#
# Should give the same digest as LineDigest.
module Demo
  class ChunkDigest
    def initialize(output)
      @output = output
      @digest = Digest('SHA256').new
    end

    def <<(chunk)
      @digest << chunk
    end

    def close
      @output << @digest.hexdigest
    end
  end
end

06_canonicalized_digest.rb

# frozen_string_literal: true

require_relative '02_canonicalize'
require_relative '04_line_digest'
require 'delegate'

# Easy chaining
module Demo
  class CanonicalizedDigest < DelegateClass(Canonicalize)
    def initialize(output)
      super(Canonicalize.new(LineDigest.new(output)))
    end
  end
end

07_null.rb

# frozen_string_literal: true

require_relative 'demo'
require 'linefeed'

# Intentionally fails to setup the feed and suffers for it.
# Don't do this.
module Demo
  class Null
    include Linefeed

    def initialize(output)
      @output = output
      @count = 0
    end

    def <<(*)
      super
    rescue ArgumentError
      @count += 1
    end

    def close
      super
    rescue ArgumentError
      @output << "rescued #{@count += 1} time(s)"
    end
  end
end

demo.rb

# frozen_string_literal: true

require_relative 'demo_helper'

if $0 == __FILE__
  example_files = Dir[File.join(__dir__, '[0-9][0-9]_*.rb')]
  example_files.each do |path|
    require_relative File.basename(path)
  end
end

def run
  recipients = Demo.setup_examples
  input = Demo.input_pipe(ARGF)
  maxlen = 8192
  chunk = ''.b

  while input.read(maxlen, chunk) && !chunk.empty?
    recipients.each do |r|
      r << chunk
    end
  end
  recipients.each(&:close)
end

Demo.launcher { run unless $! }

demo_helper.rb

# frozen_string_literal: true

module Demo
  # IO trap
  class Output
    def initialize(klass)=@prefix = klass.to_s
    def <<(obj)=puts "#{@prefix}: #{obj.inspect}"
    def close()=puts "#{@prefix} closed."
  end

  # decouple from tty if demo run interactively
  def self.input_pipe(source)
    return source unless $stdin.tty?

    reader, writer = IO.pipe
    Thread.new do
      IO.copy_stream(source, writer)
    ensure
      writer.close unless writer.closed?
    end
    reader
  end

  # one-shot method
  def self.launcher(&)=at_exit(&) && def self.launcher()=??

  # Example registry
  @example_classes = []
  class << self
    def const_added(const_name)
      super
      return unless const_get(const_name, false) in Class => klass

      register(klass)
    end

    def register(klass)
      @example_classes << klass
    end

    def setup_examples
      @example_classes.map { |k| k.new(Output.new(k)) }
    end
  end
end