woese/guara-crm

View on GitHub
lib/active_migration/migration.rb

Summary

Maintainability
A
3 hrs
Test Coverage

require 'spreadsheet'
require 'active_support'

module ActiveMigration
  class ActiveMigartionError < StandardError; end
  class ActiveMigartionDataSourceError < ActiveMigartionError; end  
  class AciteMigrationInvalidRecordError < ActiveMigartionError; end  
  
  
  class Schema
    ##
    # @attr columns Hash with name and type
    # @attr format Describe data format
    # @attr url define path or object schema
    attr_accessor :columns, :format, :url
    
  end
  
  ##
  # Module Data Transformation
  # When passing schema file or struct, the data between datasources
  # needs a transformations 
  module Transformer
    ##
    # called on start of migration
    def begin(schema_from, schema_to)
      # nothing      
    end
    
    ##
    # called on start of transaction
    def begin_transaction(schema_from, schema_to)
      # nothing
    end
    
    ##
    # transform from data row to destinate data row 
    # @result true, false or :ignore to ignore this row
    def transform(row)
      raise "Implements transform method!"
    end
    
    ##
    # called on ending migration
    def end(schema_from, schema_to)
      # nothing
    end
    
    ##
    # called on ending transaction
    def end_transaction(schema_from, schema_to)
      # nothing
    end
    
    ##
    #
    def after_row_saved(row, object)
      #
    end
    
    def transform_ignore_fields(row)
      #delete ignore
      row.reject! { |key,value| key.to_s.start_with?("ignore") }
    end
    
  end  
  
  
  ##
  # == Class Migration
  # 
  # Migrate data between  data source and transforme to destination 
  #
  class Migration
    attr_accessor :schema_from, :schema_to, :transformer, :name
  
    # constructor
    def initialize(schema_url=nil)
      self.load_schema(schema_url) unless schema_url.nil?
    end
    
    ##
    # load yml schemas from and to
    def load_schema(url)
      schema = YAML::load(File.open(url))
      self.schema_from = schema[:from]
      self.schema_to = schema[:to]
    end
    
    
    ##
    # Running migration from configured files
    # 
    # ps> Default Behaviour Ignore First line - assumes head line
    
    def migrate!
    
      raise "schema_from needs" if @schema_from.nil?
      raise "schema_to needs" if @schema_to.nil?
      
      res = @transformer.begin_transaction(@schema_from, @schema_to) unless @transformer.nil?
      
      ActiveRecord::Base.transaction do      
        begin_migration()
    
        # TODO: Make flexible configurable and more input formats
        if @schema_from[:format].to_s.to_sym == :XLS
            xls_migrate()
        end
      
        end_migration()
      end
      
      res = @transformer.end_transaction(@schema_from, @schema_to) unless @transformer.nil?
      
      return true
    end
    
    
    def xls_migrate
      begin
        @xls = Spreadsheet.open @schema_from[:url]
        # TODO: make others workbook accessible by configuration
        sheet = @xls.worksheet 0
    
        @line = 0
    
        # ignore head line
        sheet.each 1 do |row|
          @column = 0
          row_to = { }
          
          #read schema columns and types
          @schema_from[:columns].each do |schema_column, schema_type|
            row_to.merge!(schema_column.to_sym => row[@column])
            @column+=1
          end
          
          #transform row to @schema_to
          res = true
          res = @transformer.transform(row_to) unless @transformer.nil?
          
          if (res!=:ignore)
            res = res==true && send_row_to_schema(row_to)
            raise_migration if (res==false)
            
            @transformer.after_row_saved(row_to, @last_object) unless @transformer.nil?
          end
          
          @line+=1
        end
      rescue Exception => e
        line = @line.nil? ? 0 : @line
        column = @column.nil? ? 0 : @column
        raise ActiveMigartionDataSourceError.new ("Failing import excel source format from %s. %d:%d [ignored head]. " % [@schema_from[:url], column, line]).concat(e.message).concat("\n----"+e.backtrace.to_yaml)
      end
    end
    
    
    def begin_migration
      # TODO: make transactional
      res = @transformer.begin(@schema_from, @schema_to) unless @transformer.nil?
    end
    
    
    def end_migration
      res = @transformer.end(@schema_from, @schema_to) unless @transformer.nil?
    end
    
    def raise_migration
      raise "failing migration %s.  Line: %d, Column: %d" % [@name, @line, @column]
    end
  
    def send_row_to_schema(row)
      if @schema_to[:format].to_sym == :ACTIVE_RECORD
      
        # TODO: optimize on initialize migration
        class_schema_to = eval @schema_to[:url]
      
        @last_object = class_schema_to.new(row)
        res = @last_object.save
      
        if (!res)
          raise AciteMigrationInvalidRecordError.new "[Schema:%s] Error on send to ACTIVE_RECORD %s. \n%s \nrow: \n%s" % [@name, @schema_to[:url], @last_object.errors.to_yaml, row.to_yaml]
        end
      
        return res
      
      else
        raise "Not valid schema::TO format! %s" % @name  
      end
    end
    
    ##
    # loads yml file and convert to hash on schema_from
    # @deprecated
    def load_schema_from(url)
      self.schema_from = YAML::load(File.open(url))
    end
    
    ##
    # load yml file and convert to hash on schema_to
    # @deprecated
    def load_schema_to(url)
      self.schema_to = YAML::load(File.open(url))      
    end
    
  end
  
end