class Storable

Constants

NICE_TIME_FORMAT
SUPPORTED_FORMATS
USE_ORDERED_HASH
VERSION

Attributes

debug[RW]
field_names[RW]
field_opts[RW]
field_types[RW]
sensitive_fields[RW]
format[R]

This value will be used as a default unless provided on-the-fly. See SUPPORTED_FORMATS for available values.

Public Class Methods

append_file(path, content, flush=true) click to toggle source
# File lib/storable.rb, line 478
def self.append_file(path, content, flush=true)
  write_or_append_file('a', path, content, flush)
end
field(*args, &processor) click to toggle source

Accepts field definitions in the one of the follow formats:

field :product
field :product => Integer
field :product do |val|
  # modify val before it's stored. 
end

The order they're defined determines the order the will be output. The fields data is available by the standard accessors, class.product and class.product= etc… The value of the field will be cast to the type (if provided) when read from a file. The value is not touched when the type is not provided.

# File lib/storable.rb, line 69
def self.field(*args, &processor)
  # TODO: Examine casting from: http://codeforpeople.com/lib/ruby/fattr/fattr-1.0.3/
  field_definitions = {}
  if args.first.kind_of?(Hash)
    args.first.each_pair do |fname,klass|
      field_definitions[fname] = { :class => klass }
    end
  else
    fname, opts = *args
    if opts.nil?
      field_definitions[fname] = {}
    elsif Hash === opts
      field_definitions[fname] = opts
    else
      raise ArgumentError, "Second argument must be a hash" 
    end
  end
  
  self.field_names ||= []
  self.field_types ||= {}
  self.field_opts ||= {}
  field_definitions.each_pair do |fname,opts|
    self.field_names << fname
    self.field_opts[fname] = opts
    self.field_types[fname] = opts[:class] unless opts[:class].nil?
    
    # This processor automatically converts a Proc object
    # to a String of its source. 
    processor = proc_processor if opts[:class] == Proc && processor.nil?
    
    unless processor.nil?
      define_method("_storable_processor_#{fname}", &processor)
    end
    
    if method_defined?(fname) # don't redefine the getter method
      STDERR.puts "method exists: #{self}##{fname}" if Storable.debug
    else
      define_method(fname) do 
        ret = instance_variable_get("@#{fname}")
        if ret.nil? 
          if opts[:default]
            ret = opts[:default]
          elsif opts[:meth]
            ret = self.send(opts[:meth])
          end
        end
        ret
      end
    end
    
    if method_defined?("#{fname}=") # don't redefine the setter methods
      STDERR.puts "method exists: #{self}##{fname}=" if Storable.debug
    else
      define_method("#{fname}=") do |val| 
        instance_variable_set("@#{fname}",val)
      end
    end
  end
end
from_array(*from) click to toggle source
# File lib/storable.rb, line 239
def self.from_array *from
  from = from.flatten.compact
  return nil if !from || from.empty?
  me = new
  me.from_array *from
  me.postprocess
  me
end
from_csv(from=[], sensitive=false) click to toggle source

Create a new instance of the object from comma-delimited data. from a JSON string split into an array by line.

# File lib/storable.rb, line 440
def self.from_csv(from=[], sensitive=false)
  self.from_delimited(from, ',', sensitive)
end
from_delimited(from=[],delim=',',sensitive=false) click to toggle source

Create a new instance of the object from a delimited string. from a JSON string split into an array by line. delim is the field delimiter.

# File lib/storable.rb, line 447
def self.from_delimited(from=[],delim=',',sensitive=false)
  return if from.empty?
  from = from.split($/) if String === from
  hash = {}
  
  fnames = sensitive ? (field_names-sensitive_fields) : field_names
  values = from[0].chomp.split(delim)
  
  fnames.each_with_index do |key,index|
    next unless values[index]
    hash[key.to_sym] = values[index]
  end
  hash = from_hash(hash) if hash.kind_of?(Hash) 
  hash
end
from_file(file_path, format='yaml') click to toggle source

Create a new instance of the object using data from file.

