module Ecoportal module API module Common class BaseModel class UnlinkedModel < Exception 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 extend BaseClass class << self def passthrough(*methods, to: :doc) methods.each do |method| method = method.to_s define_method method do send(to)[method] end define_method "#{method}=" do |value| send(to)[method] = value end end end def embeds_one(method, key: method, nullable: false, klass:) method = method.to_s.freeze var = "@#{method}".freeze key = key.to_s.freeze define_method(method) do if instance_variable_defined?(var) value = instance_variable_get(var) return value unless nullable return value if (value && doc[key]) || (!value && !doc[key]) remove_instance_variable(var) end doc[key] ||= {} unless nullable return instance_variable_set(var, nil) unless doc[key] self.class.resolve_class(klass).new( doc[key], parent: self, key: key ).tap {|obj| instance_variable_set(var, obj)} end end end attr_reader :_parent, :_key def initialize(doc = {}, parent: self, key: nil) @_parent = parent @_key = key if !_parent || !_key @doc = JSON.parse(doc.to_json) @original_doc = JSON.parse(@doc.to_json) @initial_doc = JSON.parse(@doc.to_json) end end def doc raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked? return @doc if is_root? _parent.doc.dig(*[_key].flatten) end 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(*[_key].flatten) end def initial_doc raise UnlinkedModel.new(from: "#{self.class}#initial_doc", key: _key) unless linked? return @initial_doc if is_root? _parent.initial_doc&.dig(*[_key].flatten) end # It replaces `doc` by `new_doc` # @return [Hash] `doc` before change def replace_doc!(new_doc) raise UnlinkedModel.new(from: "#{self.class}#replace_doc", key: _key) unless linked? @doc.tap do @doc = new_doc end end # It replaces `original_doc` by `new_doc` # @return [Hash] `original_doc` before change def replace_original_doc!(new_doc) raise UnlinkedModel.new(from: "#{self.class}#replace_original_doc", key: _key) unless linked? @original_doc.tap do @original_doc = new_doc end end def as_json doc end def to_json(*args) doc.to_json(*args) end def as_update(ref = :last, ignore: []) new_doc = as_json ref_doc = ref == :total ? initial_doc : original_doc Common::HashDiff.diff(new_doc, ref_doc, ignore: ignore) end def dirty? as_update != {} end # It consolidates all the changes carried by `doc` by setting it as `original_doc`. def consolidate! raise UnlinkedModel.new(from: "#{self.class}#consolidate!", key: _key) unless linked? new_doc = JSON.parse(doc.to_json) if is_root? @original_doc = new_doc else dig_set(_parent.original_doc, [_key].flatten, new_doc) end end # It removes all the changes carried by `doc` by restoring `original_doc` into `doc`. # @note # 1. When there are nullable properties, it may be required to apply `reset!` from the parent # i.e. `parent.reset!("child")` # when parent.child is `nil` # 2. In such a case, only immediate childs are allowed to be reset # @param key [String, Array, nil] if given, it only resets the specified property def reset!(key = nil) raise "'key' should be a String. Given #{key}" unless !key || key.is_a?(String) raise UnlinkedModel.new(from: "#{self.class}#reset!", key: _key) unless linked? if key if self.respond_to?(key) && child = self.send(key) && child.is_a?(Ecoportal::API::Common::BaseModel) child.reset! else new_doc = original_doc && original_doc[key] dig_set(doc, [key], new_doc && JSON.parse(new_doc.to_json)) # regenerate object if new_doc is null self.send(key) if !new_doc && self.respond_to?(key) end else new_doc = JSON.parse(original_doc.to_json) if is_root? @doc = new_doc else dig_set(_parent.doc, [_key].flatten, new_doc) end end end def print_pretty puts JSON.pretty_generate(as_json) self end protected def is_root? _parent == self && !!defined?(@doc) end def linked? is_root? || !!_parent.doc.dig(*[_key].flatten) end private 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 def set_uniq_array_keep_order(key, value) unless value.is_a?(Array) raise "#{key}= needs to be passed an Array, got #{value.class}" end ini_vals = (original_doc && original_doc[key]) || [] value = value.uniq # preserve original order to avoid false updates doc[key] = ((ini_vals & value) + (value - ini_vals)).compact end end end end end