ruby / psych

A libyaml wrapper for Ruby
MIT License
566 stars 206 forks source link

YAML.dump(date) generates warnings with Rails 7.0.7+ #644

Open BrianHawley opened 1 year ago

BrianHawley commented 1 year ago

Psych serializes Date values with to_s, and just assumes that to_s generates ISO-8601 format dates. This is a little presumptuous of Psych, in my opinion, given that ActiveSupport in Rails overrides to_s with a method that can return strings in different formats.

It sound be better for Psych to serialize Date values with strftime('%F') rather than to_s, so it's less likely to break.

BrianHawley commented 1 year ago

Worse, if you override Date::DATE_FORMATS[:default] in your codebase to match another format - probably not recommended - then this breaks YAML serialization for Rails versions before they fix that deprecation (hopefully in Rails 7.1) by generating data in the Date::DATE_FORMATS[:default] format. This will make such values deserialize as strings rather than Date values.

Here's an initializer patch for both issues, for a codebase where someone changed Date::DATE_FORMATS[:default] to the USA date format ("%m/%d/%Y"). If you didn't override Date::DATE_FORMATS[:default] you won't need the second patch, and you probably don't need the unless clause around the first patch (because it detects the format change, not the warning. Pardon the rubocop and reek pragma comments, and the string quoting rules not matching this gem.

# frozen_string_literal: true

# NOTE: Overriding Date::DATE_FORMATS[:default] breaks YAML serialization of Date values.

date = Date.today

# Check and patch YAML serialization of Date values, if necessary.
unless ActiveSupport::Deprecation.silence { YAML.dump(date) } == "--- #{date.strftime('%F')}\n"
  # Override format and apply https://github.com/ruby/psych/pull/573 too.
  Psych::Visitors::YAMLTree.class_exec do
    # :reek:UncommunicativeMethodName and :reek:UncommunicativeParameterName are irrelevant here.
    def visit_Date(o) # rubocop:disable Naming/MethodName
      formatted = o.gregorian.strftime("%F")
      register(o, @emitter.scalar(formatted, nil, nil, true, false, ::Psych::Nodes::Scalar::ANY))
    end
  end
end

# Check YAML deserialization of the old overriden format, and patch if necessary.
unless YAML.unsafe_load("--- #{date.strftime('%m/%d/%Y')}\n") == date
  # Parse the Date strings that we used to generate before the above patch.
  Psych::ScalarScanner.prepend(
    Module.new do
      def tokenize(string)
        return nil if string.empty?

        if string.match?(/^(?:1[012]|0\d|\d)\/(?:[12]\d|3[01]|0\d|\d)\/\d{4}$/)
          # US format date
          require "date"
          begin
            class_loader.date.strptime(string, "%m/%d/%Y", ::Date::GREGORIAN)
          rescue ArgumentError
            string
          end
        else
          super
        end
      end
    end
  )
end

This patch works on the version of Psych in Ruby 3.1 as well. It does apply the #573 Gregorian date fix to YAML.dump though.