aboutsummaryrefslogtreecommitdiffstats
path: root/app/models/simple_interface.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/simple_interface.rb')
-rw-r--r--app/models/simple_interface.rb297
1 files changed, 297 insertions, 0 deletions
diff --git a/app/models/simple_interface.rb b/app/models/simple_interface.rb
new file mode 100644
index 000000000..489419482
--- /dev/null
+++ b/app/models/simple_interface.rb
@@ -0,0 +1,297 @@
+class SimpleInterface < ActiveRecord::Base
+ attr_accessor :configuration
+
+ class << self
+ def configuration_class
+ "#{self.name}::Configuration".constantize
+ end
+
+ def define name
+ @importers ||= {}
+ configuration = configuration_class.new name
+ yield configuration if block_given?
+ @importers[name.to_sym] = configuration
+ end
+
+ def find_configuration name
+ @importers ||= {}
+ configuration = @importers[name.to_sym]
+ raise "Importer not found: #{name}" unless configuration
+ configuration
+ end
+ end
+
+ def initialize *args
+ super *args
+ self.configuration = self.class.find_configuration self.configuration_name
+ self.journal ||= []
+ end
+
+ def init_env opts
+ @verbose = opts.delete :verbose
+
+ @errors = []
+ @messages = []
+ @padding = 1
+ @current_line = -1
+ @padding = [1, Math.log([@number_of_lines, 1].max, 10).ceil()].max
+ end
+
+ def configure
+ new_config = configuration.duplicate
+ yield new_config
+ self.configuration = new_config
+ end
+
+ def context
+ self.configuration.context
+ end
+
+ def fail_with_error msg=nil, opts={}
+ begin
+ yield
+ rescue => e
+ msg = msg.call if msg.is_a?(Proc)
+ custom_print "\nFAILED: \n errors: #{msg}\n exception: #{e.message}\n#{e.backtrace.join("\n")}", color: :red unless self.configuration.ignore_failures
+ push_in_journal({message: msg, error: e.message, event: :error, kind: :error})
+ @new_status = colorize("x", :red)
+ if self.configuration.ignore_failures
+ raise SimpleInterface::FailedRow if opts[:abort_row]
+ else
+ raise FailedOperation
+ end
+ end
+ end
+
+ def log msg, opts={}
+ msg = msg.to_s
+ msg = colorize msg, opts[:color] if opts[:color]
+ if opts[:append]
+ @messages[-1] = (@messages[-1] || "") + msg
+ else
+ @messages << msg
+ end
+ print_state
+ end
+
+ protected
+
+ def push_in_journal data
+ line = (@current_line || 0) + 1
+ line += 1 if configuration.headers
+ @errors ||= []
+ self.journal.push data.update(line: line, row: @current_row)
+ if data[:kind] == :error || data[:kind] == :warning
+ @errors.push data
+ end
+ end
+
+ def colorize txt, color
+ color = {
+ red: "31",
+ green: "32",
+ orange: "33",
+ }[color] || "33"
+ "\e[#{color}m#{txt}\e[0m"
+ end
+
+ def print_state
+ return unless @verbose
+
+ @status_width ||= begin
+ term_width = %x(tput cols).to_i
+ term_width - @padding - 10
+ rescue
+ 100
+ end
+
+ @status_height ||= begin
+ term_height = %x(tput lines).to_i
+ term_height - 3
+ rescue
+ 50
+ end
+
+ full_status = @statuses || ""
+ full_status = full_status.last(@status_width*10) || ""
+ padding_size = [(@number_of_lines - @current_line - 1), (@status_width - full_status.size/10)].min
+ full_status = "#{full_status}#{"."*[padding_size, 0].max}"
+
+ msg = "#{"%#{@padding}d" % (@current_line + 1)}/#{@number_of_lines}: #{full_status}"
+
+ lines_count = [(@status_height / 2) - 3, 1].max
+
+ if @messages.any?
+ msg += "\n\n"
+ msg += colorize "=== MESSAGES (#{@messages.count}) ===\n", :green
+ msg += "[...]\n" if @messages.count > lines_count
+ msg += @messages.last(lines_count).map{|m| m.truncate(@status_width)}.join("\n")
+ msg += "\n"*[lines_count-@messages.count, 0].max
+ end
+
+ if @errors.any?
+ msg += "\n\n"
+ msg += colorize "=== ERRORS (#{@errors.count}) ===\n", :red
+ msg += "[...]\n" if @errors.count > lines_count
+ msg += @errors.last(lines_count).map do |j|
+ kind = j[:kind]
+ kind = colorize(kind, kind == :error ? :red : :orange)
+ kind = "[#{kind}]"
+ kind += " "*(25 - kind.size)
+ encode_string("#{kind}L#{j[:line]}\t#{j[:error]}\t\t#{j[:message]}").truncate(@status_width)
+ end.join("\n")
+ end
+ custom_print msg, clear: true
+ end
+
+ def custom_print msg, opts={}
+ return unless @verbose
+ out = ""
+ msg = colorize(msg, opts[:color]) if opts[:color]
+ puts "\e[H\e[2J" if opts[:clear]
+ out += msg
+ print out
+ end
+
+ class FailedRow < RuntimeError
+ end
+
+ class FailedOperation < RuntimeError
+ end
+
+ class Configuration
+ attr_accessor :headers, :separator, :key, :context, :encoding, :ignore_failures, :scope
+ attr_reader :columns
+
+ def initialize import_name, opts={}
+ @import_name = import_name
+ @key = opts[:key] || "id"
+ @headers = opts.has_key?(:headers) ? opts[:headers] : true
+ @separator = opts[:separator] || ","
+ @encoding = opts[:encoding]
+ @columns = opts[:columns] || []
+ @custom_handler = opts[:custom_handler]
+ @before = opts[:before]
+ @after = opts[:after]
+ @ignore_failures = opts[:ignore_failures]
+ @context = opts[:context] || {}
+ @scope = opts[:scope]
+ end
+
+ def on_relation relation_name
+ @current_scope ||= []
+ @current_scope.push relation_name
+ yield
+ @current_scope.pop
+ end
+
+ def duplicate
+ self.class.new @import_name, self.options
+ end
+
+ def options
+ {
+ key: @key,
+ headers: @headers,
+ separator: @separator,
+ encoding: @encoding,
+ columns: @columns.map(&:duplicate),
+ custom_handler: @custom_handler,
+ before: @before,
+ after: @after,
+ ignore_failures: @ignore_failures,
+ context: @context,
+ scope: @scope
+ }
+ end
+
+ def attribute_for_col col_name
+ column = self.columns.find{|c| c.name == col_name}
+ column && column[:attribute] || col_name
+ end
+
+ def record_scope
+ _scope = @scope
+ _scope = instance_exec(&_scope) if _scope.is_a?(Proc)
+ _scope || model
+ end
+
+ def find_record attrs
+ record_scope.find_or_initialize_by(attribute_for_col(@key) => attrs[@key.to_s])
+ end
+
+ def csv_options
+ {
+ headers: self.headers,
+ col_sep: self.separator,
+ encoding: self.encoding
+ }
+ end
+
+ def add_column name, opts={}
+ @current_scope ||= []
+ @columns.push Column.new({name: name.to_s, scope: @current_scope.dup}.update(opts))
+ end
+
+ def add_value attribute, value
+ @columns.push Column.new({attribute: attribute, value: value})
+ end
+
+ def before group=:all, &block
+ @before ||= Hash.new{|h, k| h[k] = []}
+ @before[group].push block
+ end
+
+ def after group=:all, &block
+ @after ||= Hash.new{|h, k| h[k] = []}
+ @after[group].push block
+ end
+
+ def before_actions group=:all
+ @before ||= Hash.new{|h, k| h[k] = []}
+ @before[group]
+ end
+
+ def after_actions group=:all
+ @after ||= Hash.new{|h, k| h[k] = []}
+ @after[group]
+ end
+
+ def custom_handler &block
+ @custom_handler = block
+ end
+
+ def get_custom_handler
+ @custom_handler
+ end
+
+ class Column
+ attr_accessor :name, :attribute
+ def initialize opts={}
+ @name = opts[:name]
+ @options = opts
+ @attribute = @options[:attribute] ||= @name
+ end
+
+ def duplicate
+ Column.new @options.dup
+ end
+
+ def required?
+ !!@options[:required]
+ end
+
+ def omit_nil?
+ !!@options[:omit_nil]
+ end
+
+ def scope
+ @options[:scope] || []
+ end
+
+ def [](key)
+ @options[key]
+ end
+ end
+ end
+end