Repository: rails/rails
File: activesupport/lib/active_support/messages/rotation_coordinator.rb (Ruby)

Code before:
            raise <<~MESSAGE
              Cannot change #{self.class} configuration after it has already been applied.

              The configuration has been applied with the following salts:
              #{@codecs.keys.map { |salt| "- #{salt.inspect}" }.join("\n")}
            MESSAGE
          end
        end

        def normalize_options(options)
          options = options.dup

          options[:secret_generator] ||= @secret_generator

          secret_generator_kwargs = options[:secret_generator].parameters.
            filter_map { |type, name| name if type == :key || type == :keyreq }
          options[:secret_generator_options] = options.extract!(*secret_generator_kwargs)

          options[:on_rotation] = @on_rotation

          options
        end

        def build_with_rotations(salt, **build_options)
          rotate_options = (build_options.blank? ? [{}] : [build_options]) + @rotate_options
          rotate_options = rotate_options.map { |options| options.is_a?(Proc) ? options.(salt) : options }
          transitional = self.transitional && rotate_options.first
          rotate_options.compact!
          rotate_options[0..1] = rotate_options[0..1].reverse if transitional
          rotate_options = rotate_options.map { |options| normalize_options(options) }.uniq

          raise "No options have been configured for #{salt}" if rotate_options.empty?

          rotate_options.map { |options| build(salt.to_s, **options) }.reduce(&:fall_back_to)
        end

        def build(salt, secret_generator:, secret_generator_options:, **options)
          raise NotImplementedError
        end
    end
  end
end


Review comment (suggestion):
If I understand correctly, the option precedence here defeats the purpose of the `MessageVerifiers` construct.  The goal, in my opinion, is to transition away from one-off configurations such as the current default configuration of `signed_id_verifier`.  In other words, `signed_id_verifier` should prefer the options that `Rails.application.message_verifiers` is configured with, and only fall back to the old default configuration for backward compatibility.

Also, I wouldn't change `RotationCoordinator` to achieve this.  Take a look at the [block form of `MessageEncryptors#rotate`](https://api.rubyonrails.org/v8.0/classes/ActiveSupport/MessageEncryptors.html#method-i-rotate) and see if that helps.


Code after:
      end

      private
        def changing_configuration!
          if @codecs.any?
            raise <<~MESSAGE
              Cannot change #{self.class} configuration after it has already been applied.

              The configuration has been applied with the following salts:
              #{@codecs.keys.map { |salt| "- #{salt.inspect}" }.join("\n")}
            MESSAGE
          end
        end

        def normalize_options(options)
          options = options.dup

          options[:secret_generator] ||= @secret_generator

          secret_generator_kwargs = options[:secret_generator].parameters.
            filter_map { |type, name| name if type == :key || type == :keyreq }
          options[:secret_generator_options] = options.extract!(*secret_generator_kwargs)

          options[:on_rotation] = @on_rotation

          options
        end

        def build_with_rotations(salt)
          rotate_options = @rotate_options.map { |options| options.is_a?(Proc) ? options.(salt) : options }
          transitional = self.transitional && rotate_options.first
          rotate_options.compact!
          rotate_options[0..1] = rotate_options[0..1].reverse if transitional
          rotate_options = rotate_options.map { |options| normalize_options(options) }.uniq

          raise "No options have been configured for #{salt}" if rotate_options.empty?

          rotate_options.map { |options| build(salt.to_s, **options) }.reduce(&:fall_back_to)
        end

        def build(salt, secret_generator:, secret_generator_options:, **options)
          raise NotImplementedError
        end
    end
  end
end