module ROM
  class Repository
    # @api private
    class StructAttributes < Module
      def initialize(attributes)
        super()

        define_constructor(attributes)

        module_eval do
          include Dry::Equalizer.new(*attributes)

          attr_reader(*attributes)

          define_method(:to_h) do
            attributes.each_with_object({}) do |attribute, h|
              h[attribute] = __send__(attribute)
            end
          end

          define_method(:assert_known_attributes) do |values|
            actual = values.keys
            unknown, missing = actual - attributes, attributes - actual

            if unknown.size > 0 || missing.size > 0
              raise ROM::Struct::InvalidAttributes.new(self.class, missing, unknown)
            end
          end
        end
      end

      def define_constructor(attributes)
        ivs = attributes.map { |a| "@#{a}" }.join(', ')
        values = attributes.map { |a| "values[:#{a}]" }.join(', ')

        assignment = attributes.size > 0 ? "#{ivs} = #{values}" : EMPTY_STRING

        module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
          def initialize(values)
            assert_known_attributes(values)
            #{assignment}
          end
        RUBY
      end
    end
  end
end

module ROM
  class Repository
    # @api private
    class StructAttributes < Module
      def initialize(attributes)
        super()

        define_constructor(attributes)

        module_eval do
          include Dry::Equalizer.new(*attributes)

          attr_reader(*attributes)

          define_method(:to_h) do
            attributes.each_with_object({}) do |attribute, h|
              h[attribute] = __send__(attribute)
            end
          end

          define_method(:assert_known_attributes) do |values|
            actual = values.keys
            unknown, missing = actual - attributes, attributes - actual

            if unknown.size > 0 || missing.size > 0
              raise ROM::Struct::InvalidAttributes.new(self.class, missing, unknown)
            end
          end
        end
      end

      def define_constructor(attributes)
        ivs = attributes.map { |a| "@#{a}" }.join(', ')
        values = attributes.map { |a| "values.fetch(:#{a})" }.join(', ')

        assignment = attributes.size > 0 ? "#{ivs} = #{values}" : EMPTY_STRING

        module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
          def initialize(values)
            assert_known_attributes(values) if values.size > #{attributes.size}
            #{assignment}
          rescue KeyError
            assert_known_attributes(values)
          end
        RUBY
      end
    end
  end
end

Use .fetch for extracting values from hash
Use .fetch for extracting values from hash

Also check for unknown keys more lazily. This speeds initialization up
by ~3x. :heart: @AMHOL
