Class: Ecoportal::API::Common::Content::DoubleModel

Inherits:
BaseModel
  • Object
show all
Extended by:
ClassHelpers
Includes:
ModelHelpers
Defined in:
lib/ecoportal/api/common/content/double_model.rb

Overview

Basic model class, to build get / set methods for a given property which differs of attr_* ruby native class methods because pass* completelly links the methods to a subjacent Hash model

Defined Under Namespace

Classes: NoKeyMethod, UnlinkedModel

Constant Summary collapse

NOT_USED =
ClassHelpers::NOT_USED

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ClassHelpers

inheritable_attrs, inheritable_class_vars, inherited, new_class, resolve_class, to_constant, to_time, uid

Constructor Details

#initialize(doc = {}, parent: self, key: nil, read_only: self.class.read_only?) ⇒ DoubleModel

rubocop:disable Lint/MissingSuper



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/ecoportal/api/common/content/double_model.rb', line 288

def initialize(doc = {}, parent: self, key: nil, read_only: self.class.read_only?) # rubocop:disable Lint/MissingSuper
  @_dim_vars  = []
  @_parent    = parent || self
  @_key       = key    || self
  @_read_only = read_only

  self.class.enforce!(doc)

  if (_parent == self) || read_only
    @doc          = JSON.parse(doc.to_json)
    @original_doc = JSON.parse(@doc.to_json)
  end

  return unless key_method? && doc && doc.is_a?(Hash)

  self.key = doc[key_method]
  #puts "\n$(#{self.key}<=>#{self.class})"
end

Class Attribute Details

.keyObject

Returns the value of attribute key.



25
26
27
# File 'lib/ecoportal/api/common/content/double_model.rb', line 25

def key
  @key
end

Instance Attribute Details

#_keyObject (readonly)

Note:

while key refers to the value of theproperty of this model that is key (identifies an item in a set of elements)

_key refers to the _parent's property that links to this model



286
287
288
# File 'lib/ecoportal/api/common/content/double_model.rb', line 286

def _key
  @_key
end

#_parentObject (readonly)

Note:

while key refers to the value of theproperty of this model that is key (identifies an item in a set of elements)

_key refers to the _parent's property that links to this model



286
287
288
# File 'lib/ecoportal/api/common/content/double_model.rb', line 286

def _parent
  @_parent
end

#_read_onlyObject (readonly)

Note:

while key refers to the value of theproperty of this model that is key (identifies an item in a set of elements)

_key refers to the _parent's property that links to this model



286
287
288
# File 'lib/ecoportal/api/common/content/double_model.rb', line 286

def _read_only
  @_read_only
end

Class Method Details

.embeds_many(method, key: method, klass: nil, enum_class: nil, order_matters: false, order_key: nil, read_only: read_only?) ) ⇒ Object

Note:
  • if you have a dedicated Enumerable class to manage many, you should use :enum_class
  • otherwise, just indicate the child class in :klass and it will auto generate the class

