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
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.
Public Class Methods
# File lib/view_model.rb, line 265 def accepts_schema_version?(schema_version) schema_version == self.schema_version end
# File lib/view_model.rb, line 64 def add_view_alias(as) view_aliases << as ViewModel::Registry.register(self, as: as) end
# 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
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
# File lib/view_model.rb, line 257 def deserialize_context_class ViewModel::DeserializeContext end
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
# 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
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
# 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
# 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
# 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
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
# File lib/view_model.rb, line 42 def inherited(subclass) super subclass.initialize_as_viewmodel end
# File lib/view_model.rb, line 47 def initialize_as_viewmodel @_attributes = [] @schema_version = 1 @view_aliases = [] end
# 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
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
# File lib/view_model.rb, line 111 def member_names _attributes.map(&:to_s) end
# 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
# File lib/view_model.rb, line 261 def new_deserialize_context(...) deserialize_context_class.new(...) end
# File lib/view_model.rb, line 253 def new_serialize_context(...) serialize_context_class.new(...) end
# 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
# File lib/view_model.rb, line 77 def root! define_singleton_method(:root?) { true } end
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
# 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
# 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
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
# 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
# File lib/view_model.rb, line 249 def serialize_context_class ViewModel::SerializeContext end
# 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
# 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
# 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
# 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
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
# File lib/view_model.rb, line 365 def context_for_child(member_name, context:) context.for_child(self, association_name: member_name) end
# File lib/view_model.rb, line 381 def hash features = self.class._attributes.map { |attr| self.send(attr) } features << self.class features.hash end
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
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
# File lib/view_model.rb, line 369 def preload_for_serialization(lock: nil) ViewModel.preload_for_serialization([self], lock: lock) end
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
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
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
# 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
# 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
# File lib/view_model.rb, line 348 def to_reference ViewModel::Reference.new(self.class, (id if stable_id?)) end
# File lib/view_model.rb, line 346 def validate!; end
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