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

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_to_hash, #to_hash, #to_json, #to_reference, #view_name

Constructor Details

#initialize(model) ⇒ Record

Returns a new instance of Record.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/view_model/record.rb', line 173

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.



171
172
173
# File 'lib/view_model/record.rb', line 171

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.



171
172
173
# File 'lib/view_model/record.rb', line 171

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
# 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, 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



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/view_model/record.rb', line 107

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



123
124
125
# File 'lib/view_model/record.rb', line 123

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



140
141
142
# File 'lib/view_model/record.rb', line 140

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.



130
131
132
133
134
135
136
137
138
# File 'lib/view_model/record.rb', line 130

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



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

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
end

Instance Method Details

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



278
279
280
# File 'lib/view_model/record.rb', line 278

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

#attribute_changed!(attr_name) ⇒ Object



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

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

#changed_nested_children?Boolean

Returns:

  • (Boolean)


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

def changed_nested_children?
  @changed_nested_children
end

#changed_referenced_children?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/view_model/record.rb', line 210

def changed_referenced_children?
  @changed_referenced_children
end

#changesObject



254
255
256
257
258
259
260
261
# File 'lib/view_model/record.rb', line 254

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



263
264
265
266
267
268
269
270
# File 'lib/view_model/record.rb', line 263

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.



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

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.



190
191
192
193
194
195
196
# File 'lib/view_model/record.rb', line 190

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

#model_is_new!Object



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

def model_is_new!
  @new_model = true
end

#nested_children_changed!Object



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

def nested_children_changed!
  @changed_nested_children = true
end

#new_model?Boolean

Returns:

  • (Boolean)


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

def new_model?
  @new_model
end

#referenced_children_changed!Object



250
251
252
# File 'lib/view_model/record.rb', line 250

def referenced_children_changed!
  @changed_referenced_children = true
end

#serialize_members(json, serialize_context:) ⇒ Object



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

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



214
215
216
217
218
219
220
# File 'lib/view_model/record.rb', line 214

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)


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

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.



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

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