# frozen_string_literal: true require 'ice_nine' require_relative '../types' module Fortnox module API module Model class Base < Fortnox::API::Types::Model # TODO(jonas): Restructure this class a bit, it is not very readable. attr_accessor :unsaved attr_writer :parent def self.attribute(name, *args) define_method("#{name}?") do !send(name).nil? end super end def self.new(hash = {}) begin obj = preserve_meta_properties(hash) do super(hash) end rescue Dry::Struct::Error => e raise Fortnox::API::AttributeError, e end IceNine.deep_freeze(obj) end def self.stub new(self::STUB.dup) end def unique_id send(self.class::UNIQUE_ID) end # This filtering logic could be improved since it is currently O(N*M). def attributes(*options) return self.class.schema if options.nil? options = Array(options) self.class.schema.find_all do |_name, attribute| options.all? { |option| attribute.is?(option) } end end def to_hash(recursive = false) return super() if recursive self.class.schema.each_with_object({}) do |key, result| # Only output attributes that have a value set result[key.name] = self[key.name] if self.send("#{key.name}?") end end def update(hash) old_attributes = to_hash new_attributes = old_attributes.merge(hash) return self if new_attributes == old_attributes new_hash = new_attributes.delete_if { |_, value| value.nil? } new_hash[:new] = @new new_hash[:parent] = self self.class.new(new_hash) end # Generic comparison, by value, use .eql? or .equal? for object identity. def ==(other) return false unless other.is_a? self.class to_hash == other.to_hash end def new? @new end def saved? @saved end def parent? !@parent.nil? end def parent @parent || self.class.new(self.class::STUB.dup) end # dry-types filter anything that isn't specified as an attribute on the # class that is being instantiated. This wrapper preserves the meta # properties we need to track object state during that initialisation and # sets them on the object after dry-types is done with it. def self.preserve_meta_properties(hash) is_unsaved = hash.delete(:unsaved) { true } is_new = hash.delete(:new) { true } parent = hash.delete(:parent) { nil } obj = yield # TODO: remove new, unsaved, saved obj.instance_variable_set(:@unsaved, is_unsaved) obj.instance_variable_set(:@saved, !is_unsaved) obj.instance_variable_set(:@new, is_new) obj.instance_variable_set(:@parent, parent) obj end private def private_attributes @private_attributes ||= attribute_set.reject(&:public_writer?) end end end end end