diff options
author | Rafael França <rafaelmfranca@gmail.com> | 2018-02-14 14:47:46 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-14 14:47:46 -0500 |
commit | fa9e791e014a650f5ea6a14b283fed9621fc83e2 (patch) | |
tree | 42d892b55c66fd57cc62b7c996b01e30746341c5 /activejob | |
parent | 57495086f0bb1315ea287f8d6bafad8fd48e18b2 (diff) | |
parent | 25a14bf2bde90224debee343ebfbb882c02b9588 (diff) | |
download | rails-fa9e791e014a650f5ea6a14b283fed9621fc83e2.tar.gz rails-fa9e791e014a650f5ea6a14b283fed9621fc83e2.tar.bz2 rails-fa9e791e014a650f5ea6a14b283fed9621fc83e2.zip |
Merge pull request #30941 from toptal/introduce-custom-serializers-to-activejob-arguments
Introduce custom serializers to ActiveJob arguments
Diffstat (limited to 'activejob')
-rw-r--r-- | activejob/CHANGELOG.md | 3 | ||||
-rw-r--r-- | activejob/lib/active_job.rb | 1 | ||||
-rw-r--r-- | activejob/lib/active_job/arguments.rb | 29 | ||||
-rw-r--r-- | activejob/lib/active_job/base.rb | 1 | ||||
-rw-r--r-- | activejob/lib/active_job/railtie.rb | 6 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers.rb | 62 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers/date_serializer.rb | 21 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers/date_time_serializer.rb | 21 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers/duration_serializer.rb | 24 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers/object_serializer.rb | 54 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers/symbol_serializer.rb | 21 | ||||
-rw-r--r-- | activejob/lib/active_job/serializers/time_serializer.rb | 21 | ||||
-rw-r--r-- | activejob/test/cases/argument_serialization_test.rb | 50 | ||||
-rw-r--r-- | activejob/test/cases/serializers_test.rb | 98 |
14 files changed, 400 insertions, 12 deletions
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index ec99b5d975..e4768eb3d4 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,6 @@ +* Add support to define custom argument serializers. + + *Evgenii Pecherkin*, *Rafael Mendonça França* Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 626abaa767..01fab4d918 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -33,6 +33,7 @@ module ActiveJob autoload :Base autoload :QueueAdapters + autoload :Serializers autoload :ConfiguredJob autoload :TestCase autoload :TestHelper diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index de11e7fcb1..e6ada163e8 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -44,13 +44,24 @@ module ActiveJob end private + # :nodoc: GLOBALID_KEY = "_aj_globalid".freeze # :nodoc: SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze # :nodoc: WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze - private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY + # :nodoc: + OBJECT_SERIALIZER_KEY = "_aj_serialized" + + # :nodoc: + RESERVED_KEYS = [ + GLOBALID_KEY, GLOBALID_KEY.to_sym, + SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym, + OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym, + WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym, + ] + private_constant :RESERVED_KEYS def serialize_argument(argument) case argument @@ -70,7 +81,7 @@ module ActiveJob result[SYMBOL_KEYS_KEY] = symbol_keys result else - raise SerializationError.new("Unsupported argument type: #{argument.class.name}") + Serializers.serialize(argument) end end @@ -85,6 +96,8 @@ module ActiveJob when Hash if serialized_global_id?(argument) deserialize_global_id argument + elsif custom_serialized?(argument) + Serializers.deserialize(argument) else deserialize_hash(argument) end @@ -101,6 +114,10 @@ module ActiveJob GlobalID::Locator.locate hash[GLOBALID_KEY] end + def custom_serialized?(hash) + hash.key?(OBJECT_SERIALIZER_KEY) + end + def serialize_hash(argument) argument.each_with_object({}) do |(key, value), hash| hash[serialize_hash_key(key)] = serialize_argument(value) @@ -117,14 +134,6 @@ module ActiveJob result end - # :nodoc: - 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 - def serialize_hash_key(key) case key when *RESERVED_KEYS diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index ae112abb2c..6194f89956 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -59,6 +59,7 @@ module ActiveJob #:nodoc: # * SerializationError - Error class for serialization errors. class Base include Core + include Serializers include QueueAdapter include QueueName include QueuePriority diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb index 7b0742a6d2..427ad1e3af 100644 --- a/activejob/lib/active_job/railtie.rb +++ b/activejob/lib/active_job/railtie.rb @@ -7,11 +7,17 @@ module ActiveJob # = Active Job Railtie class Railtie < Rails::Railtie # :nodoc: config.active_job = ActiveSupport::OrderedOptions.new + config.active_job.custom_serializers = [] initializer "active_job.logger" do ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger } end + initializer "active_job.custom_serializers" do |app| + custom_serializers = app.config.active_job.delete(:custom_serializers) + ActiveJob::Serializers.add_serializers custom_serializers + end + initializer "active_job.set_configs" do |app| options = app.config.active_job options.queue_adapter ||= :async diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb new file mode 100644 index 0000000000..d9a130fa73 --- /dev/null +++ b/activejob/lib/active_job/serializers.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "set" + +module ActiveJob + # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers + # and to add new ones. It also has helpers to serialize/deserialize objects + module Serializers + extend ActiveSupport::Autoload + extend ActiveSupport::Concern + + autoload :ObjectSerializer + autoload :SymbolSerializer + autoload :DurationSerializer + autoload :DateSerializer + autoload :TimeSerializer + autoload :DateTimeSerializer + + mattr_accessor :_additional_serializers + self._additional_serializers = Set.new + + class << self + # Returns serialized representative of the passed object. + # Will look up through all known serializers. + # Raises `ActiveJob::SerializationError` if it can't find a proper serializer. + def serialize(argument) + serializer = serializers.detect { |s| s.serialize?(argument) } + raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer + serializer.serialize(argument) + end + + # Returns deserialized object. + # Will look up through all known serializers. + # If no serializers found will raise `ArgumentError` + def deserialize(argument) + serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY] + raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name + + serializer = serializer_name.safe_constantize + raise ArgumentError, "Serializer #{serializer_name} is not know" unless serializer + + serializer.deserialize(argument) + end + + # Returns list of known serializers + def serializers + self._additional_serializers + end + + # Adds a new serializer to a list of known serializers + def add_serializers(*new_serializers) + self._additional_serializers += new_serializers + end + end + + add_serializers SymbolSerializer, + DurationSerializer, + DateTimeSerializer, + DateSerializer, + TimeSerializer + end +end diff --git a/activejob/lib/active_job/serializers/date_serializer.rb b/activejob/lib/active_job/serializers/date_serializer.rb new file mode 100644 index 0000000000..e995d30faa --- /dev/null +++ b/activejob/lib/active_job/serializers/date_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class DateSerializer < ObjectSerializer # :nodoc: + def serialize(date) + super("value" => date.iso8601) + end + + def deserialize(hash) + Date.iso8601(hash["value"]) + end + + private + + def klass + Date + end + end + end +end diff --git a/activejob/lib/active_job/serializers/date_time_serializer.rb b/activejob/lib/active_job/serializers/date_time_serializer.rb new file mode 100644 index 0000000000..fe780a1978 --- /dev/null +++ b/activejob/lib/active_job/serializers/date_time_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class DateTimeSerializer < ObjectSerializer # :nodoc: + def serialize(time) + super("value" => time.iso8601) + end + + def deserialize(hash) + DateTime.iso8601(hash["value"]) + end + + private + + def klass + DateTime + end + end + end +end diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb new file mode 100644 index 0000000000..715fe27a5c --- /dev/null +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class DurationSerializer < ObjectSerializer # :nodoc: + def serialize(duration) + super("value" => duration.value, "parts" => Arguments.serialize(duration.parts)) + end + + def deserialize(hash) + value = hash["value"] + parts = Arguments.deserialize(hash["parts"]) + + klass.new(value, parts) + end + + private + + def klass + ActiveSupport::Duration + end + end + end +end diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb new file mode 100644 index 0000000000..9f59e8236f --- /dev/null +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Base class for serializing and deserializing custom times. + # + # Example + # + # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer + # def serialize(money) + # super("cents" => money.cents, "currency" => money.currency) + # end + # + # def deserialize(hash) + # Money.new(hash["cents"], hash["currency"]) + # end + # + # private + # + # def klass + # Money + # end + # end + class ObjectSerializer + include Singleton + + class << self + delegate :serialize?, :serialize, :deserialize, to: :instance + end + + # Determines if an argument should be serialized by a serializer. + def serialize?(argument) + argument.is_a?(klass) + end + + # Serializes an argument to a JSON primitive type. + def serialize(hash) + { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) + end + + # Deserilizes an argument form a JSON primiteve type. + def deserialize(_argument) + raise NotImplementedError + end + + protected + + # The class of the object that will be serialized. + def klass + raise NotImplementedError + end + end + end +end diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb new file mode 100644 index 0000000000..7e1f9553a2 --- /dev/null +++ b/activejob/lib/active_job/serializers/symbol_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class SymbolSerializer < ObjectSerializer # :nodoc: + def serialize(argument) + super("value" => argument.to_s) + end + + def deserialize(argument) + argument["value"].to_sym + end + + private + + def klass + Symbol + end + end + end +end diff --git a/activejob/lib/active_job/serializers/time_serializer.rb b/activejob/lib/active_job/serializers/time_serializer.rb new file mode 100644 index 0000000000..fe20772f35 --- /dev/null +++ b/activejob/lib/active_job/serializers/time_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class TimeSerializer < ObjectSerializer # :nodoc: + def serialize(time) + super("value" => time.iso8601) + end + + def deserialize(hash) + Time.iso8601(hash["value"]) + end + + private + + def klass + Time + end + end + end +end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 7e7f854da0..ff6ac6fc43 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -12,7 +12,10 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, - "a", true, false, BigDecimal(5), + "a", true, false, BigDecimal.new(5), + :a, 1.day, Date.new(2001, 2, 3), Time.new(2002, 10, 31, 2, 2, 2, "+02:00"), + DateTime.new(2001, 2, 3, 4, 5, 6, "+03:00"), + ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]), [ 1, "a" ], { "a" => 1 } ].each do |arg| @@ -21,7 +24,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end - [ :a, Object.new, self, Person.find("5").to_gid ].each do |arg| + [ Object.new, self, Person.find("5").to_gid ].each do |arg| test "does not serialize #{arg.class}" do assert_raises ActiveJob::SerializationError do ActiveJob::Arguments.serialize [ arg ] @@ -46,6 +49,49 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_arguments_roundtrip([a: 1, "b" => 2]) end + test "serialize a hash" do + symbol_key = { a: 1 } + string_key = { "a" => 1 } + indifferent_access = { a: 1 }.with_indifferent_access + + assert_equal( + { "a" => 1, "_aj_symbol_keys" => ["a"] }, + ActiveJob::Arguments.serialize([symbol_key]).first + ) + assert_equal( + { "a" => 1, "_aj_symbol_keys" => [] }, + ActiveJob::Arguments.serialize([string_key]).first + ) + assert_equal( + { "a" => 1, "_aj_hash_with_indifferent_access" => true }, + ActiveJob::Arguments.serialize([indifferent_access]).first + ) + end + + test "deserialize a hash" do + symbol_key = { "a" => 1, "_aj_symbol_keys" => ["a"] } + string_key = { "a" => 1, "_aj_symbol_keys" => [] } + another_string_key = { "a" => 1 } + indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true } + + assert_equal( + { a: 1 }, + ActiveJob::Arguments.deserialize([symbol_key]).first + ) + assert_equal( + { "a" => 1 }, + ActiveJob::Arguments.deserialize([string_key]).first + ) + assert_equal( + { "a" => 1 }, + ActiveJob::Arguments.deserialize([another_string_key]).first + ) + assert_equal( + { "a" => 1 }, + ActiveJob::Arguments.deserialize([indifferent_access]).first + ) + end + test "should maintain hash with indifferent access" do symbol_key = { a: 1 } string_key = { "a" => 1 } diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb new file mode 100644 index 0000000000..a86f168d03 --- /dev/null +++ b/activejob/test/cases/serializers_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "helper" +require "active_job/serializers" + +class SerializersTest < ActiveSupport::TestCase + class DummyValueObject + attr_accessor :value + + def initialize(value) + @value = value + end + + def ==(other) + self.value == other.value + end + end + + class DummySerializer < ActiveJob::Serializers::ObjectSerializer + def serialize(object) + super({ "value" => object.value }) + end + + def deserialize(hash) + DummyValueObject.new(hash["value"]) + end + + private + + def klass + DummyValueObject + end + end + + setup do + @value_object = DummyValueObject.new 123 + @original_serializers = ActiveJob::Serializers.serializers + end + + teardown do + ActiveJob::Serializers._additional_serializers = @original_serializers + end + + test "can't serialize unknown object" do + assert_raises ActiveJob::SerializationError do + ActiveJob::Serializers.serialize @value_object + end + end + + test "will serialize objects with serializers registered" do + ActiveJob::Serializers.add_serializers DummySerializer + + assert_equal( + { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 }, + ActiveJob::Serializers.serialize(@value_object) + ) + end + + test "won't deserialize unknown hash" do + hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] } + error = assert_raises(ArgumentError) do + ActiveJob::Serializers.deserialize(hash) + end + assert_equal( + 'Serializer name is not present in the argument: {"_dummy_serializer"=>123, "_aj_symbol_keys"=>[]}', + error.message + ) + end + + test "won't deserialize unknown serializer" do + hash = { "_aj_serialized" => "DoNotExist", "value" => 123 } + error = assert_raises(ArgumentError) do + ActiveJob::Serializers.deserialize(hash) + end + assert_equal( + "Serializer DoNotExist is not know", + error.message + ) + end + + test "will deserialize know serialized objects" do + ActiveJob::Serializers.add_serializers DummySerializer + hash = { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 } + assert_equal DummyValueObject.new(123), ActiveJob::Serializers.deserialize(hash) + end + + test "adds new serializer" do + ActiveJob::Serializers.add_serializers DummySerializer + assert ActiveJob::Serializers.serializers.include?(DummySerializer) + end + + test "can't add serializer with the same key twice" do + ActiveJob::Serializers.add_serializers DummySerializer + assert_no_difference(-> { ActiveJob::Serializers.serializers.size }) do + ActiveJob::Serializers.add_serializers DummySerializer + end + end +end |