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)