lib/eac_ruby_utils/yaml.rb
# frozen_string_literal: true require 'active_support/time_with_zone' require 'date' require 'yaml' module EacRubyUtils # A safe YAML loader/dumper with common types included. class Yaml class << self DEFAULT_PERMITTED_CLASSES = [ActiveSupport::TimeWithZone, ActiveSupport::TimeZone, ::Array, ::Date, ::DateTime, ::FalseClass, ::Hash, ::NilClass, ::Numeric, ::String, ::Symbol, ::Time, ::TrueClass].freeze def dump(object) ::YAML.dump(sanitize(object)) end def dump_file(path, object) ::File.write(path.to_s, dump(object)) end def load(string) ::YAML.safe_load(string, permitted_classes: permitted_classes) end def load_file(path) load(::File.read(path.to_s)) end def permitted_classes DEFAULT_PERMITTED_CLASSES end def sanitize(object) Sanitizer.new(object).result end def yaml?(object) return false unless object.is_a?(::String) return false unless object.start_with?('---') load(object) true rescue ::Psych::Exception false end class Sanitizer attr_reader :source RESULT_TYPES = %w[permitted enumerableable hashable].freeze def initialize(source) @source = source end def result RESULT_TYPES.each do |type| return send("result_#{type}") if send("result_#{type}?") end source.to_s end private def result_enumerableable? source.respond_to?(:to_a) && !source.is_a?(::Hash) end def result_enumerableable source.to_a.map { |child| sanitize_value(child) } end def result_hashable? source.respond_to?(:to_h) end def result_hashable source.to_h.to_h { |k, v| [k.to_sym, sanitize_value(v)] } end def result_nil? source.nil? end def result_nil nil end def result_permitted? (::EacRubyUtils::Yaml.permitted_classes - [::Array, ::Hash]) .any? { |klass| source.is_a?(klass) } end def result_permitted source end def sanitize_value(value) self.class.new(value).result end end end end end