sitef.cr 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. # Sitef is a simple text format for serializing data to
  2. # It's intent is to be human readable and easy to
  3. # use for multi line text.
  4. module Sitef
  5. # All Sitef data is stored in files with one or more records.
  6. # Records are separated by separated by at least 2 dashes on a line.
  7. # Records contain key/value fields. The key starts in the first column
  8. # with a : and is followed by a : the value starts after the second :
  9. # A multiline key / value needs a key that starts with . and ends with .
  10. # the end of the value is a pair of dots .. by itself
  11. # Keys may not be nested, however, you could use spaces or dots,
  12. # or array indexes to emulate nexted keys.
  13. # A # at the start optionally after whitespace is a comment
  14. #
  15. def self.parse_file(file)
  16. lineno = 0
  17. results = []
  18. errors = []
  19. record = {}
  20. key = nil
  21. value = nil
  22. until file.eof?
  23. lineno += 1
  24. line = file.gets(256)
  25. break if line.nil?
  26. next if line.empty?
  27. # new record
  28. if line[0,2] == '--'
  29. # Store last key used if any.
  30. if key
  31. record[key] = value.chomp
  32. key = nil
  33. end
  34. results << record
  35. record = {}
  36. elsif line[0] == '#'
  37. # Comments start with #
  38. elsif line[0] == ':'
  39. # a key/value pair
  40. key, value = line[1,line.size].split(':', 2)
  41. record[key] = value.chomp
  42. key = value = nil
  43. elsif line[0, 2] == '..'
  44. # end of multiline value
  45. record[key] = value.chomp
  46. key = value = nil
  47. elsif (line[0] == '.') && key.nil?
  48. # Multiline key/value starts here (but is ignored
  49. # until .. is encountered)
  50. key = line[1, line.size]
  51. key.chomp!
  52. value = ""
  53. # multiline value
  54. elsif key
  55. if line[0] == '\\'
  56. # remove any escapes
  57. line.slice!(0)
  58. end
  59. # continue the value
  60. value << line
  61. else
  62. # Not in a key, sntax error.
  63. errors << "#{lineno}: Don't know how to process line"
  64. end
  65. end
  66. # Store last key used if any.
  67. if key
  68. record[key] = value.chomp
  69. end
  70. # store last record
  71. results << record unless record.empty?
  72. return results, errors
  73. end
  74. def self.load_filename(filename)
  75. results, errors = nil, nil, nil;
  76. file = File.open(filename, 'rt') rescue nil
  77. return nil, ["Could not open #{filename}"] unless file
  78. begin
  79. results, errors = parse_file(file)
  80. ensure
  81. file.close
  82. end
  83. return results, errors
  84. end
  85. # Loads a Sitef fileas obejcts. Uses the ruby_klass atribute to load the object
  86. # If that is missing, uses defklass
  87. def self.load_objects(filename, defklass=nil)
  88. results, errors = load_filename(filename)
  89. p filename, results, errors
  90. unless errors.nil? || errors.empty?
  91. return nil, errors
  92. end
  93. objres = []
  94. results.each do | result |
  95. klassname = result['ruby_class'] || defklass
  96. return nil unless klassname
  97. klass = klassname.split('::').inject(Kernel) { |klass, name| klass.const_get(name) rescue nil }
  98. return nil unless klass
  99. if klass.respond_to? :from_sitef
  100. objres << klass.from_sitef(result)
  101. else
  102. objres << klass.new(result)
  103. end
  104. end
  105. return objres, errors
  106. end
  107. # Saves a single field to a file in Sitef format.
  108. def self.save_field(file, key, value)
  109. if value.is_a? String
  110. sval = value.dup
  111. else
  112. sval = value.to_s
  113. end
  114. if sval["\n"]
  115. file.puts(".#{key}\n")
  116. # Escape everything that could be misinterpreted with a \\
  117. sval.gsub!(/\n([\.\-\:\#\\]+)/, "\n\\\\\\1")
  118. sval.gsub!(/\A([\.\-\:\#\\]+)/, "\\\\\\1")
  119. file.printf("%s", sval)
  120. file.printf("\n..\n")
  121. else
  122. file.printf(":#{key}:#{sval}\n")
  123. end
  124. end
  125. def self.save_object(file, object, *fields)
  126. save_field(file, :ruby_class, object.class.to_s)
  127. fields.each do | field |
  128. value = object.send(field.to_sym)
  129. save_field(file, field, value)
  130. end
  131. end
  132. def self.save_record(file, record, *fields)
  133. record.each do | key, value |
  134. next if fields && !fields.empty? && !fields.member?(key)
  135. save_field(file, key, value)
  136. end
  137. end
  138. def self.save_file(file, records, *fields)
  139. records.each do | record |
  140. if record.is_a? Hash
  141. save_record(file, record, *fields)
  142. else
  143. save_object(file, record, *fields)
  144. end
  145. file.puts("--\n")
  146. end
  147. end
  148. def self.save_filename(filename, records, *fields)
  149. results , errors = nil, nil
  150. file = File.open(filename, 'wt')
  151. return false, ["Could not open #{filename}"] unless file
  152. begin
  153. save_file(file, records, *fields)
  154. ensure
  155. file.close
  156. end
  157. return true, []
  158. end
  159. end