class ViewModel::ActiveRecord::AssociationData

Attributes

association_name[R]
direct_reflection[R]

Public Class Methods

new(owner:, association_name:, direct_association_name:, indirect_association_name:, target_viewmodels:, external:, through_order_attr:, read_only:) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 8
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

Public Instance Methods

accepts?(viewmodel_class) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 189
def accepts?(viewmodel_class)
  viewmodel_classes.include?(viewmodel_class)
end
association?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 90
def association?
  true
end
collection?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 227
def collection?
  through? || direct_reflection.collection?
end
direct_reflection_inverse(foreign_class = nil) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 150
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
direct_viewmodel() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 220
def direct_viewmodel
  raise ArgumentError.new('not a through association') unless through?

  lazy_initialize! unless @initialized
  @direct_viewmodel
end
external?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 112
def external?
  @external
end
indirect_association_data() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 231
def indirect_association_data
  direct_viewmodel._association_data(indirect_reflection.name)
end
indirect_reflection() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 145
def indirect_reflection
  lazy_initialize! unless @initialized
  @indirect_reflection
end
lazy_initialize!() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 36
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
nested?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 99
def nested?
  !referenced?
end
ordered?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 201
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
owned?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 103
def owned?
  lazy_initialize! unless @initialized
  @owned
end
pointer_location() click to toggle source

The side of the immediate association that holds the pointer.

# File lib/view_model/active_record/association_data.rb, line 136
def pointer_location
  case direct_reflection.macro
  when :belongs_to
    :local
  when :has_one, :has_many
    :remote
  end
end
polymorphic?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 129
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
read_only?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 116
def read_only?
  @read_only
end
referenced?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 94
def referenced?
  lazy_initialize! unless @initialized
  @referenced
end
shared?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 108
def shared?
  !owned?
end
target_reflection() click to toggle source

reflection for the target of this association: indirect if through, direct otherwise

# File lib/view_model/active_record/association_data.rb, line 121
def target_reflection
  if through?
    indirect_reflection
  else
    direct_reflection
  end
end
through?() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 216
def through?
  @indirect_association_name.present?
end
viewmodel_class() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 193
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
viewmodel_class_for_model(model_class) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 163
def viewmodel_class_for_model(model_class)
  model_to_viewmodel[model_class]
end
viewmodel_class_for_model!(model_class) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 167
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
viewmodel_class_for_name(name) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 176
def viewmodel_class_for_name(name)
  name_to_viewmodel[name]
end
viewmodel_class_for_name!(name) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 180
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
viewmodel_classes() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 158
def viewmodel_classes
  lazy_initialize! unless @initialized
  @viewmodel_classes
end

Private Instance Methods

build_direct_viewmodel(direct_reflection, indirect_reflection, viewmodel_classes, through_order_attr) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 259
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
infer_viewmodel_class(model_class) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 284
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
load_indirect_reflection(intermediate_model, indirect_association_name) click to toggle source

Through associations must always be to a root viewmodel, via an owned has_many association to an intermediate model. A synthetic viewmodel is created to represent this intermediate, but is used only internally by the deserialization update operations, which directly understands the semantics of through associations.

# File lib/view_model/active_record/association_data.rb, line 242
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
model_to_viewmodel() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 296
def model_to_viewmodel
  @model_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h|
    h[vm.model_class] = vm
  end
end
name_to_viewmodel() click to toggle source
# File lib/view_model/active_record/association_data.rb, line 302
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
resolve_viewmodel_class(v) click to toggle source
# File lib/view_model/active_record/association_data.rb, line 273
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