class ViewModel

Constants

BULK_UPDATES_ATTRIBUTE
BULK_UPDATE_ATTRIBUTE
BULK_UPDATE_TYPE
Config
ID_ATTRIBUTE
MIGRATED_ATTRIBUTE

Migrations leave a metadata attribute _migrated on any views that they alter. This attribute is accessible as metadata when deserializing migrated input, and is included in the output serialization sent to clients.

Metadata
NEW_ATTRIBUTE
REFERENCE_ATTRIBUTE
TYPE_ATTRIBUTE
VERSION_ATTRIBUTE

Attributes

_attributes[RW]
schema_version[RW]
synthetic[RW]

Boolean to indicate if the viewmodel is synthetic. Synthetic viewmodels are nearly-invisible glue. They're full viewmodels, but do not participate in hooks or registration. For example, a join table connecting A and B through T has a synthetic viewmodel T to represent the join model, but the external interface is a relationship of A to a list of Bs.

view_aliases[R]
view_name[W]

Public Class Methods

accepts_schema_version?(schema_version) click to toggle source
# File lib/view_model.rb, line 265
def accepts_schema_version?(schema_version)
  schema_version == self.schema_version
end
add_view_alias(as) click to toggle source
# File lib/view_model.rb, line 64
def add_view_alias(as)
  view_aliases << as
  ViewModel::Registry.register(self, as: as)
end
attribute(attr, **_args) click to toggle source
# File lib/view_model.rb, line 88
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
attributes(*attrs, **args) click to toggle source

ViewModels are typically going to be pretty simple structures. Make it a bit easier to define them: attributes specified this way are given accessors and assigned in order by the default constructor.

# File lib/view_model.rb, line 84
def attributes(*attrs, **args)
  attrs.each { |attr| attribute(attr, **args) }
end
deserialize_context_class() click to toggle source
# File lib/view_model.rb, line 257
def deserialize_context_class
  ViewModel::DeserializeContext
end
deserialize_from_view(hash_data, references: {}, deserialize_context: new_deserialize_context) click to toggle source

Rebuild this viewmodel from a serialized hash.

# File lib/view_model.rb, line 211
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
deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:) { |hook_control| ... } click to toggle source
# File lib/view_model.rb, line 217
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
eager_includes(include_referenced: true) click to toggle source

If this viewmodel represents an AR model, what associations does it make use of? Returns a includes spec appropriate for DeepPreloader, either as AR-style nested hashes or DeepPreloader::Spec.

# File lib/view_model.rb, line 149
def eager_includes(include_referenced: true)
  {}
end
encode_json(value) click to toggle source
# File lib/view_model.rb, line 199
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
extract_reference_metadata(hash) click to toggle source
# File lib/view_model.rb, line 135
def extract_reference_metadata(hash)
  ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_REFERENCE, hash)
  hash.delete(ViewModel::REFERENCE_ATTRIBUTE)
end
extract_reference_only_metadata(hash) click to toggle source
# File lib/view_model.rb, line 127
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
extract_viewmodel_metadata(hash) click to toggle source

In deserialization, verify and extract metadata from a provided hash.

# File lib/view_model.rb, line 116
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
inherited(subclass) click to toggle source
Calls superclass method
# File lib/view_model.rb, line 42
def inherited(subclass)
  super
  subclass.initialize_as_viewmodel
end
initialize_as_viewmodel() click to toggle source
# File lib/view_model.rb, line 47
def initialize_as_viewmodel
  @_attributes    = []
  @schema_version = 1
  @view_aliases   = []
end
is_update_hash?(hash) click to toggle source
# File lib/view_model.rb, line 140
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
lock_attribute_inheritance() click to toggle source

An abstract viewmodel may want to define attributes to be shared by their subclasses. Redefine `_attributes` to close over the current class's _attributes and ignore children.

# File lib/view_model.rb, line 104
def lock_attribute_inheritance
  _attributes.tap do |attrs|
    define_singleton_method(:_attributes) { attrs }
    attrs.freeze
  end
end
member_names() click to toggle source
# File lib/view_model.rb, line 111
def member_names
  _attributes.map(&:to_s)
end
new(*args) click to toggle source
# File lib/view_model.rb, line 291
def initialize(*args)
  self.class._attributes.each_with_index do |attr, idx|
    self.public_send(:"#{attr}=", args[idx])
  end
end
new_deserialize_context(...) click to toggle source
# File lib/view_model.rb, line 261
def new_deserialize_context(...)
  deserialize_context_class.new(...)
end
new_serialize_context(...) click to toggle source
# File lib/view_model.rb, line 253
def new_serialize_context(...)
  serialize_context_class.new(...)
