1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Allow keyword arguments to work with ActiveJob

Unfortunately, the HashWithIndifferent access approach is insufficient
for our needs. It's perfectly reasonable to want to use keyword
arguments with Active Job, which we will see as a symbol keyed hash. For
Ruby to convert this back to keyword arguments, it must deserialize to a
symbol keyed hash.

There are two primary changes to the serialization behavior. We first
treat a HWIA separately, and mark it as such so we can convert it back
into a HWIA during deserialization.

For normal hashes, we keep a list of all symbol keys, and convert them
back to symbol keys after deserialization.

Fixes #18741.
This commit is contained in:
Sean Griffin 2015-01-30 13:40:49 -07:00
parent b93b39eff6
commit 31085a5cd4
4 changed files with 83 additions and 20 deletions

View file

@ -1,3 +1,9 @@
* Allow keyword arguments to be used with Active Job.
Fixes #18741.
*Sean Griffin*
* Add `:only` option to `assert_enqueued_jobs`, to check the number of times * Add `:only` option to `assert_enqueued_jobs`, to check the number of times
a specific kind of job is enqueued. a specific kind of job is enqueued.

View file

@ -1,4 +1,4 @@
require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash'
module ActiveJob module ActiveJob
# Raised when an exception is raised during job arguments deserialization. # Raised when an exception is raised during job arguments deserialization.
@ -44,7 +44,9 @@ module ActiveJob
private private
GLOBALID_KEY = '_aj_globalid'.freeze GLOBALID_KEY = '_aj_globalid'.freeze
private_constant :GLOBALID_KEY SYMBOL_KEYS_KEY = '_aj_symbol_keys'.freeze
WITH_INDIFFERENT_ACCESS_KEY = '_aj_hash_with_indifferent_access'.freeze
private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
def serialize_argument(argument) def serialize_argument(argument)
case argument case argument
@ -54,10 +56,15 @@ module ActiveJob
{ GLOBALID_KEY => argument.to_global_id.to_s } { GLOBALID_KEY => argument.to_global_id.to_s }
when Array when Array
argument.map { |arg| serialize_argument(arg) } argument.map { |arg| serialize_argument(arg) }
when ActiveSupport::HashWithIndifferentAccess
result = serialize_hash(argument)
result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
result
when Hash when Hash
argument.each_with_object({}) do |(key, value), hash| symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
hash[serialize_hash_key(key)] = serialize_argument(value) result = serialize_hash(argument)
end result[SYMBOL_KEYS_KEY] = symbol_keys
result
else else
raise SerializationError.new("Unsupported argument type: #{argument.class.name}") raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
end end
@ -75,7 +82,7 @@ module ActiveJob
if serialized_global_id?(argument) if serialized_global_id?(argument)
deserialize_global_id argument deserialize_global_id argument
else else
deserialize_hash argument deserialize_hash(argument)
end end
else else
raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
@ -90,13 +97,27 @@ module ActiveJob
GlobalID::Locator.locate hash[GLOBALID_KEY] GlobalID::Locator.locate hash[GLOBALID_KEY]
end end
def deserialize_hash(serialized_hash) def serialize_hash(argument)
serialized_hash.each_with_object({}.with_indifferent_access) do |(key, value), hash| argument.each_with_object({}) do |(key, value), hash|
hash[key] = deserialize_argument(value) hash[serialize_hash_key(key)] = serialize_argument(value)
end end
end end
RESERVED_KEYS = [GLOBALID_KEY, GLOBALID_KEY.to_sym] def deserialize_hash(serialized_hash)
result = serialized_hash.transform_values { |v| deserialize_argument(v) }
if result.delete(WITH_INDIFFERENT_ACCESS_KEY)
result = result.with_indifferent_access
elsif symbol_keys = result.delete(SYMBOL_KEYS_KEY)
result = transform_symbol_keys(result, symbol_keys)
end
result
end
RESERVED_KEYS = [
GLOBALID_KEY, GLOBALID_KEY.to_sym,
SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
]
private_constant :RESERVED_KEYS private_constant :RESERVED_KEYS
def serialize_hash_key(key) def serialize_hash_key(key)
@ -109,5 +130,15 @@ module ActiveJob
raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}")
end end
end end
def transform_symbol_keys(hash, symbol_keys)
hash.transform_keys do |key|
if symbol_keys.include?(key)
key.to_sym
else
key
end
end
end
end end
end end

View file

@ -2,6 +2,7 @@ require 'helper'
require 'active_job/arguments' require 'active_job/arguments'
require 'models/person' require 'models/person'
require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/indifferent_access'
require 'jobs/kwargs_job'
class ArgumentSerializationTest < ActiveSupport::TestCase class ArgumentSerializationTest < ActiveSupport::TestCase
setup do setup do
@ -31,16 +32,26 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end end
test 'should convert records to Global IDs' do test 'should convert records to Global IDs' do
assert_arguments_roundtrip [@person], ['_aj_globalid' => @person.to_gid.to_s] assert_arguments_roundtrip [@person]
end end
test 'should dive deep into arrays and hashes' do test 'should dive deep into arrays and hashes' do
assert_arguments_roundtrip [3, [@person]], [3, ['_aj_globalid' => @person.to_gid.to_s]] assert_arguments_roundtrip [3, [@person]]
assert_arguments_roundtrip [{ 'a' => @person }], [{ 'a' => { '_aj_globalid' => @person.to_gid.to_s }}.with_indifferent_access] assert_arguments_roundtrip [{ 'a' => @person }]
end end
test 'should stringify symbol hash keys' do test 'should maintain string and symbol keys' do
assert_equal [ 'a' => 1 ], ActiveJob::Arguments.serialize([ a: 1 ]) assert_arguments_roundtrip([a: 1, "b" => 2])
end
test 'should maintain hash with indifferent access' do
symbol_key = { a: 1 }
string_key = { 'a' => 1 }
indifferent_access = { a: 1 }.with_indifferent_access
assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([symbol_key]).first
assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([string_key]).first
assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first
end end
test 'should disallow non-string/symbol hash keys' do test 'should disallow non-string/symbol hash keys' do
@ -71,14 +82,22 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end end
end end
test 'allows for keyword arguments' do
KwargsJob.perform_later(argument: 2)
assert_equal "Job with argument: 2", JobBuffer.last_value
end
private private
def assert_arguments_unchanged(*args) def assert_arguments_unchanged(*args)
assert_arguments_roundtrip args, args assert_arguments_roundtrip args
end end
def assert_arguments_roundtrip(args, expected_serialized_args) def assert_arguments_roundtrip(args)
serialized = ActiveJob::Arguments.serialize(args) assert_equal args, perform_round_trip(args)
assert_equal expected_serialized_args, serialized end
assert_equal args, ActiveJob::Arguments.deserialize(serialized)
def perform_round_trip(args)
ActiveJob::Arguments.deserialize(ActiveJob::Arguments.serialize(args))
end end
end end

View file

@ -0,0 +1,7 @@
require_relative '../support/job_buffer'
class KwargsJob < ActiveJob::Base
def perform(argument: 1)
JobBuffer.add("Job with argument: #{argument}")
end
end