module Softwear module Library module SpecDump Record = Struct.new(:object, :history) do def model; object.class; end end def spec_dump(root, ignored_models = [], whitelist_array = nil) types_recorded = {} records_by_type = {} record_queue = Queue.new whitelist = whitelist_array ? whitelist_array.reduce({}) { |h,n| h.merge(n => true) } : Hash.new(true) is_ignored = lambda do |name| next true if ignored_models.any? { |i| name =~ i } next true if !whitelist[name] false end # Begin dump routine. This will return true when we added a new entry. dump = lambda do |record| cache = (records_by_type[record.model.name] ||= {}) identifier = record.object.id next false if cache[identifier].present? attributes = {} record.model.column_names.each do |col| value = record.object[col] if value.respond_to?(:iso8601) # DateTimes don't serialize properly in attributes_before_type_cast # for some reason, so we explicitly call to_s(:db) to make sure # they can be loaded again correctly. raw_value = value.to_s(:db) else raw_value = record.object.attributes_before_type_cast[col] end attributes[col] = raw_value end cache[identifier] = attributes true end # end dump routine # Begin actual dumping of records Array(root).each do |record| record_queue << Record.new(record, ["#{record.class.name}##{record.id}"]) end while record_queue.present? record = record_queue.pop next if record.object.nil? next unless record.model.respond_to?(:column_names) # If dump returns false, that means we've already dumped this record next unless dump.(record) types_recorded[record.model.name] = true yield record, types_recorded if block_given? record.model.reflect_on_all_associations.each do |assoc| next if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) next if is_ignored["#{record.model.name}##{assoc.name}"] case assoc when ActiveRecord::Reflection::BelongsToReflection # A belongs_to association will never cause an infinite loop record_queue << Record.new( record.object.send(assoc.name), record.history + ["#{record.model.name}##{record.object.id}##{assoc.name}"] ) when ActiveRecord::Reflection::HasManyReflection # A has_many association can cause an infinite loop, so we only # process these if we've never seen the record type before. # # If there's a whitelist, no need to care about that next if whitelist_array.blank? && types_recorded[assoc.klass.name] record.object.send(assoc.name).each_with_index do |child, i| next if child.nil? record_queue << Record.new( child, record.history + ["#{record.model.name}##{record.object.id}##{assoc.name}[#{i}]"] ) end end end end # end actual dumping of records records_by_type end def expand_spec_dump(dump, use_outside_of_test = false) if !Rails.env.test? && !use_outside_of_test raise "Tried to call expand_spec_dump outside of test environment. "\ "If you really want to do this, pass `true` as the second parameter." end if ActiveRecord::Base.configurations[Rails.env]['adapter'].include?('sqlite') insert_cmd = lambda do |model| "INSERT OR REPLACE INTO #{model.table_name} (#{model.column_names.map { |c| "`#{c}`" }.join(', ')}) VALUES\n" end cmd_suffix = ->_{ "" } else insert_cmd = lambda do |model| "INSERT INTO #{model.table_name} (#{model.column_names.map { |c| "`#{c}`" }.join(', ')}) VALUES\n" end cmd_suffix = lambda do |model| "\nON DUPLICATE KEY UPDATE\n" + model.column_names .map { |col| "`#{col}` = VALUES(`#{col}`)" } .join(",\n") end end dump.each do |class_name, entries| model = class_name.constantize sql = insert_cmd[model] sanitize = model.connection.method(:quote) sql += entries.map do |entry| _id, attributes = entry '(' + model.column_names.map { |col| sanitize[attributes[col]] }.join(', ') + ')' end.join(",\n") sql += cmd_suffix[model] model.connection.execute sql end end extend self end end end