module Fabrial::Fabricate

TODO: properly hook up parent associations for pre-created objects passed in

Public Instance Methods

add_default_return(objects) click to toggle source

If a return object(s) wasn't specified, default to returning the first object. TODO: Raise error if mixing array and hash return styles

# File lib/fabrial/fabricate.rb, line 25
def add_default_return(objects)
  return if contains_return? objects

  ret = objects.values.find do |v|
    v.is_a?(Hash) || (v.is_a?(Array) && !v.empty?)
  end
  if ret
    ret = ret.first if ret.is_a? Array
    ret[:RETURN] = true
  end
end
extract_child_records(klass, data) click to toggle source
# File lib/fabrial/fabricate.rb, line 37
def extract_child_records(klass, data)
  children = data.select do |type, v|
    # Must have nested data
    [Array, Hash].any? { |c| v.is_a? c } &&
      # Must be a class that we can instantiate
      get_class(type) &&

      # Even if it has the same name as a Model in the system, if it is also
      # the name of a column in the table, assume the data is for a serialzed
      # field and not a nested relation.  Ex: Requests have a serialized field
      # called content and there is also a Content model in the system.
      (
        # If they are using a class as the key, then always choose the class
        # over the field.
        type.is_a?(Class) ||

        !column_names(klass).include?(type.to_s)
      )
  end
  data.extract!(*children.keys)
end
fabricate(objects) click to toggle source

Make expects a nested hash of type => data or [data]

# File lib/fabrial/fabricate.rb, line 7
def fabricate(objects)
  objects = Fabrial.run_before_fabricate objects
  add_default_return objects

  ancestors = {}
  returns = make_types objects, ancestors

  # If returns is made up of pairs, return a hash.
  if returns.first.is_a? Array
    returns.to_h
  else
    returns.length <= 1 ? returns.first : returns
  end
end

Private Instance Methods

collect_associations(klass, ancestors) click to toggle source
# File lib/fabrial/fabricate.rb, line 154
def collect_associations(klass, ancestors)
  associations = collect_parents(klass, ancestors)
  if (p = polymorphic(klass))
    associations[p] = ancestors.values.last
  end
  associations
end
collect_parents(klass, ancestors) click to toggle source
  unless children.key? connector.name.demodulize.underscore.to_sym
    children.reverse_merge! connector => {}
  end
end

end

# File lib/fabrial/fabricate.rb, line 183
def collect_parents(klass, ancestors)
  associations = klass.reflect_on_all_associations
  polymorphics = associations.select(&:polymorphic?)
  associations
    .select do |a|
      (!a.collection? || a.macro == :has_and_belongs_to_many) &&
        !a.polymorphic? &&

        # This is to throw out specified versions of polymorphic associations.
        # Ex: Alert has a polymorphic association called `alertable` and two
        # other associations, `alertable_patient` and `alertable_client` that
        # allow joining to specific tables.  These specified associations
        # should be skipped.
        polymorphics.none? do |other|
          other != a && other.name.to_s == a.name.to_s.split('_').first
        end
    end
    .select { |a| ancestors.key? a.klass } # Find ancestors that match
    .map do |a| # Create data hash
      if a.macro == :has_and_belongs_to_many
        [a.name, [ancestors[a.klass]]]
      else
        [a.name, ancestors[a.klass]]
      end
    end.to_h
end
column_names(klass) click to toggle source
# File lib/fabrial/fabricate.rb, line 162
def column_names(klass)
  klass.column_names

  # our project uses
  # klass.column_names_including_stored
end
contains_return?(objects) click to toggle source
# File lib/fabrial/fabricate.rb, line 61
def contains_return?(objects)
  return false unless objects.is_a? Hash

  objects.key?(:RETURN) || objects.any? do |_k, v|
    v.is_a?(Hash) && contains_return?(v) ||
      v.is_a?(Array) && v.any?(&method(:contains_return?))
  end
end
get_class(type) click to toggle source
# File lib/fabrial/fabricate.rb, line 216
def get_class(type)
  return type if type.is_a? Class

  type.to_s.classify.safe_constantize ||
    type.to_s.classify.pluralize.safe_constantize
end
make_object(klass, data, associations) click to toggle source
# File lib/fabrial/fabricate.rb, line 139
def make_object(klass, data, associations)
  # Check for already created object
  return data[:object] if data[:object]

  type_col = klass.inheritance_column.try :to_sym
  type = data.delete(type_col).try :safe_constantize
  type ||= klass
  begin
    create type, data.reverse_merge(associations)
  rescue
    raise Fabrial::CreationError,
      "Error creating #{type.name} with data: #{data}"
  end
end
make_type(klass, data_list, ancestors) click to toggle source
# File lib/fabrial/fabricate.rb, line 107
def make_type(klass, data_list, ancestors)
  returns = []

  # TODO: may need a hook here
  # ::Maker.next_bank klass # Needed if we aren't in test

  associations = collect_associations(klass, ancestors)
  Array.wrap(data_list).each do |data|
    should_return = data.delete :RETURN
    children = extract_child_records klass, data
    run_before_create klass, data, ancestors, children
    object = make_object klass, data, associations

    # Make sure new object is added as last item of ancestor hash
    # collect_associations expects this in order to hook up polymorphic fields
    next_ancestors = ancestors.dup.except klass
    next_ancestors[klass] = object

    if should_return
      # If `RETURN` holds a value, use it as the key.
      returns << if should_return == true
                   object
                 else
                   [should_return, object]
                 end
    end

    returns.concat make_types children, next_ancestors
  end
  returns
end
make_types(objects, ancestors) click to toggle source

return_skip_levels allows us to skip created default practices and sources when choosing an object to return.

# File lib/fabrial/fabricate.rb, line 94
def make_types(objects, ancestors)
  returns = []
  objects.each do |type, data|
    klass = get_class(type)
    if klass.nil?
      raise Fabrial::UnknownClassError, "Class #{type} does not exist"
    end

    returns.concat make_type klass, data, ancestors
  end
  returns
end
polymorphic(klass) click to toggle source
# File lib/fabrial/fabricate.rb, line 210
def polymorphic(klass)
  klass.reflect_on_all_associations
    .select(&:polymorphic?)
    .map { |a| a.class_name.underscore.to_sym }[0]
end