Skip to content

Commit

Permalink
WIP owned referenced collections
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisandreae committed May 24, 2019
1 parent 3ab214d commit 362d98e
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 95 deletions.
4 changes: 0 additions & 4 deletions lib/view_model/active_record/association_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ def lazy_initialize!
raise InvalidAssociation.new('Invalid association target: mixed root and non-root viewmodels')
end

if @referenced && !through? && direct_reflection.collection?
raise InvalidAssociation.new('Invalid association type: referenced has_many associations to roots are not yet implemented')
end

if external? && !@referenced
raise InvalidAssociation.new('External associations must be to root viewmodels')
end
Expand Down
14 changes: 11 additions & 3 deletions lib/view_model/active_record/update_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,13 @@ class Functional < AbstractCollectionUpdate::Functional

def used_vm_refs(update_context)
update_datas
.map { |upd| upd.viewmodel_reference if upd.id }
.map { |upd| resolve_vm_reference(upd, update_context) }
.compact
end

def resolve_vm_reference(update_data, _update_context)
update_data.viewmodel_reference if update_data.id
end
end

class Parser < AbstractCollectionUpdate::Parser
Expand Down Expand Up @@ -322,8 +326,12 @@ class Functional < AbstractCollectionUpdate::Functional

def used_vm_refs(update_context)
references.map do |ref|
update_context.resolve_reference(ref, nil).viewmodel_reference
end
resolve_vm_reference(ref, update_context)
end # TODO: Why doesn't this compact? Otherwise identical to Owned. Can legitimately be nil if ref to new.
end

def resolve_vm_reference(ref, update_context)
update_context.resolve_reference(ref, nil).viewmodel_reference
end
end

Expand Down
248 changes: 160 additions & 88 deletions lib/view_model/active_record/update_operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ def build!(update_context)

