lib/ecoportal/api/common/content/double_model.rb in ecoportal-api-v2-1.1.7 vs lib/ecoportal/api/common/content/double_model.rb in ecoportal-api-v2-1.1.8
- old
+ new
@@ -4,25 +4,25 @@
module Content
# 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**
class DoubleModel < Common::BaseModel
- NOT_USED = Common::Content::ClassHelpers::NOT_USED
- extend Common::Content::ClassHelpers
- include Common::Content::ModelHelpers
-
class UnlinkedModel < StandardError
- def initialize (msg = "Something went wrong when linking the document.", from: nil, key: nil)
+ def initialize(msg = "Something went wrong when linking the document.", from: nil, key: nil)
msg += " From: #{from}." if from
msg += " key: #{key}." if key
super(msg)
end
end
class NoKeyMethod < StandardError
end
+ NOT_USED = Common::Content::ClassHelpers::NOT_USED
+ extend Common::Content::ClassHelpers
+ include Common::Content::ModelHelpers
+
class << self
attr_reader :key
def key?
!!key
@@ -116,20 +116,21 @@
# - 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
- def passforced(method, default: , read_only: false)
+ def passforced(method, default:, read_only: false)
model_forced_keys[method.to_s.freeze] = default
passthrough(method, read_only: read_only)
end
# Ensures `doc` has the `model_forced_keys`. If it doesn't, it adds those missing
# with the defined `default` values
def enforce!(doc)
- return unless doc && doc.is_a?(Hash)
- return if model_forced_keys.empty?
+ 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
@@ -137,37 +138,33 @@
# 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?
def passthrough(*methods, read_only: false)
- pass_reader *methods
- pass_writer *methods unless read_only
+ pass_reader(*methods)
+ pass_writer(*methods) unless read_only
self
end
# 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?
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
+ pass_writer(*methods) {|value| to_time(value)&.iso8601} unless read_only
self
end
# 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?
def passboolean(*methods, read_only: false)
pass_reader(*methods) {|value| value}
- unless read_only
- pass_writer(*methods) {|value| !!value}
- end
+ pass_writer(*methods) {|value| !!value} unless read_only
self
end
# To link as plain `Array` to a subjacent `Hash` model property
# @param methods [Array<Symbol>] the method that exposes the value
@@ -184,22 +181,22 @@
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?)
+ new_obj = dim_class.new(parent: self, key: method, read_only: read_only?)
variable_set(var, new_obj)
end
end
end
# 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
- def embeds_one(method, key: method, nullable: false, klass:)
+ def embeds_one(method, klass:, key: method, nullable: false)
embed(method, key: key, nullable: nullable, multiple: false, klass: klass)
end
# @note
# - if you have a dedicated `Enumerable` class to manage `many`, you should use `:enum_class`
@@ -210,78 +207,87 @@
# @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
# @param read_only [Boolean] 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)
def embeds_many(method, key: method, klass: nil, enum_class: nil,
- order_matters: false, order_key: nil, read_only: self.read_only?)
+ order_matters: false, order_key: nil, read_only: read_only?)
if enum_class
eclass = enum_class
elsif klass
eclass = new_class("#{method}::#{klass}", inherits: Common::Content::CollectionModel) do |dim_class|
- dim_class.klass = klass
+ # 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|
+
+ 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
+ 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
private
- def embed(method, key: method, nullable: false, multiple: false, klass:, read_only: self.read_only?, &block)
+ def embed(
+ method, klass:, key: method,
+ nullable: false, multiple: false, read_only: read_only?,
+ &embed_block
+ )
method = method.to_s.freeze
var = instance_variable_name(method).freeze
- k = key.to_s.freeze
+ obj_k = key.to_s.freeze
# retrieving method (getter)
define_method(method) do
- yield(self) if block_given?
+ # set item klass as referrer to klass (to allow resolve symbol)
+ embed_block&.call(self)
return instance_variable_get(var) if instance_variable_defined?(var)
- unless nullable
- doc[k] ||= multiple ? [] : {}
- end
- return variable_set(var, nil) unless doc[k]
+ doc[obj_k] ||= (multiple ? [] : {}) unless nullable
+ return variable_set(var, nil) unless doc[obj_k]
+
embedded_class = self.class.resolve_class(klass)
+ setup_items_key(embedded_class, doc[obj_k]) if multiple && read_only
- if multiple && read_only
- if doc[k].is_a?(Array) && embedded_class < Common::Content::CollectionModel
- if (item_class = embedded_class.klass) && !item_class.key?
- item_class.passkey :id
- doc[k].each_with_index do |item_doc, i|
- item_doc["id"] = "#{i}" unless item_doc.key?("id")
- end
- end
- end
+ embedded_class.new(
+ doc[obj_k],
+ parent: self,
+ key: obj_k,
+ read_only: read_only? || read_only
+ ).tap do |collection|
+ variable_set(var, collection)
end
-
- embedded_class.new(doc[k], parent: self, key: k, read_only: self.read_only? || read_only).tap do |obj|
- variable_set(var, obj)
- end
end
end
# The list of keys that will be forced in the model
def model_forced_keys
- @forced_model_keys ||= {}
+ @model_forced_keys ||= {}
end
end
- inheritable_class_vars :forced_model_keys, :key, :read_only
+ inheritable_class_vars :model_forced_keys, :key, :read_only
- # `_key` refers to the parent's property that links to this model
+ # `_key` refers to the `_parent`'s property that links to this model
+ # @note while `key` refers to the value of theproperty of this model
+ # that is key (identifies an item in a set of elements)
attr_reader :_parent, :_key, :_read_only
- def initialize(doc = {}, parent: self, key: nil, read_only: self.class.read_only?)
+ 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
@@ -290,14 +296,14 @@
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
+ return unless key_method? && doc && doc.is_a?(Hash)
+
+ self.key = doc[key_method]
+ #puts "\n$(#{self.key}<=>#{self.class})"
end
# @note `read_only` allows for some optimizations, such as storing values
# in instance variables, for optimization purposes
def read_only?
@@ -309,21 +315,26 @@
_parent.root
end
# @return [String] the `value` of the `key` method (i.e. `id` value)
def key
- raise NoKeyMethod.new "No key_method defined for #{self.class}" unless key_method?
- self.method(key_method).call
+ raise NoKeyMethod, "No key_method defined for #{self.class}" unless key_method?
+
+ method(key_method).call
end
# @param [String] the `value` of the `key` method (i.e. `id` value)
def key=(value)
- raise NoKeyMethod.new "No key_method defined for #{self.class}" unless key_method?
- method = "#{key_method}="
- self.method(method).call(value)
+ raise NoKeyMethod, "No key_method defined for #{self.class}" unless key_method?
+
+ method("#{key_method}=").call(value)
end
+ def resolved_doc_key
+ [_doc_key(_key)]
+ end
+
# Offers a method for child classes to transform the key,
# provided that the child's `doc` can be accessed
def _doc_key(value)
if value.is_a?(Content::DoubleModel) && !value.is_root?
#print "?(#{value.class}<=#{value._parent.class})"
@@ -335,29 +346,26 @@
end
# @return [nil, Hash] the underlying `Hash` model as is (carrying current changes)
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
+ 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
# The `original_doc` holds the model as is now on server-side.
# @return [nil, Hash] the underlying `Hash` model as after last `consolidate!` changes
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
+ return @original_doc if is_root?
+
+ _parent.original_doc.dig(*resolved_doc_key.flatten)
end
def as_json
doc
end
@@ -374,11 +382,11 @@
end
# @return [Boolean] stating if there are changes
def dirty?
au = as_update
- !((au == {}) || (au == nil))
+ !((au == {}) || au.nil?)
end
# It makes `original_doc` to be like `doc`
# @note
# - after executing it, there will be no pending changes
@@ -394,11 +402,11 @@
# @key [Symbol] the specific part of the model you want to `reset`
def reset!(key = nil)
if key
keys = [key].flatten.compact
odoc = original_doc.dig(*keys)
- odoc = odoc && JSON.parse(odoc.to_json)
+ odoc &&= JSON.parse(odoc.to_json)
dig_set(doc, keys, odoc)
else
replace_doc(JSON.parse(original_doc.to_json))
end
end
@@ -408,17 +416,15 @@
self
end
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
+ 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
protected
def doc_var?
@@ -436,15 +442,13 @@
is_root? || !!_parent.doc
end
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
+ return (@original_doc = new_doc) if is_root?
+
+ dig_set(_parent.original_doc, resolved_doc_key.flatten, new_doc)
end
# Helper to track down persistent variables
def variable_set(key, value)
var = instance_variable_name(key)
@@ -453,20 +457,20 @@
end
# Helper to remove tracked down instance variables
def variable_remove!(key)
var = instance_variable_name(key)
- unless !@_dim_vars.include?(var)
- @_dim_vars.delete(var)
- remove_instance_variable(var)
- end
+ return unless @_dim_vars.include?(var)
+
+ @_dim_vars.delete(var)
+ remove_instance_variable(var)
end
# Removes all the persistent variables
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)}
+ @_dim_vars.dup.map {|var| variable_remove!(var)}
end
private
def identify_parent(object)
@@ -500,9 +504,23 @@
def key_method
self.class.key
end
+ # It allows to work-around missing item_key
+ def setup_items_key(embedded_class, obj_doc)
+ # only if is going to be a collection
+ return unless obj_doc.is_a?(Array) && embedded_class < Common::Content::CollectionModel
+ return unless (item_class = embedded_class.klass)
+ # if already has key don't need to work around
+ return if item_class&.key
+
+ # apply work around
+ item_class.passkey :id
+ obj_doc.each_with_index do |item_doc, idx|
+ item_doc["id"] = idx.to_s unless item_doc.key?("id")
+ end
+ end
end
end
end
end
end