Class: ViewModel::Record

Inherits:
ViewModel show all
Includes:
MigratableView
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

BULK_UPDATES_ATTRIBUTE, BULK_UPDATE_ATTRIBUTE, BULK_UPDATE_TYPE, ID_ATTRIBUTE, MIGRATED_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?, schema_hash, schema_versions, #serialize, serialize, serialize_as_reference, serialize_context_class, serialize_from_cache, serialize_to_hash, #serialize_to_hash, #to_json, #to_reference, #view_name

Constructor Details

#initialize(model) ⇒ Record

Returns a new instance of Record.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/view_model/record.rb', line 187

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

  super()
end

Class Attribute Details

._membersObject (readonly)

Returns the value of attribute _members.



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

def _members
  @_members
end

.abstract_classObject

Returns the value of attribute abstract_class.



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

def abstract_class
  @abstract_class
end

.unregisteredObject

Returns the value of attribute unregistered.



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

def unregistered
  @unregistered
end

Instance Attribute Details

#changed_attributesObject (readonly)

Returns the value of attribute changed_attributes.



185
186
187
# File 'lib/view_model/record.rb', line 185

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.



185
186
187
# File 'lib/view_model/record.rb', line 185

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



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
79
80
81
# File 'lib/view_model/record.rb', line 42

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



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/view_model/record.rb', line 83

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, .schema_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
rescue ViewModel::DeserializationError => e
  if (new_error = customize_deserialization_error(e))
    raise new_error
  else
    raise
  end
end

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



113
114
115
116
117
118
119
120
121
122
123
# File 'lib/view_model/record.rb', line 113

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



129
130
131
# File 'lib/view_model/record.rb', line 129

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

.inherited(subclass) ⇒ Object



21
22
23
24
25
# File 'lib/view_model/record.rb', line 21

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

.initialize_as_viewmodel_recordObject



27
28
29
30
31
32
33
34
# File 'lib/view_model/record.rb', line 27

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

  @generated_accessor_module = Module.new
  include @generated_accessor_module
end

.member_namesObject



146
147
148
# File 'lib/view_model/record.rb', line 146

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.



136
137
138
139
140
141
142
143
144
# File 'lib/view_model/record.rb', line 136

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



125
126
127
# File 'lib/view_model/record.rb', line 125

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)


37
38
39
# File 'lib/view_model/record.rb', line 37

def should_register?
  !abstract_class && !unregistered && !synthetic
end

Instance Method Details

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



292
293
294
# File 'lib/view_model/record.rb', line 292

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

#attribute_changed!(attr_name) ⇒ Object



256
257
258
# File 'lib/view_model/record.rb', line 256

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

#changed_nested_children?Boolean

Returns:

  • (Boolean)


220
221
222
# File 'lib/view_model/record.rb', line 220

def changed_nested_children?
  @changed_nested_children
end

#changed_referenced_children?Boolean

Returns:

  • (Boolean)


224
225
226
# File 'lib/view_model/record.rb', line 224

def changed_referenced_children?
  @changed_referenced_children
end

#changesObject



268
269
270
271
272
273
274
275
# File 'lib/view_model/record.rb', line 268

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



277
278
279
280
281
282
283
284
# File 'lib/view_model/record.rb', line 277

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.



288
289
290
# File 'lib/view_model/record.rb', line 288

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.



204
205
206
207
208
209
210
# File 'lib/view_model/record.rb', line 204

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

#model_is_new!Object



252
253
254
# File 'lib/view_model/record.rb', line 252

def model_is_new!
  @new_model = true
end

#nested_children_changed!Object



260
261
262
# File 'lib/view_model/record.rb', line 260

def nested_children_changed!
  @changed_nested_children = true
end

#new_model?Boolean

Returns:

  • (Boolean)


216
217
218
# File 'lib/view_model/record.rb', line 216

def new_model?
  @new_model
end

#referenced_children_changed!Object



264
265
266
# File 'lib/view_model/record.rb', line 264

def referenced_children_changed!
  @changed_referenced_children = true
end

#serialize_members(json, serialize_context:) ⇒ Object



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

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



228
229
230
231
232
233
234
# File 'lib/view_model/record.rb', line 228

def serialize_view(json, serialize_context: self.class.new_serialize_context)
  json.set!(ViewModel::ID_ATTRIBUTE, self.id) if stable_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)


212
213
214
# File 'lib/view_model/record.rb', line 212

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.



246
247
248
249
250
# File 'lib/view_model/record.rb', line 246

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