Skip to content

Commit

Permalink
Code dump.
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Eberlin committed Dec 10, 2015
0 parents commit a96438f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 0 deletions.
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby-2.2.3
3 changes: 3 additions & 0 deletions lib/csv_transform.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require 'csv_transform/configuration'
require 'csv_transform/importer'
require 'csv_transform/object_mixins'
96 changes: 96 additions & 0 deletions lib/csv_transform/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'yaml'

class Configuration
def initialize(file_path)
@raw = Configuration.normalize_partial(YAML.load_file(file_path))
end

def default_value(header)
@raw.try(:fields).try(header).try(:default_value)
end

def formatters(defaults, key)
@raw[:fields].inject({}) do |hash, (field, opts)|
if opts.is_a?(Hash) && opts.try(key)
hash << { field => (opts[key] + defaults) }
else
hash << { field => defaults }
end
end
end

def default_field_formatters
@raw.try(:global).try(:fields).try(:formatters) || []
end

def field_keys
@raw.try(:fields).try(:keys)
end

def field_formatters
formatters(default_field_formatters, :formatters)
end

def default_header_formatters
defaults = @raw.try(:global).try(:headers).try(:formatters) || []
defaults << :to_sym
end

def header_formatters
formatters(default_header_formatters, :header_formatters)
end

def field_translations
@raw[:fields].inject({}) do |vocab, (field, opts)|
if opts.try(:has_key?, :lexicon)
vocab << { field => opts[:lexicon] }
else
vocab
end
end
end

def header_translations
table = @raw[:fields].inject({}) do |vocab, (field, opts)|
if opts.is_a?(Hash) && opts.try(:translations)
vocab << { field => opts[:translations] }
else
vocab << { field => [field] }
end
end
table.try(:inverse) || {}
end

def value_type(field)
@raw.try(:fields).try(field).try(:type) || :symbol
end

def normalized_value(field, value)
Configuration.normalize_value(value_type(field), value)
end

def self.normalize_partial(data)
data.instance_exec(self) do |config|
return config.normalize_partial_hash(self) if is_a?(Hash)
return config.normalize_partial_array(self) if is_a?(Array)
return config.normalize_value(:symbol, self) if is_a?(String)
self
end
end

def self.normalize_partial_hash(data)
data.inject({}) { |a, (k, v)| a << { k.to_sym => normalize_partial(v) } }
end

def self.normalize_partial_array(data)
data.inject([]) { |a, e| a << normalize_partial(e) }
end

def self.normalize_value(type, value)
case type
when :symbol then value.to_s.downcase.snake_case.to_sym
when :integer then value.to_i
else value
end
end
end
95 changes: 95 additions & 0 deletions lib/csv_transform/importer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require 'csv'

class Importer
OPTIONS = {
converters: [:instantiate, :numeric, :transform],
headers: :first_row,
header_converters: [:transform]
}

def self.import(file_path, entry_klass, entries = [])
data = load(file_path)
config = data[:configuration]
data[:table].each.inject(entries) do |a, e|
entry = include_missing(config, Hash[e.headers.zip(e.fields)])
a << entry_klass.new(**entry)
end
end

def self.load(file_path)
config = Configuration.new("#{file_path}.yml")
setup_converters(config)
{ configuration: config, table: CSV.read(file_path, OPTIONS) }
end

def self.include_missing(config, entry_hash)
(config.field_keys - entry_hash.keys).inject(entry_hash) do |a, e|
a << { e => config.default_value(e) }
end
end

def self.setup_converters(config)
setup_field_converters(config)
setup_header_converters(config)
end

def self.setup_field_converters(config)
instantiate_field_converter(config)
transform_field_converter(config)
end

def self.instantiate_field_converter(config)
CSV::Converters[:instantiate] = lambda do |value, info|
value = value.try(:empty?) ? nil : value
config.default_value(info.header) || value
end
end

def self.transform_field_converter(config)
locale = config.field_translations
formatters = config.field_formatters
CSV::Converters[:transform] = lambda do |value, info|
unless value.nil?
value = config.normalized_value(info.header, value)
format_field(formatters, locale, info.header, value)
end
end
end

def self.setup_header_converters(config)
locale = config.header_translations
formatters = config.header_formatters
CSV::HeaderConverters[:transform] = lambda do |value|
value = Configuration.normalize_value(:symbol, value)
format_header(formatters, locale, value)
end
end

def self.format_value(formatters, field, value = nil)
formatters.instance_exec(field, value || field) do |k, v|
(try(k) || []).inject(v) { |a, e| a.try(e) || a }
end
end

def self.format_field(formatters, locale, field, value)
field = format_value(formatters, field)
translate_field(locale, field, value)
end

def self.format_header(formatters, locale, field)
field = format_value(formatters, field)
translate_header(locale, field)
end

def self.translate(matrix, *keys)
keys.inject(matrix) { |a, e| a.try(e) } || keys.last
end

def self.translate_field(locale, field, value)
translate(locale, field, value)
end

def self.translate_header(locale, value)
translate(locale, value)
end
end
55 changes: 55 additions & 0 deletions lib/csv_transform/object_mixins.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Utility
module ObjectMixins
def try(*a, &b)
if a.empty? && block_given?
yield self
else
public_send(*a, &b) if respond_to?(a.first)
end
end
end
Object.include(ObjectMixins)
module HashMixins
def inverse
inject({}) do |hash, (key, array)|
array.map { |word| hash[word] = key }
hash
end
end

def try(*a, &b)
if a.size == 1
k = a.first
return self[k] if key?(k) && !respond_to?(k)
end
super(*a, &b)
end

def <<(hash)
merge!(hash)
end
end
Hash.include(HashMixins)
module StringMixins
def snake_case
strip.gsub(' ', '_')
end
end
String.include(StringMixins)
module SymbolMixins
def include?(target)
to_s.include?(target.to_s)
end

def snake_case
to_s.snake_case.to_sym
end
end
Symbol.include(SymbolMixins)
module TextMixins
def text?
true
end
end
[String, Symbol].each { |i| i.include(TextMixins) }
end

0 comments on commit a96438f

Please sign in to comment.