Class: ViewModel::Record

Inherits:
ViewModel show all
Defined in:
lib/view_model/record.rb

Overview

Abstract ViewModel type for serializing a subset of attributes from a record. A record viewmodel wraps a single underlying model, exposing a fixed set of real or calculated attributes.

Direct Known Subclasses

ActiveRecord, ErrorView

Defined Under Namespace

Classes: AttributeData

Constant Summary

Constants inherited from ViewModel

ID_ATTRIBUTE, NEW_ATTRIBUTE, REFERENCE_ATTRIBUTE, TYPE_ATTRIBUTE, VERSION_ATTRIBUTE

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ViewModel

accepts_schema_version?, add_view_alias, attributes, #blame_reference, #context_for_child, deserialize_context_class, eager_includes, encode_json, extract_reference_metadata, extract_reference_only_metadata, extract_viewmodel_metadata, initialize_as_viewmodel, is_update_hash?, lock_attribute_inheritance, new_deserialize_context, new_serialize_context, preload_for_serialization, #preload_for_serialization, root!, root?, serialize, #serialize, serialize_as_reference, serialize_context_class, serialize_to_hash, #to_hash, #to_json, #to_reference, #view_name

Constructor Details

#initialize(model) ⇒ Record

Returns a new instance of Record.



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/view_model/record.rb', line 170

def initialize(model)
  unless model.is_a?(model_class)
    raise ArgumentError.new("'#{model.inspect}' is not an instance of #{model_class.name}")
  end

  self.model = model

  @new_model                     = false
  @changed_attributes            = []
  @changed_nested_children       = false
  @changed_referenced_children   = false
end

Class Attribute Details

._membersObject (readonly)

Returns the value of attribute _members.



15
16
17
# File 'lib/view_model/record.rb', line 15

def _members
  @_members
end

.abstract_classObject

Returns the value of attribute abstract_class.



16
17
18
# File 'lib/view_model/record.rb', line 16

def abstract_class
  @abstract_class
end

.unregisteredObject

Returns the value of attribute unregistered.



16
17
18
# File 'lib/view_model/record.rb', line 16

def unregistered
  @unregistered
end

Instance Attribute Details

#changed_attributesObject (readonly)

Returns the value of attribute changed_attributes.



168
169
170
# File 'lib/view_model/record.rb', line 168

def changed_attributes
  @changed_attributes
end

#modelObject

All ViewModel::Records have the same underlying ViewModel attribute: the record model they back on to. We want this to be inherited by subclasses, so we override ViewModel’s :_attributes to close over it.



10
11
12
# File 'lib/view_model/record.rb', line 10

def model
  @model
end

#previous_changesObject (readonly)

Returns the value of attribute previous_changes.



168
169
170
# File 'lib/view_model/record.rb', line 168

def previous_changes
  @previous_changes
end

Class Method Details

.attribute(attr, as: nil, read_only: false, write_once: false, using: nil, format: nil, array: false) ⇒ Object

Specifies an attribute from the model to be serialized in this view



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/view_model/record.rb', line 39

def attribute(attr, as: nil, read_only: false, write_once: false, using: nil, format: nil, array: false)
  model_attribute_name = attr.to_s
  vm_attribute_name    = (as || attr).to_s

  if using && format
    raise ArgumentError.new("Only one of ':using' and ':format' may be specified")
  end
  if using && !(using.is_a?(Class) && using < ViewModel)
    raise ArgumentError.new("Invalid 'using:' viewmodel: not a viewmodel class")
  end
  if using && using.root?
    raise ArgumentError.new("Invalid 'using:' viewmodel: is a root")
  end
  if format && !format.respond_to?(:dump) && !format.respond_to?(:load)
    raise ArgumentError.new("Invalid 'format:' serializer: must respond to :dump and :load")
  end

  attr_data = AttributeData.new(name: vm_attribute_name,
                                model_attr_name: model_attribute_name,
                                attribute_viewmodel: using,
                                attribute_serializer: format,
                                array: array,
                                read_only: read_only,
                                write_once: write_once)
  _members[vm_attribute_name] = attr_data

  @generated_accessor_module.module_eval do
    define_method vm_attribute_name do
      _get_attribute(attr_data)
    end

    define_method "serialize_#{vm_attribute_name}" do |json, serialize_context: self.class.new_serialize_context|
      _serialize_attribute(attr_data, json, serialize_context: serialize_context)
    end

    define_method "deserialize_#{vm_attribute_name}" do |value, references: {}, deserialize_context: self.class.new_deserialize_context|
      _deserialize_attribute(attr_data, value, references: references, deserialize_context: deserialize_context)
    end
  end
