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

Inherits:
Common::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 =
Common::Content::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: false) ⇒ DoubleModel

Returns a new instance of DoubleModel.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/ecoportal/api/common/content/double_model.rb', line 271

def initialize(doc = {}, parent: self, key: nil, read_only: false)
  @_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

  if key_method? && doc && doc.is_a?(Hash)
    self.key = doc[key_method]
    #puts "\n$(#{self.key}<=>#{self.class})"
  end
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)

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



269
270
271
# File 'lib/ecoportal/api/common/content/double_model.rb', line 269

def _key
  @_key
end

#_parentObject (readonly)

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



269
270
271
# File 'lib/ecoportal/api/common/content/double_model.rb', line 269

def _parent
  @_parent
end

#_read_onlyObject (readonly)

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



269
270
271
# File 'lib/ecoportal/api/common/content/double_model.rb', line 269

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: false) ⇒ 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: false)

    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)


204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/ecoportal/api/common/content/double_model.rb', line 204

def embeds_many(method, key: method, klass: nil, enum_class: nil,
  order_matters: false, order_key: nil, read_only: false)
  if enum_class
    eclass = enum_class
  elsif klass
    eclass = new_class("#{method}::#{klass}", inherits: Common::Content::CollectionModel) do |dim_class|
      dim_class.klass         = klass
      dim_class.order_matters = order_matters
      dim_class.order_key     = order_key
    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
  end
end

.embeds_one(method, key: method, nullable: false, klass:) ⇒ 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



190
191
192
# File 'lib/ecoportal/api/common/content/double_model.rb', line 190

def embeds_one(method, key: method, nullable: false, klass:)
  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



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

def enforce!(doc)
  return unless doc && 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.



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ecoportal/api/common/content/double_model.rb', line 45

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.



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/ecoportal/api/common/content/double_model.rb', line 62

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



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/ecoportal/api/common/content/double_model.rb', line 167

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: Common::Content::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: self._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?



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

def passboolean(*methods, read_only: false)
  pass_reader(*methods) {|value| value}
  unless read_only
    pass_writer(*methods) {|value| !!value}
  end
  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


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

def passdate(*methods, read_only: false)
  pass_reader(*methods) {|value| to_time(value)}
  unless read_only
    pass_writer(*methods) {|value| to_time(value)&.iso8601}
  end
  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



111
112
113
114
# File 'lib/ecoportal/api/common/content/double_model.rb', line 111

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.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ecoportal/api/common/content/double_model.rb', line 82

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?



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

def passthrough(*methods, read_only: false)
  pass_reader *methods
  pass_writer *methods unless read_only
  self
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



310
311
312
313
314
315
316
317
318
# File 'lib/ecoportal/api/common/content/double_model.rb', line 310

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



344
345
346
# File 'lib/ecoportal/api/common/content/double_model.rb', line 344

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



354
355
356
357
# File 'lib/ecoportal/api/common/content/double_model.rb', line 354

def as_update
  new_doc = as_json
  Common::Content::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



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

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



360
361
362
363
# File 'lib/ecoportal/api/common/content/double_model.rb', line 360

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:



321
322
323
324
325
326
327
328
329
330
331
# File 'lib/ecoportal/api/common/content/double_model.rb', line 321

def doc
  return @doc if doc_var?
  raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
  if is_root?
    @doc
  else
    # transform parent's `_key` to this object into a
    # path key that can rerieve from the parents's doc
    _parent.doc.dig(*[_doc_key(_key)].flatten)
  end
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)



296
297
298
299
# File 'lib/ecoportal/api/common/content/double_model.rb', line 296

def key
  raise NoKeyMethod.new "No key_method defined for #{self.class}" unless key_method?
  self.method(key_method).call
end

#key=(value) ⇒ Object

Parameters:

  • the (String)

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



302
303
304
305
306
# File 'lib/ecoportal/api/common/content/double_model.rb', line 302

def key=(value)
  raise NoKeyMethod.new "No key_method defined for #{self.class}" unless key_method?
  method = "#{key_method}="
  self.method(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:



335
336
337
338
339
340
341
342
# File 'lib/ecoportal/api/common/content/double_model.rb', line 335

def original_doc
  raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
  if is_root?
    @original_doc
  else
    _parent.original_doc.dig(*[_doc_key(_key)].flatten)
  end
end


389
390
391
392
# File 'lib/ecoportal/api/common/content/double_model.rb', line 389

def print_pretty
  puts JSON.pretty_generate(as_json)
  self
end

#replace_doc(new_doc) ⇒ Object

Raises:



394
395
396
397
398
399
400
401
402
403
# File 'lib/ecoportal/api/common/content/double_model.rb', line 394

def replace_doc(new_doc)
  raise UnlinkedModel.new(from: "#{self.class}#replace_doc", key: _key) unless linked?
  if is_root?
    @doc = new_doc
  else
    dig_set(_parent.doc, [_doc_key(_key)].flatten, new_doc)
    _parent.variable_remove!(_key) unless new_doc
    #variables_remove!
  end
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



378
379
380
381
382
383
384
385
386
387
# File 'lib/ecoportal/api/common/content/double_model.rb', line 378

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

#rootObject



290
291
292
293
# File 'lib/ecoportal/api/common/content/double_model.rb', line 290

def root
  return self if is_root?
  _parent.root
end

#to_json(*args) ⇒ Object



348
349
350
# File 'lib/ecoportal/api/common/content/double_model.rb', line 348

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