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

Returns a new instance of DoubleModel.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/ecoportal/api/common/content/double_model.rb', line 250

def initialize(doc = {}, parent: self, key: nil)
  @_dim_vars = []
  @_parent   = parent || self
  @_key      = key    || self

  self.class.enforce!(doc)

  if _parent == self
    @doc          = doc
    @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.



22
23
24
# File 'lib/ecoportal/api/common/content/double_model.rb', line 22

def key
  @key
end

Instance Attribute Details

#_keyObject (readonly)

Returns the value of attribute _key.



248
249
250
# File 'lib/ecoportal/api/common/content/double_model.rb', line 248

def _key
  @_key
end

#_parentObject (readonly)

Returns the value of attribute _parent.



248
249
250
# File 'lib/ecoportal/api/common/content/double_model.rb', line 248

def _parent
  @_parent
end

Class Method Details

.embeds_many(method, key: method, order_matters: false, order_key: nil, klass: nil, enum_class: nil) ⇒ 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



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/ecoportal/api/common/content/double_model.rb', line 197

def embeds_many(method, key: method, order_matters: false, order_key: nil, klass: nil, enum_class: nil)
  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) 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



185
186
187
# File 'lib/ecoportal/api/common/content/double_model.rb', line 185

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



113
114
115
116
117
118
119
120
# File 'lib/ecoportal/api/common/content/double_model.rb', line 113

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)


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

def key?
  !!key
end

.new_uuid(length: 24) ⇒ Object



32
33
34
# File 'lib/ecoportal/api/common/content/double_model.rb', line 32

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.



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/ecoportal/api/common/content/double_model.rb', line 40

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.



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

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



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/ecoportal/api/common/content/double_model.rb', line 162

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)
      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?



149
150
151
152
153
154
155
# File 'lib/ecoportal/api/common/content/double_model.rb', line 149

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


137
138
139
140
141
142
143
# File 'lib/ecoportal/api/common/content/double_model.rb', line 137

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



106
107
108
109
# File 'lib/ecoportal/api/common/content/double_model.rb', line 106

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 invinite 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.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/ecoportal/api/common/content/double_model.rb', line 77

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?



126
127
128
129
130
# File 'lib/ecoportal/api/common/content/double_model.rb', line 126

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



288
289
290
291
292
293
294
295
296
# File 'lib/ecoportal/api/common/content/double_model.rb', line 288

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



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

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



329
330
331
332
# File 'lib/ecoportal/api/common/content/double_model.rb', line 329

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



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

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



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

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:



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

def doc
  raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
  if is_root?
    @doc
  else
    _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)



274
275
276
277
# File 'lib/ecoportal/api/common/content/double_model.rb', line 274

def key
  raise "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)



280
281
282
283
284
# File 'lib/ecoportal/api/common/content/double_model.rb', line 280

def key=(value)
  raise "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:



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

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


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

def print_pretty
  puts JSON.pretty_generate(as_json)
  self
end

#replace_doc(new_doc) ⇒ Object

Raises:



369
370
371
372
373
374
375
376
377
378
# File 'lib/ecoportal/api/common/content/double_model.rb', line 369

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



353
354
355
356
357
358
359
360
361
362
# File 'lib/ecoportal/api/common/content/double_model.rb', line 353

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



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

def root
  return self if is_root?
  _parent.root
end

#to_json(*args) ⇒ Object



323
324
325
# File 'lib/ecoportal/api/common/content/double_model.rb', line 323

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