end

.deserialize_from_view(view_hashes, references: {}, deserialize_context: new_deserialize_context) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/view_model/record.rb', line 80

def deserialize_from_view(view_hashes, references: {}, deserialize_context: new_deserialize_context)
  ViewModel::Utils.map_one_or_many(view_hashes) do |view_hash|
    view_hash = view_hash.dup
     = ViewModel.(view_hash)

    unless self.view_name == .view_name || self.view_aliases.include?(.view_name)
      raise ViewModel::DeserializationError::InvalidViewType.new(
              self.view_name,
              ViewModel::Reference.new(self, .id))
    end

    if .schema_version && !self.accepts_schema_version?(.schema_version)
      raise ViewModel::DeserializationError::SchemaVersionMismatch.new(
              self, version, ViewModel::Reference.new(self, .id))
    end

    viewmodel = resolve_viewmodel(, view_hash, deserialize_context: deserialize_context)

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

    viewmodel
  end
end

.deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/view_model/record.rb', line 104

def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
  super do |hook_control|
    final_changes = viewmodel.clear_changes!

    if final_changes.changed?
      deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, viewmodel, changes: final_changes)
    end

    hook_control.record_changes(final_changes)
  end
end

.for_new_model(*model_args) ⇒ Object



120
121
122
# File 'lib/view_model/record.rb', line 120

def for_new_model(*model_args)
  self.new(model_class.new(*model_args)).tap { |v| v.model_is_new! }
end

.inherited(subclass) ⇒ Object



18
19
20
21
22
# File 'lib/view_model/record.rb', line 18

def inherited(subclass)
  super
  subclass.initialize_as_viewmodel_record
  ViewModel::Registry.register(subclass)
end

.initialize_as_viewmodel_recordObject



24
25
26
27
28
29
30
31
# File 'lib/view_model/record.rb', line 24

def initialize_as_viewmodel_record
  @_members       = {}
  @abstract_class = false
  @unregistered   = false

  @generated_accessor_module = Module.new
  include @generated_accessor_module
end

.member_namesObject



137
138
139
# File 'lib/view_model/record.rb', line 137

def member_names
  self._members.keys
end

.model_classObject

