class ViewModel::ActiveRecord::UpdateData
def self.check_duplicate_updates(updates, type:)
def self.check_duplicate_updates(updates, type:) # Ensure that no root is referred to more than once duplicates = updates.duplicates_by { |upd| upd.viewmodel_reference if upd.id } if duplicates.present? raise ViewModel::DeserializationError::DuplicateNodes.new(type, duplicates.keys) end end
def self.empty_update_for(viewmodel)
def self.empty_update_for(viewmodel) metadata = ViewModel::Metadata.new(viewmodel.id, viewmodel.view_name, viewmodel.class.schema_version, false) self.new(viewmodel.class, metadata, {}, []) end
def self.parse_associated(association_data, blame_reference, valid_reference_keys, child_hash)
def self.parse_associated(association_data, blame_reference, valid_reference_keys, child_hash) child_metadata = ViewModel.extract_viewmodel_metadata(child_hash) child_viewmodel_class = association_data.viewmodel_class_for_name(child_metadata.view_name) if child_viewmodel_class.nil? raise ViewModel::DeserializationError::InvalidAssociationType.new( association_data.association_name.to_s, child_metadata.view_name, blame_reference) end verify_schema_version!(child_viewmodel_class, child_metadata.schema_version, child_metadata.id) if child_metadata.schema_version UpdateData.new(child_viewmodel_class, child_metadata, child_hash, valid_reference_keys) end
def self.parse_hashes(root_subtree_hashes, referenced_subtree_hashes = {})
def self.parse_hashes(root_subtree_hashes, referenced_subtree_hashes = {}) valid_reference_keys = referenced_subtree_hashes.keys.to_set valid_reference_keys.each do |ref| unless ref.is_a?(String) raise ViewModel::DeserializationError::InvalidSyntax.new("Invalid reference string: #{ref}") end end # Construct root UpdateData root_updates = root_subtree_hashes.map do |subtree_hash| metadata = ViewModel.extract_viewmodel_metadata(subtree_hash) viewmodel_class = ViewModel::Registry.for_view_name(metadata.view_name) verify_schema_version!(viewmodel_class, metadata.schema_version, metadata.id) if metadata.schema_version UpdateData.new(viewmodel_class, metadata, subtree_hash, valid_reference_keys) end # Ensure that no root is referred to more than once check_duplicate_updates(root_updates, type: 'root') # Construct reference UpdateData referenced_updates = referenced_subtree_hashes.transform_values do |subtree_hash| metadata = ViewModel.extract_viewmodel_metadata(subtree_hash) viewmodel_class = ViewModel::Registry.for_view_name(metadata.view_name) verify_schema_version!(viewmodel_class, metadata.schema_version, metadata.id) if metadata.schema_version UpdateData.new(viewmodel_class, metadata, subtree_hash, valid_reference_keys) end check_duplicate_updates(referenced_updates.values, type: 'reference') return root_updates, referenced_updates end
def self.reference_only_hash?(hash)
def self.reference_only_hash?(hash) hash.size == 2 && hash.has_key?(ViewModel::ID_ATTRIBUTE) && hash.has_key?(ViewModel::TYPE_ATTRIBUTE) end
def self.verify_schema_version!(viewmodel_class, schema_version, id)
def self.verify_schema_version!(viewmodel_class, schema_version, id) unless viewmodel_class.accepts_schema_version?(schema_version) raise ViewModel::DeserializationError::SchemaVersionMismatch.new( viewmodel_class, schema_version, ViewModel::Reference.new(viewmodel_class, id)) end end
def [](name)
def [](name) case name when :id id when :_type viewmodel_class.view_name else attributes.fetch(name) { associations.fetch(name) { referenced_associations.fetch(name) } } end end
def blame_reference
def blame_reference ViewModel::Reference.new(self.viewmodel_class, self.id) end
def build_preload_specs(association_data, updates)
def build_preload_specs(association_data, updates) if association_data.polymorphic? updates.map do |update_data| target_model = update_data.viewmodel_class.model_class DeepPreloader::PolymorphicSpec.new( target_model.name => update_data.preload_dependencies) end else updates.map { |update_data| update_data.preload_dependencies } end end
def has_key?(name)
def has_key?(name) case name when :id, :_type true else attributes.has_key?(name) || associations.has_key?(name) || referenced_associations.has_key?(name) end end
def initialize(viewmodel_class, metadata, hash_data, valid_reference_keys)
def initialize(viewmodel_class, metadata, hash_data, valid_reference_keys) self.viewmodel_class = viewmodel_class self.metadata = metadata self.attributes = {} self.associations = {} self.referenced_associations = {} parse(hash_data, valid_reference_keys) end
def merge_preload_specs(association_data, specs)
def merge_preload_specs(association_data, specs) empty = association_data.polymorphic? ? DeepPreloader::PolymorphicSpec.new : DeepPreloader::Spec.new specs.inject(empty) { |acc, spec| acc.merge!(spec) } end
def parse(hash_data, valid_reference_keys)
def parse(hash_data, valid_reference_keys) hash_data = hash_data.dup # handle view pre-parsing if defined self.viewmodel_class.pre_parse(viewmodel_reference, metadata, hash_data) if self.viewmodel_class.respond_to?(:pre_parse) hash_data.keys.each do |key| # rubocop:disable Style/HashEachMethods if self.viewmodel_class.respond_to?(:"pre_parse_#{key}") self.viewmodel_class.public_send("pre_parse_#{key}", viewmodel_reference, metadata, hash_data, hash_data.delete(key)) end end hash_data.each do |name, value| member_data = self.viewmodel_class._members[name] case member_data when ViewModel::Record::AttributeData attributes[name] = value when AssociationData association_data = member_data case when value.nil? if association_data.collection? raise ViewModel::DeserializationError::InvalidSyntax.new( "Invalid collection update value 'nil' for association '#{name}'", blame_reference) end if association_data.referenced? referenced_associations[name] = nil else associations[name] = nil end when association_data.referenced? if association_data.collection? referenced_associations[name] = ReferencedCollectionUpdate::Parser .new(association_data, blame_reference, valid_reference_keys) .parse(value) else # Extract and check reference ref = ViewModel.extract_reference_metadata(value) unless valid_reference_keys.include?(ref) raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference) end referenced_associations[name] = ref end else associations[name] = if association_data.collection? OwnedCollectionUpdate::Parser .new(association_data, blame_reference, valid_reference_keys) .parse(value) else # not a collection if value.nil? # rubocop:disable Style/IfInsideElse nil else self.class.parse_associated(association_data, blame_reference, valid_reference_keys, value) end end end else raise ViewModel::DeserializationError::UnknownAttribute.new(name, blame_reference) end end end
def preload_dependencies
Updates in terms of activerecord associations: used for preloading subtree
def preload_dependencies deps = {} associations.merge(referenced_associations).each do |assoc_name, reference| association_data = self.viewmodel_class._association_data(assoc_name) preload_specs = build_preload_specs(association_data, to_sequence(assoc_name, reference)) referenced_deps = merge_preload_specs(association_data, preload_specs) if association_data.through? referenced_deps = DeepPreloader::Spec.new(association_data.indirect_reflection.name.to_s => referenced_deps) end deps[association_data.direct_reflection.name.to_s] = referenced_deps end deserialization_includes = viewmodel_class.deserialization_includes unless deserialization_includes.is_a?(DeepPreloader::AbstractSpec) deserialization_includes = DeepPreloader::Spec.parse(deserialization_includes) end DeepPreloader::Spec.new(deps).merge!(deserialization_includes) end
def reference_only?
def reference_only? attributes.empty? && associations.empty? && referenced_associations.empty? end
def to_sequence(name, value)
def to_sequence(name, value) association_data = self.viewmodel_class._association_data(name) case when value.nil? [] when association_data.referenced? [] when association_data.collection? # nested, because of referenced? check above value.update_datas else [value] end end
def viewmodel_reference
def viewmodel_reference ViewModel::Reference.new(viewmodel_class, id) end