spec/support/serializer_matchers.rb
# frozen_string_literal: true

# This file contains custom RSpec matchers for AMS.
# NOTE: It was developed for AMS v0.9 and the API might be changed in future, so that should be examined when moving
# between stable versions of AMS (e.g. 0.9 to 0.10, or 0.9 to a possible 1.0).

# This is a matcher that tests a ActiveModel Serializer derivatives to run a serializer for
# an association with expected properties and data.
#
# This matcher is expected to be used in unit testing of ActiveModel Serializer derivatives.
#
# It is mostly a wrapper around RSpec::Mocks::Matchers::Receive matcher with expectations based on
# ActiveModel::Serializer internal API knowledge.
# NOTE: this matcher uses knowledge of AMS internals
RSpec::Matchers.define :serialize_association do |association_name|
  match do |root_serializer_class|
    association = fetch_association(root_serializer_class, association_name)
    @serializer_from_options = association.serializer_from_options
    execute_receive_matcher_with(association)
  end

  # Sets expectation for a specific serializer class to be used for has_one association serialization
  chain :with_serializer, :association_serializer_class

  # Sets expectation for a specific serializer class to be user for has_many association serialization
  chain :with_each_serializer, :each_serializer_class

  # Sets expectation for actual data to be passed to the serializer of the association
  chain :with_object, :association_object
  alias_method :with_objects, :with_object

  private

  # subject is what comes from the expect method argument in usual notation.
  # So this method is equivalent of calling `expect(subject).to receive(:build_serializer)` but valid within a custom
  # RSpec matcher's match method.
  def execute_receive_matcher_with(subject)
    receive_matcher.matches?(subject)
  end

  def receive_matcher
    receive(:build_serializer).and_wrap_original do |original, object, options, &block|
      with_object_expectation(object)

      original.call(object, options, &block).tap do |serializer|
        expect(serializer).to be_an_instance_of(serializer_class) unless serializer_class.nil?
        expect(serializer).to serialize_each_with(each_serializer_class) unless each_serializer_class.nil?
      end
    end
  end

  def with_object_expectation(object)
    if association_object.is_a?(Array)
      if serializer_class == FlatMapArraySerializer
        expect(object.flat_map(&:to_a)).to match_array(association_object)
      else
        expect(object).to match_array(association_object)
      end
    elsif !association_object.nil?
      expect(object).to eq(association_object)
    end
  end

  def fetch_association(serializer_class, association_name)
    serializer_class._associations[association_name]
  end

  def serializer_class
    @serializer_class ||= pick_serializer_class
  end

  def pick_serializer_class
    return association_serializer_class unless association_serializer_class.nil?
    return @serializer_from_options unless @serializer_from_options.nil?
    return ActiveModel::ArraySerializer unless each_serializer_class.nil?
  end
end

# This serializer tests that ActiveModel::ArraySerializer uses specific serializer for each member object of the array.
# We could also set a mock expectation on each serializer, but it maybe overly complicated for our present
# requirements.
# NOTE: this matcher uses knowledge of AMS internals
RSpec::Matchers.define :serialize_each_with do |expected|
  match do |actual|
    actual.is_a?(ActiveModel::ArraySerializer) && actual.instance_variable_get("@each_serializer") == expected
  end
end
