class ViewModel

def ==(other)

def ==(other)
  other.class == self.class && self.class._attributes.all? do |attr|
    other.send(attr) == self.send(attr)
  end
end

def accepts_schema_version?(schema_version)

def accepts_schema_version?(schema_version)
  schema_version == self.schema_version
end

def add_view_alias(as)

def add_view_alias(as)
  view_aliases << as
  ViewModel::Registry.register(self, as: as)
end

def attribute(attr, **_args)

def attribute(attr, **_args)
  unless attr.is_a?(Symbol)
    raise ArgumentError.new('ViewModel attributes must be symbols')
  end
  attr_accessor attr
  define_method("deserialize_#{attr}") do |value, references: {}, deserialize_context: self.class.new_deserialize_context|
    self.public_send("#{attr}=", value)
  end
  _attributes << attr
end

def attributes(*attrs, **args)

accessors and assigned in order by the default constructor.
bit easier to define them: attributes specified this way are given
ViewModels are typically going to be pretty simple structures. Make it a
def attributes(*attrs, **args)
  attrs.each { |attr| attribute(attr, **args) }
end

def blame_reference

merged with its parent.
is reported as to blame. Can be overridden for example when a viewmodel is
When deserializing, if an error occurs within this viewmodel, what viewmodel
def blame_reference
  to_reference
end

def context_for_child(member_name, context:)

def context_for_child(member_name, context:)
  context.for_child(self, association_name: member_name)
end

def deserialize_context_class

def deserialize_context_class
  ViewModel::DeserializeContext
end

def deserialize_from_view(hash_data, references: {}, deserialize_context: new_deserialize_context)

Rebuild this viewmodel from a serialized hash.
def deserialize_from_view(hash_data, references: {}, deserialize_context: new_deserialize_context)
  viewmodel = self.new
  deserialize_members_from_view(viewmodel, hash_data, references: references, deserialize_context: deserialize_context)
  viewmodel
end

def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)

def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
  ViewModel::Callbacks.wrap_deserialize(viewmodel, deserialize_context: deserialize_context) do |hook_control|
    if (bad_attrs = view_hash.keys - member_names).present?
      causes = bad_attrs.map do |bad_attr|
        ViewModel::DeserializationError::UnknownAttribute.new(bad_attr, viewmodel.blame_reference)
      end
      raise ViewModel::DeserializationError::Collection.for_errors(causes)
    end
    member_names.each do |attr|
      next unless view_hash.has_key?(attr)
      viewmodel.public_send("deserialize_#{attr}",
                            view_hash[attr],
                            references: references,
                            deserialize_context: deserialize_context)
    end
    deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel)
    viewmodel.validate!
    # More complex viewmodels can use this hook to track changes to
    # persistent backing models, and record the results. Primitive
    # viewmodels record no changes.
    if block_given?
      yield(hook_control)
    else
      hook_control.record_changes(Changes.new)
    end
  end
end

def eager_includes(include_referenced: true)

AR-style nested hashes or DeepPreloader::Spec.
use of? Returns a includes spec appropriate for DeepPreloader, either as
If this viewmodel represents an AR model, what associations does it make
def eager_includes(include_referenced: true)
  {}
end

def encode_json(value)

def encode_json(value)
  # Jbuilder#encode no longer uses MultiJson, but instead calls `.to_json`. In
  # the context of ActiveSupport, we don't want this, because AS replaces the
  # .to_json interface with its own .as_json, which demands that everything is
  # reduced to a Hash before it can be JSON encoded. Using this is not only
  # slightly more expensive in terms of allocations, but also defeats the
  # purpose of our precompiled `CompiledJson` terminals. Instead serialize
  # using OJ with options equivalent to those used by MultiJson.
  Oj.dump(value, mode: :compat, time_format: :ruby, use_to_json: true)
end

def extract_reference_metadata(hash)

def extract_reference_metadata(hash)
  ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_REFERENCE, hash)
  hash.delete(ViewModel::REFERENCE_ATTRIBUTE)
