module FSDB

# Formats are handled as instances rather than as subclasses so that they
# can be dynamically generated, serialized, etc. When defining a dump
# proc for a new format, be sure to perform the write with a single syswrite
# call to protect data consistency in case of exceptions.
class Format
  attr_reader :name, :options, :patterns, :dump, :load
  def initialize(*args)
    @options = []; @patterns = []
    while arg = args.shift
      case arg
      when Symbol
        @options << arg
        eval "@#{arg} = true"
      when Hash
        @name ||= arg[:name]
        @load ||= arg[:load]
        @dump ||= arg[:dump]
      else
        @patterns << arg
      end
    end
  end

  def ===(path)
    @patterns.any? {|pat| pat === path}
  end

  # Used only on Windows.
  def binary?
    @binary
  end

  def load(f)
    @load[f] if @load
  rescue StandardError => e
    raise e,
      "Format #{name} can't load object at path #{f.path}: #{e.inspect}"
  end

  def dump(object, f, *opts)
    if @dump
      f.rewind; f.truncate(0)
      @dump[object, f]
      f.flush
    end
  rescue StandardError => e
    raise e,
      "Format #{name} can't dump object at path #{f.path}: #{e.inspect}"
  end
end

module Formats

  # Files of the form '..*', as well as '.', are excluded from dir lists.
  HIDDEN_FILE_PAT = /^\.(?:$|\.)/

  DIR_PAT = /\/$/

  dir_load = proc do |f|
    path = f.path
    begin
      files = Dir.entries(path).reject { |e| HIDDEN_FILE_PAT =~ e }
    rescue Errno::ENOENT
      []
    else
      Dir.chdir path do
        files.map! do |entry|
          File.directory?(entry) ? (entry << ?/) : entry
        end
      end
    end
  end

  DIR_FORMAT = Format.new DIR_PAT, :name => "DIR_FORMAT", :load => dir_load

  TEXT_FORMAT =
    Format.new /\.txt$/, /\.text$/,
      :name => "TEXT_FORMAT",
      :load => proc {|f| f.read},
      :dump => proc {|string, f| f.syswrite(string)}

  marshal_load = proc do |f|
    begin
      Marshal.load(f)
    end
  end

  marshal_dump = proc do |object, f|
    begin
      f.syswrite(Marshal.dump(object))
    rescue StandardError => e
      raise e, "Unknown object format at path #{f.path}: #{e.inspect}"
    end
  end

  MARSHAL_FORMAT =
    Format.new //, :binary,
      :name => "MARSHAL_FORMAT",
      :load => marshal_load,
      :dump => marshal_dump
      # Actually, Marshal does binmode=true automatically.

end

end # module FSDB