class ViewModel::ActiveRecord::AssociationData
def accepts?(viewmodel_class)
def accepts?(viewmodel_class) viewmodel_classes.include?(viewmodel_class) end
def association?
def association? true end
def build_direct_viewmodel(direct_reflection, indirect_reflection, viewmodel_classes, through_order_attr)
def build_direct_viewmodel(direct_reflection, indirect_reflection, viewmodel_classes, through_order_attr) # Join table viewmodel class. For A has_many B through T; where this association is defined on A # direct_reflection = A -> T # indirect_reflection = T -> B Class.new(ViewModel::ActiveRecord) do self.synthetic = true self.model_class = direct_reflection.klass self.view_name = direct_reflection.klass.name association indirect_reflection.name, viewmodels: viewmodel_classes acts_as_list through_order_attr if through_order_attr end end
def collection?
def collection? through? || direct_reflection.collection? end
def direct_reflection_inverse(foreign_class = nil)
def direct_reflection_inverse(foreign_class = nil) if direct_reflection.polymorphic? direct_reflection.polymorphic_inverse_of(foreign_class) else direct_reflection.inverse_of end end
def direct_viewmodel
def direct_viewmodel raise ArgumentError.new('not a through association') unless through? lazy_initialize! unless @initialized @direct_viewmodel end
def external?
def external? @external end
def indirect_association_data
def indirect_association_data direct_viewmodel._association_data(indirect_reflection.name) end
def indirect_reflection
def indirect_reflection lazy_initialize! unless @initialized @indirect_reflection end
def infer_viewmodel_class(model_class)
def infer_viewmodel_class(model_class) # If we weren't given explicit viewmodel classes, try to work out from the # names. This should work unless the association is polymorphic. if model_class.nil? raise InvalidAssociation.new("Couldn't derive target class for model association '#{target_reflection.name}'") end inferred_view_name = ViewModel::Registry.default_view_name(model_class.name) viewmodel_class = ViewModel::Registry.for_view_name(inferred_view_name) # TODO: improve error message to show it's looking for default name [viewmodel_class] end
def initialize(owner:,
def initialize(owner:, association_name:, direct_association_name:, indirect_association_name:, target_viewmodels:, external:, through_order_attr:, read_only:) @association_name = association_name @direct_reflection = owner.model_class.reflect_on_association(direct_association_name) if @direct_reflection.nil? raise InvalidAssociation.new("Association '#{direct_association_name}' not found in model '#{owner.model_class.name}'") end @indirect_association_name = indirect_association_name @read_only = read_only @external = external @through_order_attr = through_order_attr @target_viewmodels = target_viewmodels # Target models/reflections/viewmodels are lazily evaluated so that we can # safely express cycles. @initialized = false @mutex = Mutex.new end
def lazy_initialize!
def lazy_initialize! @mutex.synchronize do return if @initialized if through? intermediate_model = @direct_reflection.klass @indirect_reflection = load_indirect_reflection(intermediate_model, @indirect_association_name) target_reflection = @indirect_reflection else target_reflection = @direct_reflection end @viewmodel_classes = if @target_viewmodels.present? # Explicitly named @target_viewmodels.map { |v| resolve_viewmodel_class(v) } else # Infer name from name of model if target_reflection.polymorphic? raise InvalidAssociation.new( 'Cannot automatically infer target viewmodels from polymorphic association') end infer_viewmodel_class(target_reflection.klass) end @referenced = @viewmodel_classes.first.root? # Non-referenced viewmodels must be owned. For referenced viewmodels, we # own it if it points to us. Through associations aren't considered # `owned?`: while we do own the implicit direct viewmodel, we don't own # the target of the association. @owned = !@referenced || (target_reflection.macro != :belongs_to) unless @viewmodel_classes.all? { |v| v.root? == @referenced } raise InvalidAssociation.new('Invalid association target: mixed root and non-root viewmodels') end if external? && !@referenced raise InvalidAssociation.new('External associations must be to root viewmodels') end if through? unless @referenced raise InvalidAssociation.new('Through associations must be to root viewmodels') end @direct_viewmodel = build_direct_viewmodel(@direct_reflection, @indirect_reflection, @viewmodel_classes, @through_order_attr) end @initialized = true end end
def load_indirect_reflection(intermediate_model, indirect_association_name)
deserialization update operations, which directly understands the semantics
created to represent this intermediate, but is used only internally by the
has_many association to an intermediate model. A synthetic viewmodel is
Through associations must always be to a root viewmodel, via an owned
def load_indirect_reflection(intermediate_model, indirect_association_name) indirect_reflection = intermediate_model.reflect_on_association(ActiveSupport::Inflector.singularize(indirect_association_name)) if indirect_reflection.nil? raise InvalidAssociation.new( "Indirect association '#{@indirect_association_name}' not found in "\ "intermediate model '#{intermediate_model.name}'") end unless direct_reflection.macro == :has_many raise InvalidAssociation.new('Through associations must be `has_many`') end indirect_reflection end
def model_to_viewmodel
def model_to_viewmodel @model_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h| h[vm.model_class] = vm end end
def name_to_viewmodel
def name_to_viewmodel @name_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h| h[vm.view_name] = vm vm.view_aliases.each do |view_alias| h[view_alias] = vm end end end
def nested?
def nested? !referenced? end
def ordered?
def ordered? @ordered ||= if through? direct_viewmodel._list_member? else list_members = viewmodel_classes.map { |c| c._list_member? }.uniq if list_members.size > 1 raise ArgumentError.new('Inconsistent associated views: mixed list membership') end list_members[0] end end
def owned?
def owned? lazy_initialize! unless @initialized @owned end
def pointer_location
def pointer_location case direct_reflection.macro when :belongs_to :local when :has_one, :has_many :remote end end
def polymorphic?
def polymorphic? # STI polymorphism isn't shown on the association reflection, so in that # case we have to infer it by having multiple target viewmodel types. target_reflection.polymorphic? || viewmodel_classes.size > 1 end
def read_only?
def read_only? @read_only end
def referenced?
def referenced? lazy_initialize! unless @initialized @referenced end
def resolve_viewmodel_class(v)
def resolve_viewmodel_class(v) case v when String, Symbol ViewModel::Registry.for_view_name(v.to_s) when Class v else raise InvalidAssociation.new("Invalid viewmodel class: #{v.inspect}") end end
def shared?
def shared? !owned? end
def target_reflection
def target_reflection if through? indirect_reflection else direct_reflection end end
def through?
def through? @indirect_association_name.present? end
def viewmodel_class
def viewmodel_class unless viewmodel_classes.size == 1 raise ArgumentError.new("More than one possible class for association '#{target_reflection.name}'") end viewmodel_classes.first end
def viewmodel_class_for_model(model_class)
def viewmodel_class_for_model(model_class) model_to_viewmodel[model_class] end
def viewmodel_class_for_model!(model_class)
def viewmodel_class_for_model!(model_class) vm_class = viewmodel_class_for_model(model_class) if vm_class.nil? raise ArgumentError.new( "Invalid viewmodel model for association '#{target_reflection.name}': '#{model_class.name}'") end vm_class end
def viewmodel_class_for_name(name)
def viewmodel_class_for_name(name) name_to_viewmodel[name] end
def viewmodel_class_for_name!(name)
def viewmodel_class_for_name!(name) vm_class = viewmodel_class_for_name(name) if vm_class.nil? raise ArgumentError.new( "Invalid viewmodel name for association '#{target_reflection.name}': '#{name}'") end vm_class end
def viewmodel_classes
def viewmodel_classes lazy_initialize! unless @initialized @viewmodel_classes end