end

def extract_reference_only_metadata(hash)

def extract_reference_only_metadata(hash)
  ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
  id             = hash.delete(ViewModel::ID_ATTRIBUTE)
  type_name      = hash.delete(ViewModel::TYPE_ATTRIBUTE)
  Metadata.new(id, type_name, nil, false, false)
end

def extract_viewmodel_metadata(hash)

In deserialization, verify and extract metadata from a provided hash.
def extract_viewmodel_metadata(hash)
  ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
  id             = hash.delete(ViewModel::ID_ATTRIBUTE)
  type_name      = hash.delete(ViewModel::TYPE_ATTRIBUTE)
  schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
  new            = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
  migrated       = hash.delete(ViewModel::MIGRATED_ATTRIBUTE) { false }
  Metadata.new(id, type_name, schema_version, new, migrated)
end

def hash

def hash
  features = self.class._attributes.map { |attr| self.send(attr) }
  features << self.class
  features.hash
end

def id

it.
model with a concept of identity, this method should be overridden to use
so we fall back on the view's `object_id`. If a viewmodel is backed by a
default views cannot make assumptions about the identity of our attributes,
Provide a stable way to identify this view through attribute changes. By
def id
  object_id
end

def inherited(subclass)

def inherited(subclass)
  super
  subclass.initialize_as_viewmodel
end

def initialize(*args)

def initialize(*args)
  self.class._attributes.each_with_index do |attr, idx|
    self.public_send(:"#{attr}=", args[idx])
  end
end

def initialize_as_viewmodel

def initialize_as_viewmodel
  @_attributes    = []
  @schema_version = 1
  @view_aliases   = []
end

def is_update_hash?(hash) # rubocop:disable Naming/PredicateName

rubocop:disable Naming/PredicateName
def is_update_hash?(hash) # rubocop:disable Naming/PredicateName
  ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
  hash.has_key?(ViewModel::ID_ATTRIBUTE) &&
    !hash.fetch(ViewModel::ActiveRecord::NEW_ATTRIBUTE, false)
end

def lock_attribute_inheritance

_attributes and ignore children.
subclasses. Redefine `_attributes` to close over the current class's
An abstract viewmodel may want to define attributes to be shared by their
def lock_attribute_inheritance
  _attributes.tap do |attrs|
    define_singleton_method(:_attributes) { attrs }
    attrs.freeze
  end
end

def member_names

def member_names
  _attributes.map(&:to_s)
end

def model

change this, override this method.
if necessary we assume that the wrapped model is the first attribute. To
ViewModels are often used to serialize ActiveRecord models. For convenience,
def model
  self.public_send(self.class._attributes.first)
end

def new_deserialize_context(...)

def new_deserialize_context(...)
  deserialize_context_class.new(...)
end

def new_serialize_context(...)

def new_serialize_context(...)
  serialize_context_class.new(...)
end

def preload_for_serialization(viewmodels, include_referenced: true, lock: nil)

def preload_for_serialization(viewmodels, include_referenced: true, lock: nil)
  Array.wrap(viewmodels).group_by(&:class).each do |type, views|
    DeepPreloader.preload(views.map(&:model),
                          type.eager_includes(include_referenced: include_referenced),
                          lock: lock)
  end
end

def preload_for_serialization(lock: nil)

def preload_for_serialization(lock: nil)
  ViewModel.preload_for_serialization([self], lock: lock)
end

def root!

def root!
  define_singleton_method(:root?) { true }
end

def root?

references.
their parent. Associations to root viewmodel types always use indirect
(de)serialized directly, whereas child viewmodels are always nested within
ViewModels are either roots or children. Root viewmodels may be
def root?
  false
end

def schema_hash(schema_versions)

def schema_hash(schema_versions)
  version_string = schema_versions.to_a.sort.join(',')
  # We want a short hash value, as this will be used in cache keys
  hash = Digest::SHA256.digest(version_string).byteslice(0, 16)
  Base64.urlsafe_encode64(hash, padding: false)
end

