botandrose/chop

View on GitHub
lib/chop/create.rb

Summary

Maintainability
A
45 mins
Test Coverage
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/object/blank"
require "active_support/hash_with_indifferent_access"
require "active_support/core_ext/class/attribute"

module Chop
  class Create < Struct.new(:klass, :table, :block)
    def self.create! klass, table, &block
      new(klass, table, block).create!
    end

    class_attribute :creation_strategies
    self.creation_strategies = {}

    def self.register_creation_strategy key, &block
      creation_strategies[key] = block
    end

    register_creation_strategy nil do |klass, attributes|
      klass.create! attributes
    end

    register_creation_strategy :factory_girl do |factory, attributes|
      FactoryGirl.create factory, attributes
    end

    register_creation_strategy :factory_bot do |factory, attributes|
      FactoryBot.create factory, attributes
    end

    attr_accessor :transformations, :deferred_attributes, :after_hooks

    def initialize(*, &other_block)
      super
      self.transformations = []
      self.deferred_attributes = HashWithIndifferentAccess.new
      self.after_hooks = []
      instance_eval &block if block.respond_to?(:call)
      instance_eval &other_block if block_given?
    end

    def create! cucumber_table = table
      cucumber_table.hashes.map do |attributes|
        attributes = HashWithIndifferentAccess.new(attributes)
        attributes = transformations.reduce(attributes) do |attrs, transformation|
          transformation.call(attrs)
        end

        strategy, factory = klass.is_a?(Hash) ? klass.to_a.first : [nil, klass]
        args = [factory, attributes]
        record = creation_strategies[strategy].call(*args.compact)

        after_hooks.each do |after_hook|
          after_hook.call(record, attributes.merge(deferred_attributes))
        end
        record
      end
    end

    def create &block
      self.creation_strategies = Proc.new { block }
    end

    def transformation &block
      transformations << block
    end

    def delete *keys
      transformation do |attributes|
        keys.reduce(attributes) do |attrs, key|
          attributes.delete(key)
          attributes
        end
      end
    end

    def copy mappings
      transformation do |attributes|
        mappings.each do |from, to|
          attributes[to] = attributes[from]
        end
        attributes
      end
    end

    def rename mappings
      transformation do |attributes|
        mappings.reduce(attributes) do |attrs, (old, new)|
          attrs[new] = attrs.delete(old) if attrs.key?(old)
          attrs
        end
      end
    end

    def field attribute, default: ""
      if attribute.is_a?(Hash)
        rename attribute
        attribute = attribute.values.first
      end
      transformation do |attributes|
        attributes.merge attribute => yield(attributes.fetch(attribute, default))
      end
    end

    def default key, default_value = nil
      field(key, default: nil) { |value| value || default_value || yield }
    end

    def underscore_keys
      transformation do |attributes|
        attributes.reduce(HashWithIndifferentAccess.new) do |hash, (key, value)|
          hash.merge key.parameterize.underscore => value
        end
      end
    end

    def file *keys
      options = extract_options!(keys)
      path = options.fetch(:path, "features/support/fixtures")
      upload = options.fetch(:upload, false)

      handle_renames! keys

      keys.each do |key|
        field key do |file|
          if file.present?
            file_path = File.join(path, file)
            if upload
              Rack::Test::UploadedFile.new(file_path)
            else
              File.open(file_path)
            end
          end
        end
      end
    end

    def files *keys
      options = extract_options!(keys)
      path = options.fetch(:path, "features/support/fixtures")
      upload = options.fetch(:upload, false)
      delimiter = options.fetch(:delimiter, " ")

      handle_renames! keys

      keys.each do |key|
        field key do |paths|
          paths.split(delimiter).map do |file|
            file_path = File.join(path, file)
            if upload
              Rack::Test::UploadedFile.new(file_path)
            else
              File.open(file_path)
            end
          end
        end
      end
    end

    def has_many key, klass=nil, delimiter: ", ", name_field: :name
      klass ||= key.to_s.classify.constantize
      field key do |names|
        names.split(delimiter).map do |name|
          klass.find_by!(name_field => name)
        end
      end
    end

    def has_one key, klass=nil, name_field: :name
      klass ||= key.to_s.classify.constantize
      field key do |name|
        klass.find_by!(name_field => name) if name.present?
      end
    end
    alias_method :belongs_to, :has_one

    def after *keys, &block
      defer *keys
      after_hooks << block
    end

    def defer *keys
      transformation do |attributes|
        keys.each do |key|
          self.deferred_attributes[key] = attributes.delete(key)
        end
        attributes
      end
    end

    private

    def extract_options! keys
      if keys.last.is_a?(Hash) && keys.length > 1
        keys.pop
      else
        {}
      end
    end

    def handle_renames! keys
      if keys.first.is_a?(Hash) && keys.length == 1
        rename keys.first
        keys.replace keys.first.values
      end
    end
  end
end