activesupport/lib/active_support/messages/metadata.rb
# frozen_string_literal: true
require "time"
require "active_support/json"
require_relative "serializer_with_fallback"
module ActiveSupport
module Messages # :nodoc:
module Metadata # :nodoc:
singleton_class.attr_accessor :use_message_serializer_for_metadata
ENVELOPE_SERIALIZERS = [
*SerializerWithFallback::SERIALIZERS.values,
ActiveSupport::JSON,
::JSON,
Marshal,
]
TIMESTAMP_SERIALIZERS = [
SerializerWithFallback::SERIALIZERS.fetch(:message_pack),
SerializerWithFallback::SERIALIZERS.fetch(:message_pack_allow_marshal),
]
ActiveSupport.on_load(:message_pack) do
ENVELOPE_SERIALIZERS << ActiveSupport::MessagePack
TIMESTAMP_SERIALIZERS << ActiveSupport::MessagePack
end
private
def serialize_with_metadata(data, **metadata)
has_metadata = metadata.any? { |k, v| v }
if has_metadata && !use_message_serializer_for_metadata?
data_string = serialize_to_json_safe_string(data)
envelope = wrap_in_metadata_legacy_envelope({ "message" => data_string }, **metadata)
serialize_to_json(envelope)
else
data = wrap_in_metadata_envelope({ "data" => data }, **metadata) if has_metadata
serialize(data)
end
end
def deserialize_with_metadata(message, **expected_metadata)
if dual_serialized_metadata_envelope_json?(message)
envelope = deserialize_from_json(message)
extracted = extract_from_metadata_envelope(envelope, **expected_metadata)
deserialize_from_json_safe_string(extracted["message"])
else
deserialized = deserialize(message)
if metadata_envelope?(deserialized)
extract_from_metadata_envelope(deserialized, **expected_metadata)["data"]
elsif expected_metadata.none? { |k, v| v }
deserialized
else
throw :invalid_message_content, "missing metadata"
end
end
end
def use_message_serializer_for_metadata?
Metadata.use_message_serializer_for_metadata && Metadata::ENVELOPE_SERIALIZERS.include?(serializer)
end
def wrap_in_metadata_envelope(hash, expires_at: nil, expires_in: nil, purpose: nil)
expiry = pick_expiry(expires_at, expires_in)
hash["exp"] = expiry if expiry
hash["pur"] = purpose.to_s if purpose
{ "_rails" => hash }
end
def wrap_in_metadata_legacy_envelope(hash, expires_at: nil, expires_in: nil, purpose: nil)
expiry = pick_expiry(expires_at, expires_in)
hash["exp"] = expiry
hash["pur"] = purpose
{ "_rails" => hash }
end
def extract_from_metadata_envelope(envelope, purpose: nil)
hash = envelope["_rails"]
if hash["exp"] && Time.now.utc >= parse_expiry(hash["exp"])
throw :invalid_message_content, "expired"
end
if hash["pur"].to_s != purpose.to_s
throw :invalid_message_content, "mismatched purpose"
end
hash
end
def metadata_envelope?(object)
object.is_a?(Hash) && object.key?("_rails")
end
def dual_serialized_metadata_envelope_json?(string)
string.start_with?('{"_rails":{"message":')
end
def pick_expiry(expires_at, expires_in)
expiry = if expires_at
expires_at.utc
elsif expires_in
Time.now.utc.advance(seconds: expires_in)
end
unless Metadata::TIMESTAMP_SERIALIZERS.include?(serializer)
expiry = expiry&.iso8601(3)
end
expiry
end
def parse_expiry(expires_at)
if !expires_at.is_a?(String)
expires_at
elsif ActiveSupport.use_standard_json_time_format
Time.iso8601(expires_at)
else
Time.parse(expires_at)
end
end
def serialize_to_json(data)
ActiveSupport::JSON.encode(data)
end
def deserialize_from_json(serialized)
ActiveSupport::JSON.decode(serialized)
rescue ::JSON::ParserError => error
# Throw :invalid_message_format instead of :invalid_message_serialization
# because here a parse error is due to a bad message rather than an
# incompatible `self.serializer`.
throw :invalid_message_format, error
end
def serialize_to_json_safe_string(data)
encode(serialize(data), url_safe: false)
end
def deserialize_from_json_safe_string(string)
deserialize(decode(string, url_safe: false))
end
end
end
end