end
preload_for_serialization(viewmodels, include_referenced: true, lock: nil) click to toggle source
# File lib/view_model.rb, line 282
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
root!() click to toggle source
# File lib/view_model.rb, line 77
def root!
  define_singleton_method(:root?) { true }
end
root?() click to toggle source

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

# File lib/view_model.rb, line 73
def root?
  false
end
schema_hash(schema_versions) click to toggle source
# File lib/view_model.rb, line 275
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
schema_versions(viewmodels) click to toggle source
# File lib/view_model.rb, line 269
def schema_versions(viewmodels)
  viewmodels.each_with_object({}) do |view, h|
    h[view.view_name] = view.schema_version
  end
end
serialize(target, json, serialize_context: new_serialize_context) click to toggle source

ViewModel can serialize ViewModels, Arrays and Hashes of ViewModels, and relies on Jbuilder#merge! for other values (e.g. primitives).

# File lib/view_model.rb, line 155
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
serialize_as_reference(target, json, serialize_context: new_serialize_context) click to toggle source
# File lib/view_model.rb, line 175
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
serialize_context_class() click to toggle source
# File lib/view_model.rb, line 249
def serialize_context_class
  ViewModel::SerializeContext
end
serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:) click to toggle source
# File lib/view_model.rb, line 188
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
serialize_to_hash(viewmodel, serialize_context: new_serialize_context) click to toggle source
# File lib/view_model.rb, line 184
def serialize_to_hash(viewmodel, serialize_context: new_serialize_context)
  Jbuilder.new { |json| serialize(viewmodel, json, serialize_context: serialize_context) }.attributes!
end
view_name() click to toggle source
# File lib/view_model.rb, line 53
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

Public Instance Methods

==(other) click to toggle source
# File lib/view_model.rb, line 373
def ==(other)
  other.class == self.class && self.class._attributes.all? do |attr|
    other.send(attr) == self.send(attr)
  end
end
Also aliased as: eql?
blame_reference() click to toggle source

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

# File lib/view_model.rb, line 361
def blame_reference
  to_reference
end
context_for_child(member_name, context:) click to toggle source
# File lib/view_model.rb, line 365
def context_for_child(member_name, context:)
  context.for_child(self, association_name: member_name)
end
eql?(other)
Alias for: ==
hash() click to toggle source
# File lib/view_model.rb, line 381
def hash
  features = self.class._attributes.map { |attr| self.send(attr) }
  features << self.class
  features.hash
end
id() click to toggle source

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

# File lib/view_model.rb, line 335
def id
  object_id
end
model() click to toggle source

ViewModels are often used to serialize ActiveRecord models. For convenience, if necessary we assume that the wrapped model is the first attribute. To change this, override this method.

# File lib/view_model.rb, line 326
def model
  self.public_send(self.class._attributes.first)
end
preload_for_serialization(lock: nil) click to toggle source
# File lib/view_model.rb, line 369
def preload_for_serialization(lock: nil)
  ViewModel.preload_for_serialization([self], lock: lock)
end
serialize(json, serialize_context: self.class.new_serialize_context) click to toggle source

Serialize this viewmodel to a jBuilder by calling serialize_view. May be overridden in subclasses to (for example) implement caching.

# File lib/view_model.rb, line 299
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
serialize_view(json, serialize_context: self.class.new_serialize_context) click to toggle source

Render this viewmodel to a jBuilder. Usually overridden in subclasses. Default implementation visits each attribute with Viewmodel.serialize.

# File lib/view_model.rb, line 315
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
stable_id?() click to toggle source

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

# File lib/view_model.rb, line 342
def stable_id?
  false
end
to_hash(serialize_context: self.class.new_serialize_context) click to toggle source
# File lib/view_model.rb, line 305
def to_hash(serialize_context: self.class.new_serialize_context)
  Jbuilder.new { |json| serialize(json, serialize_context: serialize_context) }.attributes!
end
to_json(serialize_context: self.class.new_serialize_context) click to toggle source
# File lib/view_model.rb, line 309
def to_json(serialize_context: self.class.new_serialize_context)
  ViewModel.encode_json(self.to_hash(serialize_context: serialize_context))
end
to_reference() click to toggle source
# File lib/view_model.rb, line 348
def to_reference
  ViewModel::Reference.new(self.class, (id if stable_id?))
end
validate!() click to toggle source
# File lib/view_model.rb, line 346
def validate!; end
view_name() click to toggle source

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

# File lib/view_model.rb, line 354
def view_name
  self.class.view_name
end