class Ecoportal::API::Common::Content::DoubleModel

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**

Constants

NOT_USED

Attributes

key[R]
_key[R]
_parent[R]

Public Class Methods

embeds_many(method, key: method, order_matters: false, order_key: nil, klass: nil, enum_class: nil) click to toggle source

@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

@param method [Symbol] the method that exposes the embeded object @param key [Symbol] the `key` that embeds it to the underlying `Hash` model @param order_matters [Boolean] to state if the order will matter @param klass [Class, String] the class of the individual elements it embeds @param enum_class [Class, String] the class of the collection that will hold the individual elements

# File lib/ecoportal/api/common/content/double_model.rb, line 199
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, 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)
end
embeds_one(method, key: method, nullable: false, klass:) click to toggle source

Helper to embed one nested object under one property @param method [Symbol] the method that exposes the embeded object @param key [Symbol] the `key` that embeds it to the underlying `Hash` model @nullable [Boolean] to specify if this object can be `nil` @param klass [Class, String] the class of the embedded object

# File lib/ecoportal/api/common/content/double_model.rb, line 187
def embeds_one(method, key: method, nullable: false, klass:)
  embed(method, key: key, nullable: nullable, multiple: false, klass: klass)
end
enforce!(doc) click to toggle source

Ensures `doc` has the `model_forced_keys`. If it doesn't, it adds those missing

with the defined `default` values
# File lib/ecoportal/api/common/content/double_model.rb, line 115
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=(value) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 30
def key=(value)
  @key = value.to_s.freeze
end
key?() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 26
def key?
  !!key
end
new(doc = {}, parent: self, key: nil) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 246
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
new_uuid(length: 12) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 34
def new_uuid(length: 12)
  SecureRandom.hex(length)
end
pass_reader(*methods) { |value| ... } click to toggle source

Same as `attr_reader` but links to a subjacent `Hash` model property @note it does not create an _instance variable_ @param methods [Array<Symbol>] the method that exposes the value

as well as its `key` in the underlying `Hash` model.
# File lib/ecoportal/api/common/content/double_model.rb, line 42
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) { |value| ... } click to toggle source

Same as `attr_writer` but links to a subjacent `Hash` model property @note it does not create an _instance variable_ @param methods [Array<Symbol>] the method that exposes the value

as well as its `key` in the underlying `Hash` model.
# File lib/ecoportal/api/common/content/double_model.rb, line 59
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) click to toggle source

To link as plain `Array` to a subjacent `Hash` model property @param methods [Array<Symbol>] the method that exposes the value

as well as its `key` in the underlying `Hash` model.

@param order_matters [Boolean] does the order matter @param uniq [Boolean] should it contain unique elements

# File lib/ecoportal/api/common/content/double_model.rb, line 164
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) click to toggle source

To link as a `Boolean` to a subjacent `Hash` model property @param methods [Array<Symbol>] the method that exposes the value

as well as its `key` in the underlying `Hash` model.

@param read_only [Boolean] should it only define the reader?

# File lib/ecoportal/api/common/content/double_model.rb, line 151
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) click to toggle source

To link as a `Time` date to a subjacent `Hash` model property @see Ecoportal::API::Common::Content::DoubleModel#passthrough @param methods [Array<Symbol>] the method that exposes the value

as well as its `key` in the underlying `Hash` model.

@param read_only [Boolean] should it only define the reader?

# File lib/ecoportal/api/common/content/double_model.rb, line 139
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) click to toggle source

These are methods that should always be present in patch update @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

@param method [Symbol] the method that exposes the value

as well as its `key` in the underlying `Hash` model.

@param default [Value] the default value that

this `key` will be written in the model when it doesn't exixt
# File lib/ecoportal/api/common/content/double_model.rb, line 108
def passforced(method, default: , read_only: false)
  model_forced_keys[method.to_s.freeze] = default
  passthrough(method, read_only: read_only)
end
passkey(method) { |value| ... } click to toggle source

This method is essential to give stability to the model @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

@param method [Symbol] the method that exposes the value

as well as its `key` in the underlying `Hash` model.
# File lib/ecoportal/api/common/content/double_model.rb, line 79
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) click to toggle source

Same as `attr_accessor` but links to a subjacent `Hash` model property @param methods [Array<Symbol>] the method that exposes the value

as well as its `key` in the underlying `Hash` model.

@param read_only [Boolean] should it only define the reader?

# File lib/ecoportal/api/common/content/double_model.rb, line 128
def passthrough(*methods, read_only: false)
  pass_reader *methods
  pass_writer *methods unless read_only
  self
end

Private Class Methods

embed(method, key: method, nullable: false, multiple: false, klass:) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 216
def embed(method, key: method, nullable: false, multiple: false, klass:)
  method = method.to_s.freeze
  var    = instance_variable_name(method).freeze
  k      = key.to_s.freeze

  # retrieving method (getter)
  define_method(method) do
    return instance_variable_get(var) if instance_variable_defined?(var)
    unless nullable
      doc[k] ||= multiple ? [] : {}
    end
    return variable_set(var, nil) unless doc[k]

    self.class.resolve_class(klass).new(
      doc[k], parent: self, key: k
    ).tap {|obj| variable_set(var, obj)}
  end