update =
if association_data.through?
# TODO: has_many referenced associations are not yet implemented
build_updates_for_collection_referenced_association(association_data, reference_string, update_context)
elsif association_data.collection?
build_updates_for_collection_association(association_data, reference_string, update_context)
else
build_update_for_single_association(association_data, reference_string, update_context)
end
Expand Down Expand Up @@ -286,14 +287,20 @@ def resolve_child_viewmodels(association_data, update_datas, previous_child_view
end
end

def resolve_referenced_viewmodels(reference_strings, previous_child_viewmodels, association_data, update_context)
def resolve_referenced_viewmodels(association_data, update_datas, previous_child_viewmodels, update_context)
previous_child_viewmodels = Array.wrap(previous_child_viewmodels)

previous_child_refs = previous_child_viewmodels.map(&:to_reference).to_set

ViewModel::Utils.map_one_or_many(reference_strings) do |reference_string|
child_update = update_context.resolve_reference(reference_string, blame_reference)
child_viewmodel = child_update.viewmodel
ViewModel::Utils.map_one_or_many(update_datas) do |update_data|
if update_data.is_a?(UpdateData)
# Dummy child update data for functional update; return untouched
next [update_data, update_data.viewmodel]
end

reference_string = update_data
child_update = update_context.resolve_reference(reference_string, blame_reference)
child_viewmodel = child_update.viewmodel

unless association_data.accepts?(child_viewmodel.class)
raise ViewModel::DeserializationError::InvalidAssociationType.new(
Expand All @@ -312,12 +319,25 @@ def resolve_referenced_viewmodels(reference_strings, previous_child_viewmodels,
if claimed
[child_update, child_viewmodel]
else
# Return the ref to signal a deferred update
# Return the reference to indicate a deferred update
[child_update, child_ref]
end
end
end

def set_reference_update_parent(association_data, update, parent_data)
if update.reparent_to
# Another parent has already tried to take this (probably new)
# owned referenced view. It can only be claimed by one of them.
other_parent = update.reparent_to.viewmodel.to_reference
raise ViewModel::DeserializationError::DuplicateOwner.new(
association_data.association_name,
[blame_reference, other_parent])
end

update.reparent_to = parent_data
end

def build_update_for_single_association(association_data, association_update_data, update_context)
model = self.viewmodel.model

Expand Down Expand Up @@ -345,20 +365,11 @@ def build_update_for_single_association(association_data, association_update_dat
if association_data.referenced?
# resolve reference string
reference_string = association_update_data
child_update, child_viewmodel = resolve_referenced_viewmodels(reference_string, previous_child_viewmodel,
association_data, update_context)
child_update, child_viewmodel = resolve_referenced_viewmodels(association_data, reference_string,
previous_child_viewmodel, update_context)

if reparent_data
if child_update.reparent_to
# Another parent has already tried to take this (probably new)
# owned referenced view. It can only be claimed by one of them.
other_parent = child_update.reparent_to.viewmodel.to_reference
raise ViewModel::DeserializationError::DuplicateOwner.new(
association_data.association_name,
[blame_reference, other_parent])
end

child_update.reparent_to = reparent_data
set_reference_update_parent(association_data, child_update, reparent_data)
end

if child_viewmodel.is_a?(ViewModel::Reference)
Expand Down Expand Up @@ -423,11 +434,13 @@ def build_updates_for_collection_association(association_data, association_updat
parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)

# load children already attached to this model
child_viewmodel_class = association_data.viewmodel_class
child_viewmodel_class = association_data.viewmodel_class

previous_child_viewmodels =
model.public_send(association_data.direct_reflection.name).map do |child_model|
child_viewmodel_class.new(child_model)
end

if child_viewmodel_class._list_member?
previous_child_viewmodels.sort_by!(&:_list_attribute)
end
Expand All @@ -442,114 +455,173 @@ def build_updates_for_collection_association(association_data, association_updat
clear_association_cache(model, association_data.direct_reflection)
end

child_datas =
case association_update
when OwnedCollectionUpdate::Replace
association_update.update_datas
# Update contents are either UpdateData in the case of a nested
# association or reference strings in the case of a reference association.
# The former are resolved with resolve_child_viewmodels, the latter with
# resolve_referenced_viewmodels.
case association_update
when AbstractCollectionUpdate::Replace
child_datas = association_update.contents

when OwnedCollectionUpdate::Functional
child_datas =
previous_child_viewmodels.map do |previous_child_viewmodel|
UpdateData.empty_update_for(previous_child_viewmodel)
end
when AbstractCollectionUpdate::Functional
association_update.check_for_duplicates!(update_context, blame_reference)

association_update.check_for_duplicates!(update_context, self.viewmodel.blame_reference)
# Construct empty updates for previous children
child_datas =
previous_child_viewmodels.map do |previous_child_viewmodel|
UpdateData.empty_update_for(previous_child_viewmodel)
end

association_update.actions.each do |fupdate|
case fupdate
when FunctionalUpdate::Append
if fupdate.before || fupdate.after
moved_refs = fupdate.contents.map(&:viewmodel_reference).to_set
child_datas = child_datas.reject { |child| moved_refs.include?(child.viewmodel_reference) }
# Insert or replace with either real UpdateData or reference strings
association_update.actions.each do |fupdate|
case fupdate
when FunctionalUpdate::Append
# If we're referring to existing members, ensure that they're removed before we append/insert
existing_refs = fupdate.contents
.map { |cnt| association_update.resolve_vm_reference(cnt, update_context) }
.to_set

child_datas.reject! do |child_data|
existing_refs.include?(child_data.viewmodel_reference) if child_data.is_a?(UpdateData)
end

ref = fupdate.before || fupdate.after
index = child_datas.find_index { |cd| cd.viewmodel_reference == ref }
unless index
raise ViewModel::DeserializationError::AssociatedNotFound.new(
association_data.association_name.to_s, ref, blame_reference)
end
if fupdate.before || fupdate.after
rel_ref = fupdate.before || fupdate.after

# Find the relative insert location. This might be an empty
# UpdateData from a previous child or an already-fupdated
# reference string.
index = child_datas.find_index do |child_data|
rel_ref == case child_data
when UpdateData
child_data.viewmodel_reference
else
association_update.resolve_vm_reference(child_data, update_context)
end
end

index += 1 if fupdate.after
child_datas.insert(index, *fupdate.contents)
unless index
raise ViewModel::DeserializationError::AssociatedNotFound.new(
association_data.association_name.to_s, rel_ref, blame_reference)
end

else
child_datas.concat(fupdate.contents)
index += 1 if fupdate.after
child_datas.insert(index, *fupdate.contents)

end
else
child_datas.concat(fupdate.contents)
end

when FunctionalUpdate::Remove
removed_refs = fupdate.removed_vm_refs.to_set
child_datas.reject! { |child_data| removed_refs.include?(child_data.viewmodel_reference) }
when FunctionalUpdate::Remove
removed_refs = fupdate.removed_vm_refs.to_set
child_datas.reject! do |child_data|
removed_refs.include?(child_data.viewmodel_reference) if child_data.is_a?(UpdateData)
end

when FunctionalUpdate::Update
# Already guaranteed that each ref has a single data attached
new_datas = fupdate.contents.index_by(&:viewmodel_reference)
when FunctionalUpdate::Update
# Already guaranteed that each ref has a single data attached
new_datas = fupdate.contents.index_by do |content|
association_update.resolve_vm_reference(content, update_context)
end

child_datas = child_datas.map do |child_data|
ref = child_data.viewmodel_reference
new_datas.delete(ref) { child_data }
end
# Replace matched child_datas with the update contents. We know it's
# not already a reference string, as we've already eliminated
# duplicates.
child_datas.map! do |child_data|
next child_data unless child_data.is_a?(UpdateData)

# Assertion that all values in update_op.values are present in the collection
unless new_datas.empty?
raise ViewModel::DeserializationError::AssociatedNotFound.new(
association_data.association_name.to_s, new_datas.keys, blame_reference)
end
else
raise ViewModel::DeserializationError::InvalidSyntax.new(
"Unknown functional update type: '#{fupdate.type}'",
blame_reference)
ref = child_data.viewmodel_reference
new_datas.delete(ref) { child_data }
end

# Assertion that all values in the update were found in child_datas
unless new_datas.empty?
raise ViewModel::DeserializationError::AssociatedNotFound.new(
association_data.association_name.to_s, new_datas.keys, blame_reference)
end
else
raise ViewModel::DeserializationError::InvalidSyntax.new(
"Unknown functional update type: '#{fupdate.type}'",
blame_reference)
end
end
end

if association_data.referenced?
# child_datas are either UpdateData (empty, for existing members) or
# reference strings. Resolve into pairs of [UpdateData, ViewModel] if
# claimed or [UpdateData, ViewModelReference] otherwise.
resolved_children =
resolve_referenced_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)

resolved_children.each do |child_update, child_viewmodel|
set_reference_update_parent(association_data, child_update, parent_data)

child_datas
if child_viewmodel.is_a?(ViewModel::Reference)
update_context.defer_update(child_viewmodel, child_update)
end
end

child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
else
# child datas are all UpdateData
child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)

# if the new children differ, including in order, mark that one of our
# associations has changed and release any no-longer-attached children
if child_viewmodels != previous_child_viewmodels
viewmodel.association_changed!(association_data.association_name)
released_child_viewmodels = previous_child_viewmodels - child_viewmodels
released_child_viewmodels.each do |vm|
release_viewmodel(vm, association_data, update_context)
resolved_children = child_datas.zip(child_viewmodels).map do |child_data, child_viewmodel|
child_update =
if child_viewmodel.is_a?(ViewModel::Reference)
update_context.new_deferred_update(child_viewmodel, child_data, reparent_to: parent_data)
else
update_context.new_update(child_viewmodel, child_data, reparent_to: parent_data)
end

[child_update, child_viewmodel]
end
end

# Calculate new positions for children if in a list. Ignore previous
# positions for unresolved references: they'll always need to be updated
# anyway since their parent pointer will change.
new_positions = Array.new(child_viewmodels.length)
# positions (i.e. return nil) for unresolved references: they'll always
# need to be updated anyway since their parent pointer will change.
new_positions = Array.new(resolved_children.length)

if association_data.viewmodel_class._list_member?
set_position = ->(index, pos) { new_positions[index] = pos }

get_previous_position = ->(index) do
vm = child_viewmodels[index]
vm = resolved_children[index][1]
vm._list_attribute unless vm.is_a?(ViewModel::Reference)
end

ActsAsManualList.update_positions(
(0...child_viewmodels.size).to_a, # indexes
(0...resolved_children.size).to_a, # indexes
position_getter: get_previous_position,
position_setter: set_position)
end

# Recursively build update operations for children
child_updates = child_viewmodels.zip(child_datas, new_positions).map do |child_viewmodel, association_update_data, position|
case child_viewmodel
when ViewModel::Reference # deferred
reference = child_viewmodel
update_context.new_deferred_update(reference, association_update_data, reparent_to: parent_data, reposition_to: position)
else
update_context.new_update(child_viewmodel, association_update_data, reparent_to: parent_data, reposition_to: position).build!(update_context)
resolved_children.zip(new_positions).each do |(child_update, child_viewmodel), new_position|
child_update.reposition_to = new_position

# Recurse into building child updates that we've claimed
unless child_viewmodel.is_a?(ViewModel::Reference)
child_update.build!(update_context)
end
end

child_updates, child_viewmodels = resolved_children.transpose.presence || [[], []]

# if the new children differ, including in order, mark that this
# association has changed and release any no-longer-attached children
if child_viewmodels != previous_child_viewmodels
viewmodel.association_changed!(association_data.association_name)

released_child_viewmodels = previous_child_viewmodels - child_viewmodels
released_child_viewmodels.each do |vm|
release_viewmodel(vm, association_data, update_context)
end
end

child_updates
end


class ReferencedCollectionMember
attr_reader :indirect_viewmodel_reference, :direct_viewmodel
attr_accessor :ref_string, :position
Expand Down

0 comments on commit 362d98e

Please sign in to comment.