lib/rspectre/keyword_struct.rb
# frozen_string_literal: true

module RSpectre
  # Heavily influenced by dkubb/equalizer, mbj/concord, and mbj/anima
  class KeywordStruct < Module
    def initialize(*names) # rubocop:disable Lint/MissingSuper
      raise 'Specify at least one keyword name!' if names.empty?

      names.each do |name|
        next if /\A\w+\z/.match?(name)

        raise "Invalid keyword name #{name.inspect}!"
      end

      @module =
        Module.new do
          attr_reader(*names)

          define_method(:eql?) do |other|
            other.instance_of?(self.class) && names.all? do |name|
              __send__(name).eql?(other.__send__(name))
            end
          end
          alias_method :==, :eql?

          define_method(:hash) do
            [self.class, *names.map { |name| __send__(name) }].hash
          end

          define_method(:inspect) do
            class_name = self.class.name || self.class.inspect
            attributes = names.map { |name| "#{name}=#{__send__(name).inspect}" }.join(' ')
            "#<#{class_name} #{attributes}>"
          end
        end.tap do |generated_module|
          generated_module.class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
            def initialize(#{names.map { |name| "#{name}:" }.join(', ')})
              #{names.map { |name| "@#{name} = #{name}" }.join("\n  ")}
            end
          RUBY
        end
    end

    def included(descendant)
      descendant.include(@module)
    end
  end
end