def schema_versions(viewmodels)

def schema_versions(viewmodels)
  viewmodels.each_with_object({}) do |view, h|
    h[view.view_name] = view.schema_version
  end
end

def serialize(target, json, serialize_context: new_serialize_context)

relies on Jbuilder#merge! for other values (e.g. primitives).
ViewModel can serialize ViewModels, Arrays and Hashes of ViewModels, and
def serialize(target, json, serialize_context: new_serialize_context)
  case target
  when ViewModel
    target.serialize(json, serialize_context: serialize_context)
  when Array
    json.array! target do |elt|
      serialize(elt, json, serialize_context: serialize_context)
    end
  when Hash, Struct
    json.merge!({})
    target.each_pair do |key, value|
      json.set! key do
        serialize(value, json, serialize_context: serialize_context)
      end
    end
  else
    json.merge! target
  end
end

def serialize(json, serialize_context: self.class.new_serialize_context)

overridden in subclasses to (for example) implement caching.
Serialize this viewmodel to a jBuilder by calling serialize_view. May be
def serialize(json, serialize_context: self.class.new_serialize_context)
  ViewModel::Callbacks.wrap_serialize(self, context: serialize_context) do
    serialize_view(json, serialize_context: serialize_context)
  end
end

def serialize_as_reference(target, json, serialize_context: new_serialize_context)

def serialize_as_reference(target, json, serialize_context: new_serialize_context)
  if serialize_context.flatten_references
    serialize(target, json, serialize_context: serialize_context)
  else
    ref = serialize_context.add_reference(target)
    json.set!(REFERENCE_ATTRIBUTE, ref)
  end
end

def serialize_context_class

def serialize_context_class
  ViewModel::SerializeContext
end

def serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:)

def serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:)
  plural = views.is_a?(Array)
  views = Array.wrap(views)
  json_views, json_refs = ViewModel::ActiveRecord::Cache.render_viewmodels_from_cache(
                views, locked: locked, migration_versions: migration_versions, serialize_context: serialize_context)
  json_views = json_views.first unless plural
  return json_views, json_refs
end

def serialize_to_hash(viewmodel, serialize_context: new_serialize_context)

def serialize_to_hash(viewmodel, serialize_context: new_serialize_context)
  Jbuilder.new { |json| serialize(viewmodel, json, serialize_context: serialize_context) }.attributes!
end

def serialize_view(json, serialize_context: self.class.new_serialize_context)

Default implementation visits each attribute with Viewmodel.serialize.
Render this viewmodel to a jBuilder. Usually overridden in subclasses.
def serialize_view(json, serialize_context: self.class.new_serialize_context)
  self.class._attributes.each do |attr|
    json.set! attr do
      ViewModel.serialize(self.send(attr), json, serialize_context: serialize_context)
    end
  end
end

def stable_id?

this view.
whether the id is included when constructing a ViewModel::Reference from
Is this viewmodel backed by a model with a stable identity? Used to decide
def stable_id?
  false
end

def to_hash(serialize_context: self.class.new_serialize_context)

def to_hash(serialize_context: self.class.new_serialize_context)
  Jbuilder.new { |json| serialize(json, serialize_context: serialize_context) }.attributes!
end

def to_json(serialize_context: self.class.new_serialize_context)

def to_json(serialize_context: self.class.new_serialize_context)
  ViewModel.encode_json(self.to_hash(serialize_context: serialize_context))
end

def to_reference

def to_reference
  ViewModel::Reference.new(self.class, (id if stable_id?))
end

def validate!; end

def validate!; end

def view_name

def view_name
  @view_name ||=
    begin
      # try to auto-detect based on class name
      match = /(.*)View$/.match(self.name)
      raise ArgumentError.new("Could not auto-determine ViewModel name from class name '#{self.name}'") if match.nil?
      ViewModel::Registry.default_view_name(match[1])
    end
end

def view_name

override this to select a specific alias.
Delegate view_name to class in most cases. Polymorphic views may wish to
def view_name
  self.class.view_name
end