Returns the AR model class wrapped by this viewmodel. If this has not been set via ‘model_class_name=`, attempt to automatically resolve based on the name of this viewmodel.



127
128
129
130
131
132
133
134
135
# File 'lib/view_model/record.rb', line 127

def model_class
  unless instance_variable_defined?(:@model_class)
    # try to auto-detect the model class based on our name
    self.model_class_name =
      ViewModel::Registry.infer_model_class_name(self.view_name)
  end

  @model_class
end

.resolve_viewmodel(metadata, view_hash, deserialize_context:) ⇒ Object



116
117
118
# File 'lib/view_model/record.rb', line 116

def resolve_viewmodel(, view_hash, deserialize_context:)
  self.for_new_model
end

.should_register?Boolean

Should this class be registered in the viewmodel registry

Returns:

  • (Boolean)


34
35
36
# File 'lib/view_model/record.rb', line 34

def should_register?
  !abstract_class && !unregistered
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



273
274
275
# File 'lib/view_model/record.rb', line 273

def ==(other)
  self.class == other.class && self.model == other.model
end

#attribute_changed!(attr_name) ⇒ Object



237
238
239
# File 'lib/view_model/record.rb', line 237

def attribute_changed!(attr_name)
  @changed_attributes << attr_name.to_s
end

#changed_nested_children?Boolean

Returns:

  • (Boolean)


201
202
203
# File 'lib/view_model/record.rb', line 201

def changed_nested_children?
  @changed_nested_children
end

#changed_referenced_children?Boolean

Returns:

  • (Boolean)


205
206
207
# File 'lib/view_model/record.rb', line 205

def changed_referenced_children?
  @changed_referenced_children
end

#changesObject



249
250
251
252
253
254
255
256
# File 'lib/view_model/record.rb', line 249

def changes
  ViewModel::Changes.new(
    new:                         new_model?,
    changed_attributes:          changed_attributes,
    changed_nested_children:     changed_nested_children?,
    changed_referenced_children: changed_referenced_children?,
  )
end

#clear_changes!Object



258
259
260
261
262
263
264
265
# File 'lib/view_model/record.rb', line 258

def clear_changes!
  @previous_changes           = changes
  @new_model                  = false
  @changed_attributes         = []
  @changed_nested_children    = false
  @changed_referenced_children = false
  previous_changes
end

#hashObject

Use ActiveRecord style identity for viewmodels. This allows serialization to generate a references section by keying on the viewmodel itself.



269
270
271
# File 'lib/view_model/record.rb', line 269

def hash
  [self.class, self.model].hash
end

#idObject

VM::Record identity matches the identity of its model. If the model has a stable identity, use it, otherwise fall back to its object_id.



185
186
187
188
189
190
191
# File 'lib/view_model/record.rb', line 185

def id
  if stable_id?
    model.id
  else
    model.object_id
  end
end

#model_is_new!Object



233
234
235
# File 'lib/view_model/record.rb', line 233

def model_is_new!
  @new_model = true
end

#nested_children_changed!Object



241
242
243
# File 'lib/view_model/record.rb', line 241

def nested_children_changed!
  @changed_nested_children = true
end

#new_model?Boolean

Returns:

  • (Boolean)


197
198
199
# File 'lib/view_model/record.rb', line 197

def new_model?
  @new_model
end

#referenced_children_changed!Object



245
246
247
# File 'lib/view_model/record.rb', line 245

def referenced_children_changed!
  @changed_referenced_children = true
end

#serialize_members(json, serialize_context:) ⇒ Object



217
218
219
220
221
# File 'lib/view_model/record.rb', line 217

def serialize_members(json, serialize_context:)
  self.class._members.each do |member_name, _member_data|
    self.public_send("serialize_#{member_name}", json, serialize_context: serialize_context)
  end
end

#serialize_view(json, serialize_context: self.class.new_serialize_context) ⇒ Object



209
210
211
212
213
214
215
# File 'lib/view_model/record.rb', line 209

def serialize_view(json, serialize_context: self.class.new_serialize_context)
  json.set!(ViewModel::ID_ATTRIBUTE, model.id) if model.respond_to?(:id)
  json.set!(ViewModel::TYPE_ATTRIBUTE, self.view_name)
  json.set!(ViewModel::VERSION_ATTRIBUTE, self.class.schema_version)

  serialize_members(json, serialize_context: serialize_context)
end

#stable_id?Boolean

Returns:

  • (Boolean)


193
194
195
# File 'lib/view_model/record.rb', line 193

def stable_id?
  model.respond_to?(:id)
end

#validate!Object

Check that the model backing this view is consistent, for example by calling AR validations. Default implementation handles ActiveModel::Validations, may be overridden by subclasses for other types of validation. Must raise DeserializationError::Validation if invalid.



227
228
229
230
231
# File 'lib/view_model/record.rb', line 227

def validate!
  if model_class < ActiveModel::Validations && !model.valid?
    raise ViewModel::DeserializationError::Validation.from_active_model(model.errors, self.blame_reference)
  end
end