lib/ecoportal/api/common/content/double_model.rb in ecoportal-api-v2-0.8.9 vs lib/ecoportal/api/common/content/double_model.rb in ecoportal-api-v2-0.8.10
- old
+ new
@@ -35,10 +35,12 @@
SecureRandom.hex(length)
end
# 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.
def pass_reader(*methods)
methods.each do |method|
method = method.to_s.freeze
define_method method do
@@ -50,10 +52,12 @@
self
end
# 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.
def pass_writer(*methods)
methods.each do |method|
method = method.to_s.freeze
define_method "#{method}=" do |value|
@@ -68,10 +72,12 @@
# @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.
def passkey(method)
method = method.to_s.freeze
var = instance_variable_name(method)
self.key = method
@@ -89,40 +95,72 @@
end
self
end
+ # 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
+ 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?
+ model_forced_keys.each do |key, default|
+ doc[key] = default unless doc.key?(key)
+ end
+ doc
+ end
+
# 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
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
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
self
end
# 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
def passarray(*methods, order_matters: true, uniq: true)
methods.each do |method|
method = method.to_s.freeze
@@ -140,18 +178,26 @@
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:)
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`
# - otherwise, just indicate the child class in `:klass` and it will auto generate the class
- # @param
+ # @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
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|
@@ -184,19 +230,28 @@
doc[k], parent: self, key: k
).tap {|obj| variable_set(var, obj)}
end
end
+ # The list of keys that will be forced in the model
+ def model_forced_keys
+ @forced_model_keys ||= {}
+ end
+
end
+ inheritable_class_vars :forced_model_keys
+
attr_reader :_parent, :_key
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
@@ -209,15 +264,17 @@
def root
return self if is_root?
_parent.root
end
+ # @return [String] the `value` of the `key` method (i.e. `id` value)
def key
raise "No key_method defined for #{self.class}" unless key_method?
self.method(key_method).call
end
+ # @param [String] the `value` of the `key` method (i.e. `id` value)
def key=(value)
raise "No key_method defined for #{self.class}" unless key_method?
method = "#{key_method}="
self.method(method).call(value)
end
@@ -232,42 +289,63 @@
#print "!(#{value}<=#{self.class})"
value
end
end
+ # @return [nil, Hash] the underlying `Hash` model as is (carrying current changes)
def doc
raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
- return @doc if is_root?
- _parent.doc.dig(*[_doc_key(_key)].flatten)
+ if is_root?
+ @doc
+ else
+ _parent.doc.dig(*[_doc_key(_key)].flatten)
+ end
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?
- return @original_doc if is_root?
- _parent.original_doc.dig(*[_doc_key(_key)].flatten)
+ if is_root?
+ @original_doc
+ else
+ _parent.original_doc.dig(*[_doc_key(_key)].flatten)
+ end
end
def as_json
doc
end
def to_json(*args)
doc.to_json(*args)
end
+ # @return [nil, Hash] the patch `Hash` model including only the changes between
+ # `original_doc` and `doc`
def as_update
new_doc = as_json
Common::Content::HashDiffPatch.patch_diff(new_doc, original_doc)
end
+ # @return [Boolean] stating if there are changes
def dirty?
as_update != {}
end
+ # 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
def consolidate!
replace_original_doc(JSON.parse(doc.to_json))
end
+ # 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`
def reset!(key = nil)
if key
keys = [key].flatten.compact
odoc = original_doc.dig(*keys)
odoc = odoc && JSON.parse(odoc.to_json)