module Mongoid module History class Options attr_reader :trackable, :options def initialize(trackable, opts = {}) @trackable = trackable @options = default_options.merge(opts) end def scope trackable.collection_name.to_s.singularize.to_sym end def prepared return @prepared if @prepared @prepared = options.dup prepare_skipped_fields prepare_formatted_fields parse_tracked_fields_and_relations @prepared end private def default_options { on: :all, except: %i[created_at updated_at], tracker_class_name: nil, modifier_field: :modifier, version_field: :version, changes_method: :changes, scope: scope, track_create: true, track_update: true, track_destroy: true, format: nil } end # Sets the :except attributes and relations in `options` to be an [ Array ] # The attribute names and relations are stored by their `database_field_name`s # Removes the `nil` and duplicate entries from skipped attributes/relations list def prepare_skipped_fields # normalize :except fields to an array of database field strings @prepared[:except] = Array(@prepared[:except]) @prepared[:except] = @prepared[:except].map { |field| trackable.database_field_name(field) }.compact.uniq end def prepare_formatted_fields formats = {} if @prepared[:format].class == Hash @prepared[:format].each do |field, format| next if field.nil? field = trackable.database_field_name(field) if format.class == Hash && trackable.embeds_many?(field) relation_class = trackable.relation_class_of(field) formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) } elsif format.class == Hash && trackable.embeds_one?(field) relation_class = trackable.relation_class_of(field) formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) } else formats[field] = format end end end @prepared[:format] = formats end def parse_tracked_fields_and_relations # case `options[:on]` # when `posts: [:id, :title]`, then it will convert it to `[[:posts, [:id, :title]]]` # when `:foo`, then `[:foo]` # when `[:foo, { posts: [:id, :title] }]`, then return as is @prepared[:on] = Array(@prepared[:on]) @prepared[:on] = @prepared[:on].map { |opt| opt == :all ? :fields : opt } if @prepared[:on].include?(:fields) @prepared[:on] = @prepared[:on].reject { |opt| opt == :fields } @prepared[:on] = @prepared[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym) end if @prepared[:on].include?(:embedded_relations) @prepared[:on] = @prepared[:on].reject { |opt| opt == :embedded_relations } @prepared[:on] = @prepared[:on] | trackable.embedded_relations.keys end @prepared[:fields] = [] @prepared[:dynamic] = [] @prepared[:relations] = { embeds_one: {}, embeds_many: {} } @prepared[:on].each do |option| if option.is_a?(Hash) option.each { |k, v| split_and_categorize(k => v) } else split_and_categorize(option) end end end def split_and_categorize(field_and_options) field = get_database_field_name(field_and_options) field_options = get_field_options(field_and_options) categorize_tracked_option(field, field_options) end # Returns the database_field_name key for tracked option # # @param [ String | Symbol | Array | Hash ] option The field or relation name to track # # @return [ String ] the database field name for tracked option def get_database_field_name(option) key = if option.is_a?(Hash) option.keys.first elsif option.is_a?(Array) option.first end trackable.database_field_name(key || option) end # Returns the tracked attributes for embedded relations, otherwise `nil` # # @param [ String | Symbol | Array | Hash ] option The field or relation name to track # # @return [ nil | Array ] the list of tracked fields for embedded relation def get_field_options(option) if option.is_a?(Hash) option.values.first elsif option.is_a?(Array) option.last end end # Tracks the passed option under: # `fields` # `dynamic` # `relations -> embeds_one` or # `relations -> embeds_many` # # @param [ String ] field The database field name of field or relation to track # @param [ nil | Array ] field_options The tracked fields for embedded relations def categorize_tracked_option(field, field_options = nil) return if @prepared[:except].include?(field) return if reserved_fields.include?(field) field_options = Array(field_options) if trackable.embeds_one?(field) track_relation(field, :embeds_one, field_options) elsif trackable.embeds_many?(field) track_relation(field, :embeds_many, field_options) elsif trackable.fields.keys.include?(field) @prepared[:fields] << field else @prepared[:dynamic] << field end end def track_relation(field, kind, field_options) relation_class = trackable.relation_class_of(field) @prepared[:relations][kind][field] = if field_options.blank? relation_class.fields.keys else %w[_id] | field_options.map { |opt| relation_class.database_field_name(opt) } end end def reserved_fields @reserved_fields ||= ['_id', '_type', @prepared[:version_field].to_s, "#{@prepared[:modifier_field]}_id"] end end end end