From a96438f37e13c8fc5449dde7ff118d5a3a2a5215 Mon Sep 17 00:00:00 2001 From: Adam Eberlin Date: Thu, 10 Dec 2015 11:16:08 -0600 Subject: [PATCH] Code dump. --- .ruby-version | 1 + lib/csv_transform.rb | 3 + lib/csv_transform/configuration.rb | 96 ++++++++++++++++++++++++++++++ lib/csv_transform/importer.rb | 95 +++++++++++++++++++++++++++++ lib/csv_transform/object_mixins.rb | 55 +++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 .ruby-version create mode 100644 lib/csv_transform.rb create mode 100644 lib/csv_transform/configuration.rb create mode 100644 lib/csv_transform/importer.rb create mode 100644 lib/csv_transform/object_mixins.rb diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..8274681 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.2.3 diff --git a/lib/csv_transform.rb b/lib/csv_transform.rb new file mode 100644 index 0000000..34ce2db --- /dev/null +++ b/lib/csv_transform.rb @@ -0,0 +1,3 @@ +require 'csv_transform/configuration' +require 'csv_transform/importer' +require 'csv_transform/object_mixins' diff --git a/lib/csv_transform/configuration.rb b/lib/csv_transform/configuration.rb new file mode 100644 index 0000000..764041c --- /dev/null +++ b/lib/csv_transform/configuration.rb @@ -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 diff --git a/lib/csv_transform/importer.rb b/lib/csv_transform/importer.rb new file mode 100644 index 0000000..e2ca5e3 --- /dev/null +++ b/lib/csv_transform/importer.rb @@ -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 diff --git a/lib/csv_transform/object_mixins.rb b/lib/csv_transform/object_mixins.rb new file mode 100644 index 0000000..64d6306 --- /dev/null +++ b/lib/csv_transform/object_mixins.rb @@ -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