# File lib/storable.rb, line 197
def self.from_file(file_path, format='yaml')
  raise "Cannot read file (#{file_path})" unless File.exists?(file_path)
  raise "#{self} doesn't support from_#{format}" unless self.respond_to?("from_#{format}")
  format = format || File.extname(file_path).tr('.', '')
  me = send("from_#{format}", read_file_to_array(file_path))
  me.format = format
  me
end
from_hash(from={}) click to toggle source

Create a new instance of the object from a hash.

# File lib/storable.rb, line 215
def self.from_hash(from={})
  return nil if !from || from.empty?
  if self == Storable
    Storable::Anonymous.new from
  else
    new.from_hash(from)
  end
end
from_json(*from) click to toggle source

Create a new instance of the object from a JSON string. from a YAML String or Array (split into by line).

# File lib/storable.rb, line 390
def self.from_json(*from)
  from_str = [from].flatten.compact.join('')
  #from_str.force_encoding("ISO-8859-1")
  #p [:from, from_str.encoding.name] if from_str.respond_to?(:encoding)
  if YAJL_LOADED
    tmp = Yajl::Parser.parse(from_str, :check_utf8 => false)
  elsif JSON_LOADED
    tmp = JSON::load(from_str)
  else
    raise "JSON parser not loaded"
  end
  hash_sym = tmp.keys.inject({}) do |hash, key|
     hash[key.to_sym] = tmp[key]
     hash
  end
  hash_sym = from_hash(hash_sym) if hash_sym.kind_of?(Hash)  
  hash_sym
end
from_tsv(from=[], sensitive=false) click to toggle source

Create a new instance from tab-delimited data.

from a JSON string split into an array by line.

# File lib/storable.rb, line 435
def self.from_tsv(from=[], sensitive=false)
  self.from_delimited(from, "\t", sensitive)
end
from_yaml(*from) click to toggle source

Create a new instance of the object from YAML. from a YAML String or Array (split into by line).

# File lib/storable.rb, line 381
def self.from_yaml(*from)
  from_str = [from].flatten.compact.join('')
  hash = YAML::load(from_str)
  hash = from_hash(hash) if Hash === hash
  hash
end
has_field?(n) click to toggle source
# File lib/storable.rb, line 140
def self.has_field?(n)
  field_names.member? n.to_sym
end
inherited(obj) click to toggle source

Passes along fields to inherited classes

# File lib/storable.rb, line 49
def self.inherited(obj)                           
  unless Storable == self                         
    obj.sensitive_fields = self.sensitive_fields.clone if !self.sensitive_fields.nil?
    obj.field_names = self.field_names.clone if !self.field_names.nil?
    obj.field_types = self.field_types.clone if !self.field_types.nil?
  end                                             
end
new(*args) click to toggle source
# File lib/storable.rb, line 228
def initialize *args
  init *args
end
read_file_to_array(path) click to toggle source
# File lib/storable.rb, line 463
def self.read_file_to_array(path)
  contents = []
  return contents unless File.exists?(path)
  
  open(path, 'r') do |l|
    contents = l.readlines
  end

  contents
end
sensitive_field?(name) click to toggle source
# File lib/storable.rb, line 135
def self.sensitive_field?(name)
  @sensitive_fields ||= []
  @sensitive_fields.member?(name)
end
write_file(path, content, flush=true) click to toggle source
# File lib/storable.rb, line 474
def self.write_file(path, content, flush=true)
  write_or_append_file('w', path, content, flush)
end
write_or_append_file(write_or_append, path, content = '', flush = true) click to toggle source
# File lib/storable.rb, line 482
def self.write_or_append_file(write_or_append, path, content = '', flush = true)
  #STDERR.puts "Writing to #{ path }..." 
  create_dir(File.dirname(path))
  
  open(path, write_or_append) do |f| 
    f.puts content
    f.flush if flush;
  end
  File.chmod(0600, path)
end

Public Instance Methods

