lib/inventory_refresh/inventory_collection/data_storage.rb
require "active_support/core_ext/module/delegation"
module InventoryRefresh
class InventoryCollection
class DataStorage
# @return [Array<InventoryObject>] objects of the InventoryCollection in an Array
attr_accessor :data
attr_reader :index_proxy, :inventory_collection
delegate :each, :size, :to => :data
delegate :primary_index,
:build_primary_index_for,
:build_secondary_indexes_for,
:named_ref,
:skeletal_primary_index,
:to => :index_proxy
delegate :association_to_foreign_key_mapping,
:default_values,
:inventory_object?,
:inventory_object_lazy?,
:manager_ref,
:new_inventory_object,
:to => :inventory_collection
# @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we want the storage
# for
# @param secondary_refs [Hash] Secondary_refs in format {:name_of_the_ref => [:attribute1, :attribute2]}
def initialize(inventory_collection, secondary_refs)
@inventory_collection = inventory_collection
@data = []
@index_proxy = InventoryRefresh::InventoryCollection::Index::Proxy.new(inventory_collection, secondary_refs)
end
# Adds passed InventoryObject into the InventoryCollection's storage
#
# @param inventory_object [InventoryRefresh::InventoryObject]
# @return [InventoryRefresh::InventoryCollection] Returns current InventoryCollection, to allow chaining
def <<(inventory_object)
if inventory_object.manager_uuid.present? && !primary_index.find(inventory_object.manager_uuid)
data << inventory_object
# TODO(lsmola) Maybe we do not need the secondary indexes here?
# Maybe we should index it like LocalDb indexes, on demand, and storing what was
# indexed? Maybe we should allow only lazy access and no direct find from a parser. Since for streaming
# refresh, things won't be parsed together and no full state will be taken.
build_primary_index_for(inventory_object)
build_secondary_indexes_for(inventory_object)
end
inventory_collection
end
alias push <<
# Finds of builds a new InventoryObject. By building it, we also put in into the InventoryCollection's storage.
#
# @param manager_uuid [String] manager_uuid of the InventoryObject
# @return [InventoryRefresh::InventoryObject] Found or built InventoryObject
def find_or_build(manager_uuid)
raise "The uuid consists of #{manager_ref.size} attributes, please find_or_build_by method" if manager_ref.size > 1
find_or_build_by(manager_ref.first => manager_uuid)
end
# (see #build)
def find_or_build_by(hash)
build(hash)
end
# Finds InventoryObject.
#
# @param hash [Hash] Hash that needs to contain attributes defined in :manager_ref of the InventoryCollection
# @return [InventoryRefresh::InventoryObject] Found or built InventoryObject object
def find_in_data(hash)
_hash, _uuid, inventory_object = primary_index_scan(hash)
inventory_object
end
# Finds of builds a new InventoryObject. By building it, we also put in into the InventoryCollection's storage.
#
# @param hash [Hash] Hash that needs to contain attributes defined in :manager_ref of the
# InventoryCollection
# @return [InventoryRefresh::InventoryObject] Found or built InventoryObject object
def build(hash)
hash, uuid, inventory_object = primary_index_scan(hash)
# Return InventoryObject if found in primary index
return inventory_object unless inventory_object.nil?
# We will take existing skeletal record, so we don't duplicate references for saving. We can have duplicated
# reference from local_db index, (if we are using .find in parser, that causes N+1 db queries), but that is ok,
# since that one is not being saved.
inventory_object = skeletal_primary_index.delete(uuid)
# We want to update the skeletal record with actual data
inventory_object&.assign_attributes(hash)
# Build the InventoryObject
inventory_object ||= new_inventory_object(enrich_data(hash))
# Store new InventoryObject and return it
push(inventory_object)
inventory_object
end
# Finds of builds a new InventoryObject with incomplete data.
#
# @param hash [Hash] Hash that needs to contain attributes defined in :manager_ref of the
# InventoryCollection
# @return [InventoryRefresh::InventoryObject] Found or built InventoryObject object
def build_partial(hash)
skeletal_primary_index.build(hash)
end
# Returns array of built InventoryObject objects
#
# @return [Array<InventoryRefresh::InventoryObject>] Array of built InventoryObject objects
def to_a
data
end
def to_hash
InventoryRefresh::InventoryCollection::Serialization.new(inventory_collection).to_hash
end
def from_hash(inventory_objects_data, available_inventory_collections)
InventoryRefresh::InventoryCollection::Serialization
.new(inventory_collection)
.from_hash(inventory_objects_data, available_inventory_collections)
end
private
# Scans primary index for existing InventoryObject, that would be equivalent to passed hash. It also returns
# enriched data and uuid, so we do not have to compute it multiple times.
#
# @param hash [Hash] Attributes for the InventoryObject
# @return [Array(Hash, String, InventoryRefresh::InventoryObject)] Returns enriched data, uuid and InventoryObject
# if found (otherwise nil)
def primary_index_scan(hash)
hash = enrich_data(hash)
assert_all_keys_present(hash)
assert_only_primary_index(hash)
uuid = ::InventoryRefresh::InventoryCollection::Reference.build_stringified_reference(hash, named_ref)
return hash, uuid, primary_index.find(uuid)
end
def assert_all_keys_present(hash)
if manager_ref.any? { |x| !hash.key?(x) }
raise "Needed find_or_build_by keys are: #{manager_ref}, data provided: #{hash}"
end
end
def assert_only_primary_index(data)
named_ref.each do |key|
if data[key].kind_of?(InventoryRefresh::InventoryObjectLazy) && !data[key].primary?
raise "Wrong index for key :#{key}, all references under this index must point to default :ref called"\
" :manager_ref. Any other :ref is not valid. This applies also to nested lazy links."
end
end
end
# Returns new hash enriched by (see InventoryRefresh::InventoryCollection#default_values) hash
#
# @param hash [Hash] Input hash
# @return [Hash] Enriched hash by (see InventoryRefresh::InventoryCollection#default_values)
def enrich_data(hash)
# This is 25% faster than default_values.merge(hash)
{}.merge!(default_values).merge!(hash)
end
end
end
end