end
model_forced_keys() click to toggle source

The list of keys that will be forced in the model

# File lib/ecoportal/api/common/content/double_model.rb, line 236
def model_forced_keys
  @forced_model_keys ||= {}
end

Public Instance Methods

_doc_key(value) click to toggle source

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

# File lib/ecoportal/api/common/content/double_model.rb, line 284
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_json() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 315
def as_json
  doc
end
as_update() click to toggle source

@return [nil, Hash] the patch `Hash` model including only the changes between

`original_doc` and `doc`
# File lib/ecoportal/api/common/content/double_model.rb, line 325
def as_update
  new_doc = as_json
  Common::Content::HashDiffPatch.patch_diff(new_doc, original_doc)
end
consolidate!() click to toggle source

It makes `original_doc` to be like `doc` @note

- after executing it, there will be no pending changes
- you should technically run this command, after a successful update request to the server
# File lib/ecoportal/api/common/content/double_model.rb, line 339
def consolidate!
  replace_original_doc(JSON.parse(doc.to_json))
end
dirty?() click to toggle source

@return [Boolean] stating if there are changes

# File lib/ecoportal/api/common/content/double_model.rb, line 331
def dirty?
  as_update != {}
end
doc() click to toggle source

@return [nil, Hash] the underlying `Hash` model as is (carrying current changes)

# File lib/ecoportal/api/common/content/double_model.rb, line 295
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
key() click to toggle source

@return [String] the `value` of the `key` method (i.e. `id` value)

# File lib/ecoportal/api/common/content/double_model.rb, line 270
def key
  raise "No key_method defined for #{self.class}" unless key_method?
  self.method(key_method).call
end
key=(value) click to toggle source

@param [String] the `value` of the `key` method (i.e. `id` value)

# File lib/ecoportal/api/common/content/double_model.rb, line 276
def key=(value)
  raise "No key_method defined for #{self.class}" unless key_method?
  method = "#{key_method}="
  self.method(method).call(value)
end
original_doc() click to toggle source

The `original_doc` holds the model as is now on server-side. @return [nil, Hash] the underlying `Hash` model as after last `consolidate!` changes

# File lib/ecoportal/api/common/content/double_model.rb, line 306
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
print_pretty() click to toggle source
replace_doc(new_doc) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 364
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) click to toggle source

It makes `doc` to be like `original` @note

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

@key [Symbol] the specific part of the model you want to `reset`

# File lib/ecoportal/api/common/content/double_model.rb, line 348
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
root() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 264
def root
  return self if is_root?
  _parent.root
end
to_json(*args) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 319
def to_json(*args)
  doc.to_json(*args)
end

Protected Instance Methods

is_root?() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 377
def is_root?
  _parent == self && !!defined?(@doc)
end
linked?() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 381
def linked?
  is_root? || !!_parent.doc
end
replace_original_doc(new_doc) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 385
def replace_original_doc(new_doc)
  raise UnlinkedModel.new(from: "#{self.class}#replace_original_doc", key: _key) unless linked?
  if is_root?
    @original_doc = new_doc
  else
    dig_set(_parent.original_doc, [_doc_key(_key)].flatten, new_doc)
  end
end
variable_remove!(key) click to toggle source

Helper to remove tracked down instance variables

# File lib/ecoportal/api/common/content/double_model.rb, line 402
def variable_remove!(key)
  var = instance_variable_name(key)
  unless !@_dim_vars.include?(var)
    @_dim_vars.delete(var)
    remove_instance_variable(var)
  end
end
variable_set(key, value) click to toggle source

Helper to track down persistent variables

# File lib/ecoportal/api/common/content/double_model.rb, line 395
def variable_set(key, value)
  var = instance_variable_name(key)
  @_dim_vars.push(var).uniq!
  instance_variable_set(var, value)
end
variables_remove!() click to toggle source

Removes all the persistent variables

# File lib/ecoportal/api/common/content/double_model.rb, line 411
def variables_remove!
  #puts "going to remove vars: #{@_dim_vars} on #{self.class} (parent: #{identify_parent(self._parent)})"
  @_dim_vars.dup.map {|k| variable_remove!(k)}
end

Private Instance Methods

dig_set(obj, keys, value) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 431
def dig_set(obj, keys, value)
  if keys.length == 1
    obj[keys.first] = value
  else
    dig_set(obj[keys.first], keys.slice(1..-1), value)
  end
end
identify_parent(object) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 418
def identify_parent(object)
  case object
  when Ecoportal::API::V2::Page::Stage
    "stage #{object.name}"
  when Ecoportal::API::V2::Page::Section
    "section #{object.heading}"
  end
end
instance_variable_name(key) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 427
def instance_variable_name(key)
  self.class.instance_variable_name(key)
end
key_method() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 447
def key_method
  self.class.key
end
key_method?() click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 443
def key_method?
  self.class.key?
end
used_param?(val) click to toggle source
# File lib/ecoportal/api/common/content/double_model.rb, line 439
def used_param?(val)
  self.class.used_param?(val)
end