call(fname) click to toggle source
# File lib/storable.rb, line 248
def call(fname)
  unless field_types[fname.to_sym] == Proc &&
         Proc === self.send(fname)
         raise "Field #{fname} is not a Proc"
  end
  self.instance_eval &self.send(fname)
end
dump(format=nil, with_titles=false) click to toggle source

Dump the object data to the given format.

# File lib/storable.rb, line 184
def dump(format=nil, with_titles=false)
  format &&= format.to_sym
  format ||= :s # as in, to_s
  raise "Format not defined (#{format})" unless SUPPORTED_FORMATS.member?(format)
  send("to_#{format}") 
end
field_names() click to toggle source

Returns an array of field names defined by self.field

# File lib/storable.rb, line 171
def field_names
  self.class.field_names #|| self.class.ancestors.first.field_names
end
field_types() click to toggle source

Returns an array of field types defined by self.field. Fields that did not receive a type are set to nil.

# File lib/storable.rb, line 176
def field_types
  self.class.field_types #|| self.class.ancestors.first.field_types
end
format=(v) click to toggle source

See SUPPORTED_FORMATS for available values

# File lib/storable.rb, line 153
def format=(v)
  v &&= v.to_sym
  raise "Unsupported format: #{v}" unless SUPPORTED_FORMATS.member?(v)
  @format = v
end
from_array(*from) click to toggle source
# File lib/storable.rb, line 232
def from_array *from
  (self.field_names || []).each_with_index do |n,index|
    break if index >= from.size
    send("#{n}=", from[index])
  end
end
from_hash(from={}) click to toggle source
# File lib/storable.rb, line 256
def from_hash(from={})
  fnames = field_names
  
  return from if fnames.nil? || fnames.empty?
  fnames.each_with_index do |fname,index|
    ftype = field_types[fname]
    value_orig = from[fname.to_s] || from[fname.to_s.to_sym]
    next if value_orig.nil?
    
    if ( ftype == String or ftype == Symbol ) && value_orig.to_s.empty?
      value = ''
    elsif ftype == Array
      value = Array === value_orig ? value_orig : [value_orig]
    elsif ftype == Hash
      value = value_orig
    elsif !ftype.nil?
      value_orig = value_orig.first if Array === value_orig && value_orig.size == 1
      
      if    [Time, DateTime].member?(ftype)
        value = ftype.parse(value_orig)
      elsif [TrueClass, FalseClass, Boolean].member?(ftype)
        value = (value_orig.to_s.upcase == "TRUE")
      elsif ftype == Float
        value = value_orig.to_f
      elsif ftype == Integer
        value = value_orig.to_i
      elsif ftype == Symbol
        value = value_orig.to_s.to_sym
      elsif ftype == Range
        if Range === value_orig
          value = value_orig
        elsif Numeric === value_orig
          value = value_orig..value_orig
        else
          value_orig = value_orig.to_s
          if    value_orig.match(/\.\.\./)
            el = value_orig.split('...')
            value = el.first.to_f...el.last.to_f
          elsif value_orig.match(/\.\./)
            el = value_orig.split('..')
            value = el.first.to_f..el.last.to_f
          else
            value = value_orig..value_orig
          end
        end
      elsif ftype == Proc && String === value_orig
        value = Proc.from_string value_orig           
      end
    end
    
    value = value_orig if value.nil?
    
    if self.respond_to?("#{fname}=")
      self.send("#{fname}=", value) 
    else
      self.instance_variable_set("@#{fname}", value) 
    end
    
  end

  self.postprocess
  self
end
has_field?(n) click to toggle source
# File lib/storable.rb, line 143
def has_field?(n)
  self.class.field_names.member? n.to_sym
end
has_processor?(fname) click to toggle source
# File lib/storable.rb, line 375
def has_processor?(fname)
  self.respond_to? :"_storable_processor_#{fname}"
end
init(*args) click to toggle source
# File lib/storable.rb, line 224
def init *args
  from_array *args
end
postprocess() click to toggle source
# File lib/storable.rb, line 159
def postprocess
end
process(fname, val) click to toggle source
# File lib/storable.rb, line 371
def process(fname, val)
  self.send :"_storable_processor_#{fname}", val
