# frozen_string_literal: true module Cistern::Attributes PROTECTED_METHODS = [:cistern, :service, :identity, :collection].freeze TRUTHY = ['true', '1'].freeze module ClassMethods def parsers @parsers ||= { array: ->(v, _) { [*v] }, boolean: ->(v, _) { TRUTHY.include?(v.to_s.downcase) }, date: ->(v, _) { v.is_a?(Date) ? v : v && Date.parse(v.to_s) }, float: ->(v, _) { v && v.to_f }, integer: ->(v, _) { v && v.to_i }, string: ->(v, _) { v && v.to_s }, time: ->(v, _) { v.is_a?(Time) ? v : v && Time.parse(v.to_s) }, } end def squasher(tree, path) tree.is_a?(::Hash) ? squasher(tree[path.shift], path) : tree end def transforms @transforms ||= { squash: proc do |_, _v, options| v = Cistern::Hash.stringify_keys(_v) squash = options[:squash] v.is_a?(::Hash) ? squasher(v, squash.dup) : v end, none: ->(_, v, _) { v } } end def default_parser @default_parser ||= ->(v, _opts) { v } end def aliases @aliases ||= Hash.new { |h, k| h[k] = [] } end def attributes @attributes ||= parent_attributes || {} end def attribute(name, options = {}) name_sym = name.to_sym if attributes.key?(name_sym) fail(ArgumentError, "#{self.name} attribute[#{name_sym}] specified more than once") end add_coverage(options) normalize_options(options) attributes[name_sym] = options define_attribute_reader(name_sym, options) unless options[:reader] == false define_attribute_writer(name_sym, options) unless options[:writer] == false name_sym end def identity(*args) args.any? ? @identity = attribute(*args) : (@identity ||= parent_identity) end def ignore_attributes(*args) @ignored_attributes = args end def ignored_attributes @ignored_attributes ||= [] end protected def add_coverage(options) return unless defined? Cistern::Coverage attribute_call = Cistern::Coverage.find_caller_before('cistern/attributes.rb') # Only use DSL attribute calls from within a model if attribute_call && attribute_call.label.start_with?(' v) } end def changed @changes ||= {} end def request_attributes(set = attributes) set.inject({}) do |a,(k,v)| aliases = self.class.attributes[k.to_sym][:aliases] aliases << k if aliases.empty? aliases.each_with_object(a) { |n,r| r[n.to_s] = v } end end def dirty_request_attributes request_attributes(dirty_attributes) end private def missing_attributes(keys) keys.map(&:to_sym).reduce({}) { |a,e| a.merge(e => public_send("#{e}")) } .partition { |_,v| v.nil? } .map { |s| Hash[s] } end def changed!(attribute, from, to) changed[attribute] = if existing = changed[attribute] [existing.first, to] else [from, to] end end def _merge_attributes(new_attributes) protected_methods = (Cistern::Model.instance_methods - PROTECTED_METHODS) ignored_attributes = self.class.ignored_attributes specifications = self.class.attributes class_aliases = self.class.aliases # this has the side effect of dup'ing the incoming hash new_attributes = Cistern::Hash.stringify_keys(new_attributes) new_attributes.each do |key, value| symbol_key = key.to_sym # find nested paths value.is_a?(::Hash) && specifications.each do |name, options| if options[:squash] && options[:squash].first == key send("#{name}=", key => value) end end next if ignored_attributes.include?(symbol_key) if class_aliases.key?(symbol_key) class_aliases[symbol_key].each { |attribute_alias| public_send("#{attribute_alias}=", value) } end assignment_method = "#{key}=" if !protected_methods.include?(symbol_key) && self.respond_to?(assignment_method, true) public_send(assignment_method, value) end end end protected def set_attributes(attributes) @attributes = attributes end end end