ManageIQ/inventory_refresh

View on GitHub
lib/inventory_refresh/inventory_object_lazy.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
96%
require "active_support/core_ext/module/delegation"

module InventoryRefresh
  class InventoryObjectLazy
    attr_reader :reference, :inventory_collection, :key, :default, :transform_nested_lazy_finds

    delegate :stringified_reference, :ref, :[], :to => :reference

    # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object owning the
    #        InventoryObject
    # @param index_data [Hash] data of the InventoryObject object
    # @param ref [Symbol] reference name
    # @param key [Symbol] key name, will be used to fetch attribute from resolved InventoryObject
    # @param default [Object] a default value used if the :key will resolve to nil
    # @param transform_nested_lazy_finds [Boolean] True if we want to convert all lazy objects in InventoryObject
    #        objects and reset the Reference. TODO(lsmola) we should be able to do this automatically, then we can
    #        remove this option
    def initialize(inventory_collection, index_data, ref: :manager_ref, key: nil, default: nil, transform_nested_lazy_finds: false)
      @inventory_collection = inventory_collection
      @reference            = inventory_collection.build_reference(index_data, ref)
      @key                  = key
      @default              = default

      @transform_nested_lazy_finds = transform_nested_lazy_finds

      # We do not support skeletal pre-create for :key, since :key will not be available, we want to use local_db_find
      # instead.
      skeletal_precreate! unless @key
    end

    # @return [String] stringified reference
    def to_s
      # TODO(lsmola) do we need this method?
      stringified_reference
    end

    # @return [String] string format for nice logging
    def inspect
      suffix = ""
      suffix += ", ref: #{ref}" if ref.present?
      suffix += ", key: #{key}" if key.present?
      "InventoryObjectLazy:('#{self}', #{inventory_collection}#{suffix})"
    end

    # @param inventory_object [InventoryRefresh::InventoryObject] InventoryObject object owning this relation
    # @param inventory_object_key [Symbol] InventoryObject object's attribute pointing to this relation
    # @return [InventoryRefresh::InventoryObject, Object] InventoryRefresh::InventoryObject instance or an attribute
    #         on key
    def load(inventory_object = nil, inventory_object_key = nil)
      transform_nested_secondary_indexes! if transform_nested_lazy_finds && nested_secondary_index?

      load_object(inventory_object, inventory_object_key)
    end

    # return [Boolean] true if the Lazy object is causing a dependency, Lazy link is always a dependency if no :key
    #        is provider or if it's transitive_dependency
    def dependency?
      # If key is not set, InventoryObjectLazy is a dependency, cause it points to the record itself. Otherwise
      # InventoryObjectLazy is a dependency only if it points to an attribute which is a dependency or a relation.
      !key || transitive_dependency?
    end

    # return [Boolean] true if the Lazy object is causing a transitive dependency, which happens if the :key points
    #        to an attribute that is causing a dependency.
    def transitive_dependency?
      # If the dependency is inventory_collection.lazy_find(:ems_ref, :key => :stack)
      # and a :stack is a relation to another object, in the InventoryObject object,
      # then this relation is considered transitive.
      key && association?(key)
    end

    # @return [Boolean] true if the key is an association on inventory_collection_scope model class
    def association?(key)
      # TODO(lsmola) remove this if there will be better dependency scan, probably with transitive dependencies filled
      # in a second pass, then we can get rid of this hardcoded symbols. Right now we are not able to introspect these.
      return true if [:parent, :genealogy_parent].include?(key)

      inventory_collection.dependency_attributes.key?(key) ||
        !inventory_collection.association_to_foreign_key_mapping[key].nil?
    end

    def transform_nested_secondary_indexes!(depth = 0)
      raise "Nested references are too deep!" if depth > 20

      keys.each do |x|
        attr = full_reference[x]
        next unless attr.kind_of?(InventoryRefresh::InventoryObjectLazy)
        next if attr.primary?

        if attr.nested_secondary_index?
          attr.transform_nested_secondary_indexes!(depth + 1)
        end

        full_reference[x] = full_reference[x].load
      end

      # Rebuild the reference to get the right value
      self.reference = inventory_collection.build_reference(full_reference, ref)
    end

    private

    delegate :parallel_safe?, :saved?, :saver_strategy, :skeletal_primary_index, :targeted?, :to => :inventory_collection
    delegate :nested_secondary_index?, :primary?, :full_reference, :keys, :primary?, :to => :reference

    attr_writer :reference

    # Instead of loading the reference from the DB, we'll add the skeletal InventoryObject (having manager_ref and
    # info from the default_values) to the correct InventoryCollection. Which will either be found in the DB or
    # created as a skeletal object. The later refresh of the object will then fill the rest of the data, while not
    # touching the reference.
    #
    # @return [InventoryRefresh::InventoryObject, NilClass] Returns pre-created InventoryObject or nil
    def skeletal_precreate!
      # We can do skeletal pre-create only for strategies using unique indexes. Since this can build records out of
      # the given :arel scope, we will always attempt to create the recod, so we need unique index to avoid duplication
      # of records.
      return unless parallel_safe?
      # Pre-create only for strategies that will be persisting data, i.e. are not saved already
      return if saved?
      # We can only do skeletal pre-create for primary index reference, since that is needed to create DB unique index
      return unless primary?
      # Full reference must be present
      return if full_reference.blank?

      # To avoid pre-creating invalid records all fields of a primary key must have value
      # TODO(lsmola) for composite keys, it's still valid to have one of the keys nil, figure out how to allow this
      return if keys.any? { |x| full_reference[x].blank? }

      skeletal_primary_index.build(full_reference)
    end

    # @param loaded_object [InventoryRefresh::InventoryObject, NilClass] Loaded object or nil if object wasn't found
    # @return [Object] value found or :key or default value if the value is nil
    def load_object_with_key(loaded_object)
      # TODO(lsmola) Log error if we are accessing path that is present in blacklist or not present in whitelist
      if loaded_object.present?
        if loaded_object.try(:data).present?
          loaded_object.data[key] || default
        else
          loaded_object.public_send(key) || default
        end
      else
        default
      end
    end

    # @return [InventoryRefresh::InventoryObject, NilClass] InventoryRefresh::InventoryObject instance or nil if not found
    def load_object(inventory_object = nil, inventory_object_key = nil)
      loaded_object = inventory_collection.find(reference)

      if inventory_object && inventory_object_key && !loaded_object && reference.loadable?
        # Object was not loaded, but the reference is pointing to something, lets return it as edge that should've
        # been loaded.
        inventory_object.inventory_collection.store_unconnected_edges(inventory_object, inventory_object_key, self)
      end

      key ? load_object_with_key(loaded_object) : loaded_object
    end
  end
end