end
sensitive!() click to toggle source
# File lib/storable.rb, line 166
def sensitive!
  @storable_sensitive = true
end
sensitive?() click to toggle source
# File lib/storable.rb, line 162
def sensitive?
  @storable_sensitive == true
end
sensitive_fields() click to toggle source
# File lib/storable.rb, line 179
def sensitive_fields
  self.class.sensitive_fields #|| self.class.ancestors.first.sensitive_fields
end
to_array() click to toggle source
# File lib/storable.rb, line 339
def to_array
  preprocess if respond_to? :preprocess
  fields = sensitive? ? (field_names-sensitive_fields) : field_names
  fields.collect do |fname|
    next if sensitive? && self.class.sensitive_field?(fname)
    v = self.send(fname)
    v = process(fname, v) if has_processor?(fname)
    if Array === v
      v = v.collect { |v2| v2.kind_of?(Storable) ? v2.to_a : v2 } 
    end
    v
  end
end
to_csv(with_titles=false) click to toggle source

Return the object data as a comma delimited string. with_titles specifiy whether to include field names (default: false)

# File lib/storable.rb, line 430
def to_csv(with_titles=false)
  to_delimited(with_titles, ',')
end
to_delimited(with_titles=false, delim=',') click to toggle source

Return the object data as a delimited string. with_titles specifiy whether to include field names (default: false) delim is the field delimiter.

# File lib/storable.rb, line 412
def to_delimited(with_titles=false, delim=',')
  preprocess if respond_to? :preprocess
  values = []
  fields = sensitive? ? (field_names-sensitive_fields) : field_names
  fields.each do |fname|
    values << self.send(fname.to_s)   # TODO: escape values
  end
  output = values.join(delim)
  output = field_names.join(delim) << $/ << output if with_titles
  output
end
to_file(file_path=nil, with_titles=true) click to toggle source

Write the object data to the given file.

# File lib/storable.rb, line 206
def to_file(file_path=nil, with_titles=true)
  raise "Cannot store to nil path" if file_path.nil?
  format = File.extname(file_path).tr('.', '')
  format &&= format.to_sym
  format ||= @format
  Storable.write_file(file_path, dump(format, with_titles))
end
to_hash() click to toggle source

Return the object data as a hash with_titles is ignored.

# File lib/storable.rb, line 322
def to_hash
  preprocess if respond_to? :preprocess
  tmp = USE_ORDERED_HASH ? Storable::OrderedHash.new : {}
  if field_names
    field_names.each do |fname|
      next if sensitive? && self.class.sensitive_field?(fname)
      v = self.send(fname)
      v = process(fname, v) if has_processor?(fname)
      if Array === v
        v = v.collect { |v2| v2.kind_of?(Storable) ? v2.to_hash : v2 } 
      end
      tmp[fname] = v.kind_of?(Storable) ? v.to_hash : v
    end
  end
  tmp
end
to_json(*from, &blk) click to toggle source
# File lib/storable.rb, line 353
def to_json(*from, &blk)
  preprocess if respond_to? :preprocess
  hash = to_hash
  if YAJL_LOADED # set by Storable
    ret = Yajl::Encoder.encode(hash)
    ret
  elsif JSON_LOADED
    JSON.generate(hash, *from, &blk)
  else 
    raise "no JSON parser loaded"
  end
end
to_string(*args) click to toggle source
# File lib/storable.rb, line 191
def to_string(*args)
  # TODO: sensitive?
  to_s(*args)
end
to_tsv(with_titles=false) click to toggle source

Return the object data as a tab delimited string. with_titles specifiy whether to include field names (default: false)

# File lib/storable.rb, line 425
def to_tsv(with_titles=false)
  to_delimited(with_titles, "\t")
end
to_yaml(*from, &blk) click to toggle source
# File lib/storable.rb, line 366
def to_yaml(*from, &blk)
  preprocess if respond_to? :preprocess
  to_hash.to_yaml(*from, &blk)
end