pboling/csv_pirate

View on GitHub
lib/csv_pirate/the_capn.rb

Summary

Maintainability
F
4 days
Test Coverage
module CsvPirate
  class TheCapn

    BOOKIE = [:counter, :timestamp, :none]
    MOP_HEADS = [:clean, :dirty]
    BRIGANTINE_OPTIONS = [:first, :last]
    CSV_CLASS = (defined?(CSV) ? CSV : FasterCSV)

    attr_accessor :waggoner         # First part of filename
    attr_accessor :chart            # directory, default is (['log','csv'])
    attr_accessor :aft              # extension, default is ('.csv')
    attr_accessor :gibbet           # part of the filename after waggoner and date, before swabbie and aft
    attr_accessor :chronometer      # Date object or false

    # Must provide swag or grub (not both)
    attr_accessor :swag             # Array of objects
    attr_accessor :grub             # Class
    # spyglasses is only used with grub, not swag
    attr_accessor :spyglasses       # named_scopes

    # These are the booty of the CSV
    # Should be methods/columns on the swag
    # also used to create the figurehead (CSV header)
    attr_accessor :booty            # methods / columns

    attr_accessor :bury_treasure    # should we store the csv data in an array for later inspection, or just write the CSV?
                                    # default is false
    # The array that gets built as we write the CSV... could be useful?
    attr_accessor :buried_treasure  # info array

    attr_accessor :rhumb_lines      # the file object to write the CSV lines to

    attr_accessor :astrolabe        # when true then read only CsvPirate instance for loading of CSVs

    attr_accessor :shrouds          # CSV column separator

    attr_accessor :swab             # default is :counter
    attr_accessor :mop              # default is :clean (only has an effect if :swab is :none) since overwriting is irrelevant for a new file
    attr_accessor :swabbie          # value of the counter / timestamp
    attr_accessor :maroon           # text of csv
    attr_accessor :nocturnal        # basename of the filepath (i.e. filename)
    attr_accessor :blackjack        # Hash: Specify how you want your CSV header
                                    #   {:join => '-'}    joins the method names called to get hte data for that column with '_' underscores.
                                    #   {:humanize => '-'} first joins as above, then humanizes the string
                                    #   {:array => ['col1',col2','col3'] Uses the column names provided in the array.  If the array provided is too short defaults to :humanize =>'_'

    attr_accessor :brigantine       # the complete file path
    attr_accessor :pinnacle         # an array of strings for CSV header based on blackjack
    class << self
       attr_accessor :parlay        # verbosity on a scale of 0 - 3 (0=:none, 1=:error, 2=:info, 3=:debug, 0 being no screen output, 1 is default
     end

    # CsvPirate only works for commissions of swag OR grub!
    # :swag           the ARrr collection of swag to work on (optional)
    # :grub           the ARrr class that the spyglasses will be used on (optional)
    # :spyglasses     named scopes in your model that will refine the rows in the CSV according to conditions of the spyglasses,
    #                   and order them according to the order of the spyglasses (optional)
    # :booty          booty (columns/methods) on your model that you want printed in the CSV, also used to create the figurehead (CSV header)
    # :chart          array of directory names (relative to rails root if using rails) which will be the filepath where you want to hide your loot
    # :wagonner       name of document where you will give detailed descriptions of the loot
    # :aft            filename extention ('.csv')
    # :shrouds        CSV column separator, default is ','. For tsv, tab-delimited, "\t"
    # :chronometer    keeps track of when you hunt for treasure, can be false if you don't want to keep track.
    # :gibbet         filename spacer after the date, and before the iterative counter/timestamp.  MuST contain a '.'
    # :swab           can be :counter, :timestamp, or :none
    #   :counter - default, each successive run will create a new file using a counter
    #   :timestamp - each successive run will create a new file using a HHMMSS time stamp
    #   :none - no iterative file naming convention, just use waggoner, aft and gibbet
    # :mop            can be :clean or :dirty (:overwrite or :append) (only has an effect if :swab is :none) since overwriting is irrelevant for a new file
    #   :clean - do not use :counter or :timestamp, and instead overwrite the file
    #   :dirty - do not use :counter, or :timestamp, or :overwrite.  Just keep adding on.
    # :bury_treasure  should we store the csv data as it is collected in an array in Ruby form for later use (true), or just write the CSV (false)?
    # :blackjack      Specify how you want your CSV header
    #   {:join => '-'}    joins the method names called to get hte data for that column with '_' underscores.
    #   {:humanize =>'-'} first joins as above, then humanizes the string
    #   {:array => ['col1',col2','col3'] Uses the column names provided.  If the array provided is too short defaults to :humanize =>'_'
    # See README for examples

    def initialize(*args)
      raise ArgumentError, "must provide required options" if args.nil?

      @swag = args.first[:swag]
      @grub = args.first[:grub]

      # if they provide both
      raise ArgumentError, "must provide either :swag or :grub, not both" if !self.swag.nil? && !self.grub.nil?
      # if they provide neither
      raise ArgumentError, "must provide either :swag or :grub" if self.swag.nil? && self.grub.nil?

      @swab = args.first[:swab] || :counter
      raise ArgumentError, ":swab is #{self.swab.inspect}, but must be one of #{TheCapn::BOOKIE.inspect}" unless TheCapn::BOOKIE.include?(self.swab)

      @mop = args.first[:mop] || :clean
      raise ArgumentError, ":mop is #{self.mop.inspect}, but must be one of #{TheCapn::MOP_HEADS.inspect}" unless TheCapn::MOP_HEADS.include?(self.mop)

      @gibbet = args.first[:gibbet] || '.export'
      raise ArgumentError, ":gibbet is #{self.gibbet.inspect}, and does not contain a '.' character, which is required when using iterative filenames (set :swab => :none to turn off iterative filenames)" if self.swab != :none && (self.gibbet.nil? || !self.gibbet.include?('.'))

      @waggoner = args.first[:waggoner] || "#{self.grub || self.swag}"
      raise ArgumentError, ":waggoner is #{self.waggoner.inspect}, and must be a string at least one character long" if self.waggoner.nil? || self.waggoner.length < 1

      # Not checking if empty here because PirateShip will send [] if the database is unavailable, to allow tasks like rake db:create to run
      @booty = args.first[:booty] || [] # would like to use Class#instance_variables for generic classes
      raise ArgumentError, ":booty is #{self.booty.inspect}, and must be an array of methods to call on a class for CSV data" if self.booty.nil? || !self.booty.is_a?(Array)

      @chart = args.first[:chart] || ['log','csv']
      raise ArgumentError, ":chart is #{self.chart.inspect}, and must be an array of directory names, which will become the filepath for the csv file" if self.chart.nil? || !self.chart.is_a?(Array) || self.chart.empty?

      @aft = args.first[:aft] || '.csv'
      @chronometer = args.first[:chronometer] == false ? false : (args.first[:chronometer] || Date.today)

      @spyglasses = (args.first[:spyglasses] || (self.respond_to?(:all) ? [:all] : nil)) if self.grub

      @shrouds = args.first[:shrouds] || ','  # for tsv, tab-delimited, "\t"
      raise ArgumentError, ":shrouds is #{self.shrouds.inspect}, and must be a string (e.g. ',' or '\t'), which will be used as the delimeter for the csv file" if self.shrouds.nil? || !self.shrouds.is_a?(String)

      @astrolabe = args.first[:astrolabe] || false

      @bury_treasure = args.first[:bury_treasure] || false
      @buried_treasure = []

      #does not rely on rails humanize!
      @blackjack = args.first[:blackjack] || {:humanize => '_'}

      #Make sure the header array is the same length as the booty columns
      if self.blackjack.keys.first == :array && (self.blackjack.values.first.length != self.booty.length)
        @blackjack = {:join => '_'}
        puts "Warning: :blackjack reset to {:join => '_'} because the length of the :booty is different than the length of the array provided to :blackjack" if TheCapn.parlance(2)
      end

      @pinnacle = self.block_and_tackle

      # Initialize doesn't write anything to a CSV,
      #   but does create the traverse_board (file) and opens it for reading / writing
      self.northwest_passage unless self.astrolabe

      # This will contain the text of the csv from this particular execution
      @maroon = ""

      # Once the traverse_board (dir) exists, then check if the rhumb_lines (file) already exists, and set our rhumb_lines counter
      @swabbie = self.insult_swabbie

      @brigantine = self.poop_deck(args.first[:brigantine])

      @nocturnal = File.basename(self.brigantine)

      # Then open the rhumb_lines
      self.rhumb_lines = File.open(File.expand_path(self.brigantine),self.astrolabe ? "r" : "a")
    end

    def self.create(*args)
      csv_pirate = TheCapn.new({
        :chart => args.first[:chart],
        :aft => args.first[:aft],
        :gibbet => args.first[:gibbet],
        :chronometer => args.first[:chronometer],
        :waggoner => args.first[:waggoner],
        :swag => args.first[:swag],
        :swab => args.first[:swab],
        :shrouds => args.first[:shrouds],
        :mop => args.first[:mop],
        :grub => args.first[:grub],
        :spyglasses => args.first[:spyglasses],
        :booty => args.first[:booty],
        :astrolabe => args.first[:astrolabe],
        :blackjack => args.first[:blackjack],
        :bury_treasure => args.first[:bury_treasure]
      })
      csv_pirate.hoist_mainstay()
      csv_pirate
    end

    ########################################
    ########### INSTANCE METHODS ###########
    ########################################

    # This is the hardest working method.  Get your shovels!
    def dig_for_treasure(&block)
      return false unless block_given?

      if !grub.nil?
        self.swag = grub
        spyglasses.each {|x| self.swag = self.swag.send(x) }
      end

      treasure_chest = self.swag.map do |spoils|
        self.prize(spoils)
      end

      treasure_chest.each do |loot|
        yield loot
      end
    end

    def prize(spoils)
      gold_doubloons = []
      self.booty.each do |plunder|
        # Check for nestedness
        if plunder.is_a?(Hash)
          gold_doubloons << TheCapn.marlinespike(spoils, plunder)
        # BJM: if array, assume they are args to be sent along with the function
        elsif plunder.is_a?(Array)
          gold_doubloons << spoils.send(plunder[0].to_sym, *plunder[1..-1] )
        else
          gold_doubloons << spoils.send(plunder.to_sym)
        end
      end
      gold_doubloons
    end

    def scrivener(msg)
      self.rhumb_lines.puts msg
    end

    # Sail through your db looking for buried treasure!
    # Creates the CSV file and returns the text of the CSV
    # - restricted to loot that can be seen through spyglasses (if provided)!
    def hoist_mainstay

      self.swab_poop_deck

      self.dead_mans_chest

      self.rhumb_lines.close

      self.jolly_roger if TheCapn.parlay && TheCapn.parlance(1)

      # returns the text of this CSV export
      return self.maroon
    end

    def dead_mans_chest
      self.maroon = CSV_CLASS.generate(:col_sep => self.shrouds) do |csv|
        self.sounding(csv)
      end
      self.scrivener(self.maroon)
      self.maroon
    end

    def jolly_roger
      if self.bury_treasure
        if self.buried_treasure.is_a?(Array)
          puts "Found #{self.buried_treasure.length} deniers buried here: '#{self.brigantine}'" if TheCapn.parlay && TheCapn.parlance(1)
          puts "You must weigh_anchor to review your plunder!" if TheCapn.parlay && TheCapn.parlance(1)
        else
          puts "Failed to locate treasure" if TheCapn.parlay && TheCapn.parlance(1)
        end
      end
    end

    def sounding(csv)
      csv << self.block_and_tackle
      # create the data for the csv
      self.dig_for_treasure do |treasure|
        moidore = treasure.map {|x| "#{x}"}
        csv << moidore # |x| marks the spot!
        self.buried_treasure << moidore if self.bury_treasure
      end
    end

    #Takes a potentially nested hash element of a booty array and turns it into a string for a column header
    # hash = {:a => {:b => {:c => {:d => {"e" => "fghi"}}}}}
    # run_through(hash, '_')
    # => "a_b_c_d_e_fghi"
    # Ooooh so recursive!
    def run_through(hash, join_value)
      hash.map do |k,v|
        if v.is_a?(Hash)
          [k,run_through(v, join_value)].join(join_value)
        else #works for Symbols and Strings
          [k,v].join(join_value)
        end
      end.first
    end

    #complete file path
    def poop_deck(brig)
      if BRIGANTINE_OPTIONS.include?(brig) && !self.flies.empty?
        self.old_csv_dump(brig)
      elsif brig.is_a?(String)
        "#{self.analemma}#{brig}"
      else
        "#{self.analemma}#{self.swabbie}#{self.aft}"
      end
    end

    # Swabs the poop_deck (of the brigantine) if the mop is clean. (!)
    def swab_poop_deck
      self.rhumb_lines.truncate(0) if self.swab == :none && self.mop == :clean && File.size(self.brigantine) > 0
    end

    # Must be done on order to rummage through the loot found by the pirate ship
    def weigh_anchor
      TheCapn.rinse(self.brigantine)
    end

    # Sink your own ship! Or run a block of code on each row of the current CSV
    def scuttle(&block)
      return false unless block_given?
      TheCapn.broadside(self.brigantine) do |careen|
        yield careen
      end
    end

    #permanence can be any of:
    #   {:new => :new} - only calls the initializer with data hash for each row to instantiate objects (useful with any vanilla Ruby Class)
    #   {:new => :save} - calls the initializer with the data hash for each row and then calls save on each (useful with ActiveRecord)
    #   {:new => :create} - calls a create method with the data hash for each row (useful with ActiveRecord)
    #   {:find_or_new => [column names for find_by]} - see below (returns only the new objects
    #   {:find_or_save => [column names for find_by]} - see below (returns all found or saved objects)
    #   {:find_or_create => [column names for find_by]} - looks for existing objects using find_by_#{columns.join('_and_')}, (returns all found or created objects)
    #       and if not found creates a new object.
    #       The difference between the new, save and create versions are the same as the various :new hashes above.
    #   {:update_or_new => [column names for find_by]} - see below (returns only the new objects)
    #   {:update_or_save => [column names for find_by]} - see below (returns all updated or saved objects)
    #   {:update_or_create => [column names for find_by]} - looks for existing objects using find_by_#{columns.join('_and_')} , (returns all updated or created objects)
    #       and updates them with the data hash form the csv row, otherwise creates a new object.
    #TODO: This is a nasty method.  Just a quick hack to GTD.  Needs to be rethought and refactored. --pboling
    def to_memory(permanence = {:new => :new}, exclude_id = true, exclude_timestamps = true)
      return nil unless self.grub
      buccaneers = []
      self.scuttle do |row|
        data_hash = self.data_hash_from_row(row, exclude_id, exclude_timestamps)
        case permanence
          when {:new => :new} then
            buccaneers << self.grub.new(data_hash)
          when {:new => :save} then
            obj = self.grub.new(data_hash)
            buccaneers << obj.save(false)
          when {:new => :create} then
            buccaneers << self.grub.create(data_hash)
        else
          if permanence[:find_or_new]
            obj = self.send_aye(data_hash, permanence[:find_or_new])
            buccaneers << self.grub.new(data_hash) if obj.nil?
          elsif permanence[:find_or_save]
            obj = self.send_aye(data_hash, permanence[:find_or_save])
            if obj.nil?
              obj = self.grub.new(data_hash)
              obj.save(false) if obj.respond_to?(:save)
            end
            buccaneers << obj
          elsif permanence[:find_or_create]
            obj = self.send_aye(data_hash, permanence[:find_or_create])
            if obj.nil?
              self.grub.create(data_hash)
            end
            buccaneers << obj
          elsif permanence[:update_or_new]
            obj = self.send_aye(data_hash, permanence[:update_or_new])
            if obj.nil?
              obj = self.grub.new(data_hash)
            else
              self.save_object(obj, data_hash)
            end
            buccaneers << obj
          elsif permanence[:update_or_save]
            obj = self.send_aye(data_hash, permanence[:update_or_save])
            if obj.nil?
              obj = self.grub.new(data_hash)
              obj.save(false)
            else
              self.save_object(obj, data_hash)
            end
            buccaneers << obj
          elsif permanence[:update_or_create]
            obj = self.send_aye(data_hash, permanence[:update_or_create])
            if obj.nil?
              obj = self.grub.create(data_hash)
            else
              self.save_object(obj, data_hash)
            end
            buccaneers << obj
          end
        end
      end
      buccaneers
    end

    def save_object(obj, data_hash)
      data_hash.each do |k,v|
        obj.send("#{k}=".to_sym, v)
      end
      unless obj.save(false)
        puts "Save Failed: #{obj.inspect}" if TheCapn.parlance(1)
      end
    end

    def send_aye(data_hash, columns)
      obj = self.grub.send(self.find_aye(columns), self.find_aye_arr(data_hash, columns))
      if obj
        puts "#{self.grub}.#{find_aye(columns)}(#{self.find_aye_arr(data_hash, columns).inspect}): found id = #{obj.id}" if TheCapn.parlance(2)
      end
      obj
    end

    def find_aye(columns)
      "find_by_#{columns.join('_and_')}".to_sym
    end

    def find_aye_arr(data_hash, columns)
      columns.map do |col|
        data_hash[col.to_s]
      end
    end

    def data_hash_from_row(row, exclude_id = true, exclude_timestamps = true)
      plunder = {}
      method_check = self.grub.instance_methods - Object.methods
      method_check = method_check.select {|x| "#{x}" =~ /=$/}
      my_booty = self.booty.reject {|x| x.is_a?(Hash)}
      my_booty = exclude_id ? my_booty.reject {|x| a = x.to_sym; [:id, :ID,:dbid, :DBID, :db_id, :DB_ID].include?(a)} : self.booty
      my_booty = exclude_timestamps ? my_booty.reject {|x| a = x.to_sym; [:created_at, :updated_at, :created_on, :updated_on].include?(a)} : self.booty
      my_booty = my_booty.select {|x| method_check.include?("#{x}=".to_sym)} if method_check
      my_booty.each do |method|
        plunder = plunder.merge({method => row[self.pinnacle[self.booty.index(method)]]})
      end
      plunder
    end

    # Grab an old CSV dump (first or last)
    def old_csv_dump(brig)
      file = self.flies.send(brig)
      "#{self.traverse_board}#{file}"
    end

    def flies
      Dir.entries(self.traverse_board).select {|x| x.match(self.unfurl)}.sort
    end

    # Regex for matching dumped CSVs
    def unfurl
      wibbly = self.waggoner == '' ? '' : Regexp.escape(self.waggoner)
      timey = self.sand_glass == '' ? '' : '\.\d+'
      wimey = self.gibbet == '' ? '' : Regexp.escape(self.gibbet)
      Regexp.new("#{wibbly}#{timey}#{wimey}")
    end

    protected

    # create the header of the CSV (column/method names)
    # returns an array of strings for CSV header based on blackjack
    def block_and_tackle
      self.blackjack.map do |k,v|
        case k
          #Joining is only relevant when the booty contains a nested hash of method calls as at least one of the booty array elements
          #Use the booty (methods) as the column headers
          when :join then self.binnacle(v, false)
          #Use the humanized booty (methods) as the column headers
          when :humanize then self.binnacle(v, true)
          when :array then v
        end
      end.first
    end

    #returns an array of strings for CSV header based on booty
    def binnacle(join_value, humanize = true)
      self.booty.map do |compass|
        string = compass.is_a?(Hash) ?
          self.run_through(compass, join_value) :
          compass.is_a?(String) ?
            compass :
            compass.is_a?(Symbol) ?
              compass.to_s :
              compass.to_s
        humanize ? string.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize : string
      end
    end

    # The directory path to the csv
    def traverse_board
      #If we have rails environment then we use rails root, otherwise self.chart stands on its own as a relative path
      "#{self.north_pole}#{self.chart.join('/')}/"
    end

    def sand_glass
      "#{self.chronometer.respond_to?(:strftime) ? '.' + self.chronometer.strftime("%Y%m%d") : ''}"
    end

    def merchantman
      "#{self.waggoner}#{self.sand_glass}#{self.gibbet}"
    end

    def analemma
      "#{self.traverse_board}#{self.merchantman}"
    end

    def north_pole
      "#{defined?(Rails) ? "#{Rails.root}/" : defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/" : ''}"
    end

    def northwest_passage
      self.chart.length.times do |i|
        north_star = self.north_pole + self.chart[0..i].join('/')
        Dir.mkdir(north_star) if Dir.glob(north_star).empty?
      end
    end

    def lantern
      "#{self.analemma}.*"
    end

    def filibuster(flotsam)
      base = File.basename(flotsam, self.aft)
      index = base.rindex('.')
      tail = index.nil? ? nil : base[index+1,base.length]
      # Ensure numbers
      tail.nil? ? 0 : tail[/\d*/].to_i
    end

    # Get all the files in the dir
    def axe
      Dir.glob(self.lantern)
    end

    # File increment for next CSV to dump
    def boatswain
      return self.swabbie unless self.swabbie.nil?
      highval = 0
      self.axe.each do |flotsam|
        counter = self.filibuster(flotsam)
        highval = ((highval <=> counter) == 1) ? highval : counter
      end
      ".#{highval + 1}"
    end

    def coxswain
      return self.swabbie unless self.swabbie.nil?
      ".#{Time.now.strftime("%I%M%S")}"
    end

    # Sets the swabby file counter
    def insult_swabbie
      return case self.swab
        when :counter
          self.boatswain
        when :timestamp
          self.coxswain
        else
          ""
      end
    end

    public

    ########################################
    ############ CLASS METHODS #############
    ########################################

    # if this is your booty:
    # {:booty => [
    #   :id,
    #   {:region => {:country => :name }, :state => :name },
    #   :name
    # ]}
    # so nested_hash = {:region => {:country => :name }, :state => :name }
    def self.marlinespike(spoils, navigation)
      navigation.map do |east,west|
        # BJM:
        if east.is_a?(Array)
          spoils = spoils.send(east[0].to_sym, *east[1..-1] )
        else
          spoils = spoils.send(east.to_sym)
        end
        unless spoils.nil?
          if west.is_a?(Hash)
            # Recursive madness is here!
            spoils = TheCapn.marlinespike(spoils, west)
          elsif west.is_a?(Array)
            spoils << spoils.send(west[0].to_sym, *west[1..-1] )
          else
            spoils = spoils.send(west.to_sym)
          end
        end
        spoils
      end.compact.join(' - ')
    end

    # Used to read any loot found by any pirate
    def self.rinse(quarterdeck)
      File.open(File.expand_path(quarterdeck), "r") do |bucket_line|
        bucket_line.each_line do |bucket|
          puts bucket
        end
      end
    end

    # Sink other ships! Or run a block of code on each row of a CSV
    def self.broadside(galley, &block)
      return false unless block_given?
      CSV_CLASS.foreach(galley, {:headers => :first_row, :return_headers => false}) do |gun|
        yield gun
      end
    end

    # !!!EXPERIMENTAL!!!
    #
    # During a mutiny things are a little different!
    # Essentially you are using an existing CSV to drive queries to create a second CSV cased on the first
    # The capn hash is:
    #   :grub       => is the class on which to make booty [method] calls
    #   :swag       => column index in CSV (0th, 1st, 2nd, etc. column?)
    #                  (swag OR spyglasses can be specified, but code defers to swag if provided)
    #   :spyglasses => is the column to load ("find_by_#{booty}") the ARrr object for each row on the first CSV
    #                  (swag OR spyglasses can be specified, but code defers to swag if provided)
    #   :waggoner   => where the capn's loot was stashed (filename)
    #   :chart      => array of directory names where capn's waggoner is located
    #   :astrolabe  => true (file is opened at top of file in read only mode when true)
    # The first_mate hash is:
    #   :grub       => is the class on which to make booty [method] calls, or
    #                    is a method (as a string) we call to get from the object loaded by capn,
    #                    to the object on which we'll make the first_mate booty [method] calls, or nil, if same object
    #   :swag       => is the method to call on first CSV row's object to find second CSV row's object (if grub is a class)
    #   :spyglasses => is the column to load ("find_by_#{booty}") the ARrr object for each row on the second CSV (if grub is a class)
    #   :booty      => is the methods to call on the ARrr object for each row on the second CSV
    #   :waggoner   => where to stash the first mate's loot (filename)
    #   :chart      => array of directory names where first mate's waggoner is located
    #   :astrolabe  => false (false is the default for astrolabe, so we could leave it off the first_mate)
    #
    # Example:
    #   capn      = {:grub => User,:spyglasses => [:inactive],:booty => ['id','login','status'],:waggoner => 'orig',:chart => ['log','csv'],:astrolabe => false}
    #   make_orig = CsvPirate.new(capn)
    #   make_orig.hoist_mainstay
    #   make_orig.weigh_anchor
    #
    #   first_mate = {:grub => 'account',:booty => ["id","number","name","created_at"],:waggoner => 'fake',:chart => ['log','csv']}
    #  OR
    #   # for same class, we re-use the object loaded from first CSV and make the booty [method] calls on it
    #   first_mate = {:grub => User,:booty => ["id","login","visits_count"],:waggoner => 'fake',:chart => ['log','csv']}
    #  OR
    #   first_mate = {:grub => Account,:spyglasses => 'id',:swag=>'user_id',:booty => ["id","name","number"],:waggoner => 'fake',:chart => ['log','csv']}
    #  AND
    #   capn       = {:grub => User,:spyglasses => 'login',:swag => 1,:waggoner => 'orig',:chart => ['log','csv'],:astrolabe => true}
    #   after_mutiny = CsvPirate.mutiny(capn, first_mate)
    #
    def self.mutiny(capn, first_mate)
      carrack = TheCapn.new(capn)
      cutthroat = TheCapn.new(first_mate)

      cutthroat.figurehead

      carrack.scuttle do |cutlass|
        puts "CUTLASS: #{cutlass.inspect}" if TheCapn.parlance(2)
        puts "CARRACK.SWAG: #{carrack.swag.inspect}" if TheCapn.parlance(2)
        backstaff = cutlass[carrack.swag] || cutlass["#{carrack.spyglasses}"]
        puts "BACKSTAFF: #{backstaff}" if TheCapn.parlance(2)
        puts "CARRACK.SPYGLASSES: #{carrack.spyglasses.inspect}" if TheCapn.parlance(2)
        gully = carrack.grub.send("find_by_#{carrack.spyglasses}".to_sym, backstaff)
        puts "GULLY: #{gully.inspect}" if TheCapn.parlance(2)
        if gully
          flotsam = cutthroat.grub.is_a?(String) ?
            gully.send(cutthroat.grub.to_sym) :
            cutthroat.grub.is_a?(Symbol) ?
              gully.send(cutthroat.grub) :
              cutthroat.grub.class == carrack.grub.class ?
                gully :
                cutthroat.grub.class == Class ?
                  cutthroat.grub.send("find_by_#{cutthroat.swag}", gully.send(cutthroat.spyglasses)) :
                  nil
          puts "FLOTSAM: #{flotsam.inspect}" if TheCapn.parlance(2)
          if flotsam
            plunder = cutthroat.prize(flotsam)
            cutthroat.buried_treasure << plunder
            cutthroat.scrivener(plunder.map {|bulkhead| "#{bulkhead}"}.join(','))
          else
            puts "Unable to locate: #{cutthroat.grub} related to #{carrack.grub}.#{carrack.spyglasses} '#{gully.send(carrack.spyglasses)}'" if TheCapn.parlance(1)
          end
        else
          puts "Unable to locate: #{carrack.grub}.#{carrack.spyglasses} '#{gully.send(carrack.spyglasses)}'" if TheCapn.parlance(1)
        end
      end

      carrack.rhumb_lines.close
      cutthroat.rhumb_lines.close

      cutthroat.jolly_roger

      # returns the array that is created before exporting it to CSV
      return cutthroat
    end

    # verbosity on a scale of 0 - 3 (0=:none, 1=:error, 2=:info, 3=:debug, 0 being no screen output, 1 is default
    def self.parlance(level = 1)
      self.parlay.is_a?(Numeric) && self.parlay >= level
    end

  end
end