Parameters:

  • method (Symbol)

    the method that exposes the embeded object

  • key (Symbol) (defaults to: method)

    the key that embeds it to the underlying Hash model

  • order_matters (Boolean) (defaults to: false)

    to state if the order will matter

  • klass (Class, String) (defaults to: nil)

    the class of the individual elements it embeds

  • enum_class (Class, String) (defaults to: nil)

    the class of the collection that will hold the individual elements

  • read_only (Boolean) (defaults to: read_only?) )

    whether or not should try to work around items klass missing a key

    • If set to true this is meant only for read purposes (won't be able to successufully insert)


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/ecoportal/api/common/content/double_model.rb', line 211

def embeds_many(method, key: method, klass: nil, enum_class: nil,
  order_matters: false, order_key: nil, read_only: read_only?)
  if enum_class
    eclass = enum_class
  elsif klass
    eclass = new_class("#{method}::#{klass}", inherits: CollectionModel) do |dim_class|
      # NOTE: new_class may resolve the namespace of the class to an already existing class
      dim_class.klass       ||= klass
      dim_class.order_matters = order_matters
      dim_class.order_key     = order_key
      dim_class.read_only! if read_only
    end
  else
    raise "You should either specify the 'klass' of the elements or the 'enum_class'"
  end

  embed(
    method,              key:   key,
    multiple:  true,     klass: eclass,
    read_only: read_only
  ) do |instance_with_called_method|
    # keep reference to the original class to resolve the `klass` dependency
    # See stackoverflow: https://stackoverflow.com/a/73709529/4352306
    referrer_class = instance_with_called_method.class
    eclass.klass   = {referrer_class => klass} if klass
    # This helps `resolve_class` to correctly resolve a symbol
    # by using referrer_class as a base module to resolve it
  end
end

.embeds_one(method, klass:, key: method, nullable: false) ⇒ Object

Helper to embed one nested object under one property

Parameters:

  • method (Symbol)

    the method that exposes the embeded object

  • key (Symbol) (defaults to: method)

    the key that embeds it to the underlying Hash model

  • klass (Class, String)

    the class of the embedded object



197
198
199
# File 'lib/ecoportal/api/common/content/double_model.rb', line 197

def embeds_one(method, klass:, key: method, nullable: false)
  embed(method, key: key, nullable: nullable, multiple: false, klass: klass)
end

.enforce!(doc) ⇒ Object

Ensures doc has the model_forced_keys. If it doesn't, it adds those missing with the defined default values



128
129
130
131
132
133
134
135
136
# File 'lib/ecoportal/api/common/content/double_model.rb', line 128

def enforce!(doc)
  return unless doc.is_a?(Hash)
  return if     model_forced_keys.empty?

  model_forced_keys.each do |key, default|
    doc[key] = default unless doc.key?(key)
  end
  doc
end

.key?Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/ecoportal/api/common/content/double_model.rb', line 27

def key?
  !!key
end

.new_uuid(length: 24) ⇒ Object



37
38
39
# File 'lib/ecoportal/api/common/content/double_model.rb', line 37

def new_uuid(length: 24)
  uid(length)
end

.pass_reader(*methods) ⇒ Object

Note:

it does not create an instance variable

Same as attr_reader but links to a subjacent Hash model property

Parameters:

  • methods (Array<Symbol>)

    the method that exposes the value as well as its key in the underlying Hash model.



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ecoportal/api/common/content/double_model.rb', line 55

def pass_reader(*methods)
  methods.each do |method|
    method = method.to_s.freeze

    define_method method do
      value = send(:doc)[method]
      value = yield(value) if block_given?
      value
    end
  end
  self
end

.pass_writer(*methods) ⇒ Object

Note:

it does not create an instance variable

Same as attr_writer but links to a subjacent Hash model property

Parameters:

  • methods (Array<Symbol>)

    the method that exposes the value as well as its key in the underlying Hash model.



72
73
74
75
76
77
78
79
80
81
82
# File 'lib/ecoportal/api/common/content/double_model.rb', line 72

def pass_writer(*methods)
  methods.each do |method|
    method = method.to_s.freeze

    define_method "#{method}=" do |value|
      value = yield(value) if block_given?
      send(:doc)[method] = value
    end
  end
  self
end

.passarray(*methods, order_matters: true, uniq: true) ⇒ Object

To link as plain Array to a subjacent Hash model property

Parameters:

  • methods (Array<Symbol>)

    the method that exposes the value as well as its key in the underlying Hash model.

  • order_matters (Boolean) (defaults to: true)

    does the order matter

  • uniq (Boolean) (defaults to: true)

    should it contain unique elements



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ecoportal/api/common/content/double_model.rb', line 174

def passarray(*methods, order_matters: true, uniq: true)
  methods.each do |method|
    method = method.to_s.freeze
    var    = instance_variable_name(method)

    dim_class = new_class(method, inherits: ArrayModel) do |klass|
      klass.order_matters = order_matters
      klass.uniq          = uniq
    end

    define_method method do
      return instance_variable_get(var) if instance_variable_defined?(var)
      new_obj = dim_class.new(parent: self, key: method, read_only: read_only?)
      variable_set(var, new_obj)
    end
  end
end

.passboolean(*methods, read_only: false) ⇒ Object

To link as a Boolean to a subjacent Hash model property

Parameters:

  • methods (Array<Symbol>)

    the method that exposes the value as well as its key in the underlying Hash model.

  • read_only (Boolean) (defaults to: false)

    should it only define the reader?



163
164
165
166
167
# File 'lib/ecoportal/api/common/content/double_model.rb', line 163

def passboolean(*methods, read_only: false)
  pass_reader(*methods) {|value| value}
  pass_writer(*methods) {|value| !!value} unless read_only
  self
end

.passdate(*methods, read_only: false) ⇒ Object

To link as a Time date to a subjacent Hash model property

Parameters:

  • methods (Array<Symbol>)

    the method that exposes the value as well as its key in the underlying Hash model.

  • read_only (Boolean) (defaults to: false)

    should it only define the reader?

See Also:

  • #passthrough


153
154
155
156
157
# File 'lib/ecoportal/api/common/content/double_model.rb', line 153

def passdate(*methods, read_only: false)
  pass_reader(*methods) {|value| to_time(value)}
  pass_writer(*methods) {|value| to_time(value)&.iso8601} unless read_only
  self
end

.passforced(method, default:, read_only: false) ⇒ Object

Note:
  • DoubleModel can be used with objects that do not use patch_ver
  • This ensures that does that do, will get the correct patch update model

These are methods that should always be present in patch update

Parameters:

  • method (Symbol)

    the method that exposes the value as well as its key in the underlying Hash model.

  • default (Value)

    the default value that this key will be written in the model when it doesn't exixt



121
122
123
124
# File 'lib/ecoportal/api/common/content/double_model.rb', line 121

def passforced(method, default:, read_only: false)
  model_forced_keys[method.to_s.freeze] = default
  passthrough(method, read_only: read_only)
end

.passkey(method) ⇒ Object

Note:

Content::CollectionModel needs to find elements in the doc Array. The only way to do it is via the access key (i.e. id). However, there is no chance you can avoid infinite loop for get_key without setting an instance variable key at the moment of the object creation, when the doc is firstly received

This method is essential to give stability to the model

Parameters:

  • method (Symbol)

    the method that exposes the value as well as its key in the underlying Hash model.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ecoportal/api/common/content/double_model.rb', line 92

def passkey(method)
  method   = method.to_s.freeze
  var      = instance_variable_name(method)
  self.key = method

  define_method method do
    return instance_variable_get(var) if instance_variable_defined?(var)
    value = send(:doc)[method]
    value = yield(value) if block_given?
    value
  end

  define_method "#{method}=" do |value|
    variable_set(var, value)
    value = yield(value) if block_given?
    send(:doc)[method] = value
  end

  self
end

.passthrough(*methods, read_only: false) ⇒ Object

Same as attr_accessor but links to a subjacent Hash model property

Parameters:

  • methods (Array<Symbol>)

    the method that exposes the value as well as its key in the underlying Hash model.

  • read_only (Boolean) (defaults to: false)

    should it only define the reader?



142
143
144
145
146
# File 'lib/ecoportal/api/common/content/double_model.rb', line 142

def passthrough(*methods, read_only: false)
  pass_reader(*methods)
  pass_writer(*methods) unless read_only
  self
end

.read_only!Object

Be able to define if a class should be read-only



47
48
49
# File 'lib/ecoportal/api/common/content/double_model.rb', line 47

def read_only!
  @read_only = true
end

.read_only?Boolean

Returns:

  • (Boolean)


41
42
43
44
# File 'lib/ecoportal/api/common/content/double_model.rb', line 41

def read_only?
  @read_only = false if @read_only.nil?
  @read_only
end

Instance Method Details

#_doc_key(value) ⇒ Object

Offers a method for child classes to transform the key, provided that the child's doc can be accessed



338
339
340
341
342
343
344
345
346
# File 'lib/ecoportal/api/common/content/double_model.rb', line 338

def _doc_key(value)
  if value.is_a?(Content::DoubleModel) && !value.is_root?
    #print "?(#{value.class}<=#{value._parent.class})"
    value._parent._doc_key(value)
  else
    #print "!(#{value}<=#{self.class})"
    value
  end
end

#as_jsonObject



369
370
371
# File 'lib/ecoportal/api/common/content/double_model.rb', line 369

def as_json
  doc
end

#as_updatenil, Hash

Returns the patch Hash model including only the changes between original_doc and doc.

Returns:

  • (nil, Hash)

    the patch Hash model including only the changes between original_doc and doc



379
380
381
382
# File 'lib/ecoportal/api/common/content/double_model.rb', line 379

def as_update
  new_doc = as_json
  HashDiffPatch.patch_diff(new_doc, original_doc)
end

#consolidate!Object

Note:
  • after executing it, there will be no pending changes
  • you should technically run this command, after a successful update request to the server

It makes original_doc to be like doc



394
395
396
# File 'lib/ecoportal/api/common/content/double_model.rb', line 394

def consolidate!
  replace_original_doc(JSON.parse(doc.to_json))
end

#dirty?Boolean

Returns stating if there are changes.

Returns:

  • (Boolean)

    stating if there are changes



385
386
387
388
# File 'lib/ecoportal/api/common/content/double_model.rb', line 385

def dirty?
  au = as_update
  !((au == {}) || au.nil?)
end

#docnil, Hash

Returns the underlying Hash model as is (carrying current changes).

Returns:

  • (nil, Hash)

    the underlying Hash model as is (carrying current changes)

Raises:



349
350
351
352
353
354
355
356
357
358
# File 'lib/ecoportal/api/common/content/double_model.rb', line 349

def doc
  return @doc if doc_var?

  raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
  return @doc if is_root?

  # transform parent's `_key` to this object into a
  # path key that can rerieve from the parents's doc
  _parent.doc.dig(*resolved_doc_key.flatten)
end

#keyString

Returns the value of the key method (i.e. id value).

Returns:

  • (String)

    the value of the key method (i.e. id value)

Raises:



319
320
321
322
323
# File 'lib/ecoportal/api/common/content/double_model.rb', line 319

def key
  raise NoKeyMethod, "No key_method defined for #{self.class}" unless key_method?

  method(key_method).call
end

#key=(value) ⇒ Object

Parameters:

  • the (String)

    value of the key method (i.e. id value)

Raises:



326
327
328
329
330
# File 'lib/ecoportal/api/common/content/double_model.rb', line 326

def key=(value)
  raise NoKeyMethod, "No key_method defined for #{self.class}" unless key_method?

  method("#{key_method}=").call(value)
end

#original_docnil, Hash

The original_doc holds the model as is now on server-side.

Returns:

  • (nil, Hash)

    the underlying Hash model as after last consolidate! changes

Raises:



362
363
364
365
366
367
# File 'lib/ecoportal/api/common/content/double_model.rb', line 362

def original_doc
  raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
  return @original_doc if is_root?

  _parent.original_doc.dig(*resolved_doc_key.flatten)
end


414
415
416
417
# File 'lib/ecoportal/api/common/content/double_model.rb', line 414

def print_pretty
  puts JSON.pretty_generate(as_json)
  self
end

#read_only?Boolean

Note:

read_only allows for some optimizations, such as storing values in instance variables, for optimization purposes

Returns:

  • (Boolean)


309
310
311
# File 'lib/ecoportal/api/common/content/double_model.rb', line 309

def read_only?
  @_read_only
end

#replace_doc(new_doc) ⇒ Object

Raises:



419
420
421
422
423
424
425
426
# File 'lib/ecoportal/api/common/content/double_model.rb', line 419

def replace_doc(new_doc)
  raise UnlinkedModel.new(from: "#{self.class}#replace_doc", key: _key) unless linked?
  return (@doc = new_doc) if is_root?

  dig_set(_parent.doc, resolved_doc_key.flatten, new_doc)
  _parent.variable_remove!(_key) unless new_doc
  #variables_remove!
end

#reset!(key = nil) ⇒ Object

Note:
  • after executing it, changes in doc will be lost
  • you should technically run this command only if you want to remove certain changes

It makes doc to be like original



403
404
405
406
407
408
409
410
411
412
# File 'lib/ecoportal/api/common/content/double_model.rb', line 403

def reset!(key = nil)
  if key
    keys    = [key].flatten.compact
    odoc    = original_doc.dig(*keys)
    odoc  &&= JSON.parse(odoc.to_json)
    dig_set(doc, keys, odoc)
  else
    replace_doc(JSON.parse(original_doc.to_json))
  end
end

#resolved_doc_keyObject



332
333
334
# File 'lib/ecoportal/api/common/content/double_model.rb', line 332

def resolved_doc_key
  [_doc_key(_key)]
end

#rootObject



313
314
315
316
# File 'lib/ecoportal/api/common/content/double_model.rb', line 313

def root
  return self if is_root?
  _parent.root
end

#to_json(*args) ⇒ Object



373
374
375
# File 'lib/ecoportal/api/common/content/double_model.rb', line 373

def to_json(*args)
  doc.to_json(*args)
end