From e360ac12315ed6b9eadca5bcc0d95dc766ba8523 Mon Sep 17 00:00:00 2001 From: Evgenii Pecherkin Date: Tue, 17 Oct 2017 16:05:05 +0400 Subject: Introduce serializers to ActiveJob --- activejob/README.md | 62 +++++++++- activejob/lib/active_job.rb | 1 + activejob/lib/active_job/arguments.rb | 132 +-------------------- activejob/lib/active_job/base.rb | 2 + activejob/lib/active_job/serializers.rb | 109 +++++++++++++++++ .../lib/active_job/serializers/array_serializer.rb | 26 ++++ .../lib/active_job/serializers/base_serializer.rb | 13 ++ .../lib/active_job/serializers/class_serializer.rb | 24 ++++ .../active_job/serializers/duration_serializer.rb | 42 +++++++ .../active_job/serializers/global_id_serializer.rb | 32 +++++ .../lib/active_job/serializers/hash_serializer.rb | 62 ++++++++++ .../hash_with_indifferent_access_serializer.rb | 37 ++++++ .../active_job/serializers/object_serializer.rb | 27 +++++ .../serializers/standard_type_serializer.rb | 26 ++++ .../active_job/serializers/struct_serializer.rb | 38 ++++++ .../active_job/serializers/symbol_serializer.rb | 28 +++++ .../lib/rails/generators/job/job_generator.rb | 2 +- .../test/cases/argument_serialization_test.rb | 10 +- guides/source/active_job_basics.md | 65 +++++++++- 19 files changed, 602 insertions(+), 136 deletions(-) create mode 100644 activejob/lib/active_job/serializers.rb create mode 100644 activejob/lib/active_job/serializers/array_serializer.rb create mode 100644 activejob/lib/active_job/serializers/base_serializer.rb create mode 100644 activejob/lib/active_job/serializers/class_serializer.rb create mode 100644 activejob/lib/active_job/serializers/duration_serializer.rb create mode 100644 activejob/lib/active_job/serializers/global_id_serializer.rb create mode 100644 activejob/lib/active_job/serializers/hash_serializer.rb create mode 100644 activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb create mode 100644 activejob/lib/active_job/serializers/object_serializer.rb create mode 100644 activejob/lib/active_job/serializers/standard_type_serializer.rb create mode 100644 activejob/lib/active_job/serializers/struct_serializer.rb create mode 100644 activejob/lib/active_job/serializers/symbol_serializer.rb diff --git a/activejob/README.md b/activejob/README.md index f1ebb76e08..152f924525 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -52,8 +52,21 @@ MyJob.set(wait: 1.week).perform_later(record) # Enqueue a job to be performed 1 That's it! +## Supported types for arguments -## GlobalID support +ActiveJob supports the following types of arguments by default: + + - Standard types (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) + - `Symbol` (`:foo`, `:bar`, ...) + - `ActiveSupport::Duration` (`1.day`, `2.weeks`, ...) + - Classes constants (`ActiveRecord::Base`, `MySpecialService`, ...) + - Struct instances (`Struct.new('Rectangle', :width, :height).new(12, 20)`, ...) + - `Hash`. Keys should be of `String` or `Symbol` type + - `ActiveSupport::HashWithIndifferentAccess` + - `Array` + + +### GlobalID support Active Job supports [GlobalID serialization](https://github.com/rails/globalid/) for parameters. This makes it possible to pass live Active Record objects to your job instead of class/id pairs, which @@ -81,6 +94,53 @@ end This works with any class that mixes in GlobalID::Identification, which by default has been mixed into Active Record classes. +### Serializers + +You can extend list of supported types for arguments. You just need to define your own serializer. + +```ruby +class MySpecialSerializer + class << self + # Check if this object should be serialized using this serializer + def serialize?(argument) + object.is_a? MySpecialValueObject + end + + # Convert an object to a simpler representative using supported object types. + # The recommended representative is a Hash with a specific key. Keys can be of basic types only + def serialize(object) + { + key => ActiveJob::Serializers.serialize(object.value) + 'another_attribute' => ActiveJob::Serializers.serialize(object.another_attribute) + } + end + + # Check if this serialized value be deserialized using this serializer + def deserialize?(argument) + object.is_a?(Hash) && object.keys == [key, 'another_attribute'] + end + + # Convert serialized value into a proper object + def deserialize(object) + value = ActiveJob::Serializers.deserialize(object[key]) + another_attribute = ActiveJob::Serializers.deserialize(object['another_attribute']) + MySpecialValueObject.new value, another_attribute + end + + # Define this method if you are using a hash as a representative. + # This key will be added to a list of restricted keys for hashes. Use basic types only + def key + "_aj_custom_dummy_value_object" + end + end +end +``` + +And now you just need to add this serializer to a list: + +```ruby +ActiveJob::Base.add_serializers(MySpecialSerializer) +``` ## Supported queueing systems 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..9d47131864 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -3,24 +3,6 @@ require "active_support/core_ext/hash" module ActiveJob - # Raised when an exception is raised during job arguments deserialization. - # - # Wraps the original exception raised as +cause+. - class DeserializationError < StandardError - def initialize #:nodoc: - super("Error while trying to deserialize arguments: #{$!.message}") - set_backtrace $!.backtrace - end - end - - # Raised when an unsupported argument type is set as a job argument. We - # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass, - # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). - # Raised if you set the key for a Hash something else than a string or - # a symbol. Also raised when trying to serialize an object which can't be - # identified with a Global ID - such as an unpersisted Active Record model. - class SerializationError < ArgumentError; end - module Arguments extend self # :nodoc: @@ -31,126 +13,16 @@ module ActiveJob # as-is. Arrays/Hashes are serialized element by element. # All other types are serialized using GlobalID. def serialize(arguments) - arguments.map { |argument| serialize_argument(argument) } + ActiveJob::Serializers.serialize(arguments) end # Deserializes a set of arguments. Whitelisted types are returned # as-is. Arrays/Hashes are deserialized element by element. # All other types are deserialized using GlobalID. def deserialize(arguments) - arguments.map { |argument| deserialize_argument(argument) } + ActiveJob::Serializers.deserialize(arguments) rescue raise DeserializationError 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 - - def serialize_argument(argument) - case argument - when *TYPE_WHITELIST - argument - when GlobalID::Identification - convert_to_global_id_hash(argument) - when Array - argument.map { |arg| serialize_argument(arg) } - when ActiveSupport::HashWithIndifferentAccess - result = serialize_hash(argument) - result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true) - result - when Hash - symbol_keys = argument.each_key.grep(Symbol).map(&:to_s) - result = serialize_hash(argument) - result[SYMBOL_KEYS_KEY] = symbol_keys - result - else - raise SerializationError.new("Unsupported argument type: #{argument.class.name}") - end - end - - def deserialize_argument(argument) - case argument - when String - GlobalID::Locator.locate(argument) || argument - when *TYPE_WHITELIST - argument - when Array - argument.map { |arg| deserialize_argument(arg) } - when Hash - if serialized_global_id?(argument) - deserialize_global_id argument - else - deserialize_hash(argument) - end - else - raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" - end - end - - def serialized_global_id?(hash) - hash.size == 1 && hash.include?(GLOBALID_KEY) - end - - def deserialize_global_id(hash) - GlobalID::Locator.locate hash[GLOBALID_KEY] - end - - def serialize_hash(argument) - argument.each_with_object({}) do |(key, value), hash| - hash[serialize_hash_key(key)] = serialize_argument(value) - end - end - - 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 - - # :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 - raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") - when String, Symbol - key.to_s - else - raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") - 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 - - def convert_to_global_id_hash(argument) - { GLOBALID_KEY => argument.to_global_id.to_s } - rescue URI::GID::MissingModelIdError - raise SerializationError, "Unable to serialize #{argument.class} " \ - "without an id. (Maybe you forgot to call save?)" - end end end diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index ae112abb2c..8275776820 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_job/core" +require "active_job/serializers" require "active_job/queue_adapter" require "active_job/queue_name" require "active_job/queue_priority" @@ -59,6 +60,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/serializers.rb b/activejob/lib/active_job/serializers.rb new file mode 100644 index 0000000000..ec86065149 --- /dev/null +++ b/activejob/lib/active_job/serializers.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module ActiveJob + # Raised when an exception is raised during job arguments deserialization. + # + # Wraps the original exception raised as +cause+. + class DeserializationError < StandardError + def initialize #:nodoc: + super("Error while trying to deserialize arguments: #{$!.message}") + set_backtrace $!.backtrace + end + end + + # Raised when an unsupported argument type is set as a job argument. We + # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass, + # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). + # Raised if you set the key for a Hash something else than a string or + # a symbol. Also raised when trying to serialize an object which can't be + # identified with a Global ID - such as an unpersisted Active Record model. + class SerializationError < ArgumentError; end + + # The ActiveJob::Serializers 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 :ArraySerializer + autoload :BaseSerializer + autoload :ClassSerializer + autoload :DurationSerializer + autoload :GlobalIDSerializer + autoload :HashWithIndifferentAccessSerializer + autoload :HashSerializer + autoload :ObjectSerializer + autoload :StandardTypeSerializer + autoload :StructSerializer + autoload :SymbolSerializer + + included do + class_attribute :_additional_serializers, instance_accessor: false, instance_predicate: false + self._additional_serializers = [] + end + + # Includes the method to list known serializers and to add new ones + module ClassMethods + # Returns list of known serializers + def serializers + self._additional_serializers + SERIALIZERS + end + + # Adds a new serializer to a list of known serializers + def add_serializers(*serializers) + check_duplicate_serializer_keys!(serializers) + + @_additional_serializers = serializers + @_additional_serializers + end + + # Returns a list of reserved keys, which cannot be used as keys for a hash + def reserved_serializers_keys + serializers.select { |s| s.respond_to?(:key) }.map(&:key) + end + + private + + def check_duplicate_serializer_keys!(serializers) + keys_to_add = serializers.select { |s| s.respond_to?(:key) }.map(&:key) + + duplicate_keys = reserved_keys & keys_to_add + + raise ArgumentError.new("Can't add serializers because of keys duplication: #{duplicate_keys}") if duplicate_keys.any? + end + end + + # :nodoc: + SERIALIZERS = [ + ::ActiveJob::Serializers::GlobalIDSerializer, + ::ActiveJob::Serializers::DurationSerializer, + ::ActiveJob::Serializers::StructSerializer, + ::ActiveJob::Serializers::SymbolSerializer, + ::ActiveJob::Serializers::ClassSerializer, + ::ActiveJob::Serializers::StandardTypeSerializer, + ::ActiveJob::Serializers::HashWithIndifferentAccessSerializer, + ::ActiveJob::Serializers::HashSerializer, + ::ActiveJob::Serializers::ArraySerializer + ].freeze + private_constant :SERIALIZERS + + class << self + # Returns serialized representative of the passed object. + # Will look up through all known serializers. + # Raises `SerializationError` if it can't find a proper serializer. + def serialize(argument) + serializer = ::ActiveJob::Base.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 = ::ActiveJob::Base.serializers.detect { |s| s.deserialize?(argument) } + raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" unless serializer + serializer.deserialize(argument) + end + end + end +end diff --git a/activejob/lib/active_job/serializers/array_serializer.rb b/activejob/lib/active_job/serializers/array_serializer.rb new file mode 100644 index 0000000000..f0254f4488 --- /dev/null +++ b/activejob/lib/active_job/serializers/array_serializer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize `Array` + class ArraySerializer < BaseSerializer + class << self + alias_method :deserialize?, :serialize? + + def serialize(array) + array.map { |arg| ::ActiveJob::Serializers.serialize(arg) } + end + + def deserialize(array) + array.map { |arg| ::ActiveJob::Serializers.deserialize(arg) } + end + + private + + def klass + ::Array + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/base_serializer.rb b/activejob/lib/active_job/serializers/base_serializer.rb new file mode 100644 index 0000000000..98f7852fd6 --- /dev/null +++ b/activejob/lib/active_job/serializers/base_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class BaseSerializer + class << self + def serialize?(argument) + argument.is_a?(klass) + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/class_serializer.rb b/activejob/lib/active_job/serializers/class_serializer.rb new file mode 100644 index 0000000000..d36e8c0ebc --- /dev/null +++ b/activejob/lib/active_job/serializers/class_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize `Class` (`ActiveRecord::Base`, `MySpecialService`, ...) + class ClassSerializer < ObjectSerializer + class << self + def serialize(argument_klass) + { key => "::#{argument_klass.name}" } + end + + def key + "_aj_class" + end + + private + + def klass + ::Class + end + 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..72b7b9528a --- /dev/null +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize `ActiveSupport::Duration` (`1.day`, `2.weeks`, ...) + class DurationSerializer < ObjectSerializer + class << self + def serialize(duration) + { + key => duration.value, + parts_key => ::ActiveJob::Serializers.serialize(duration.parts) + } + end + + def deserialize(hash) + value = hash[key] + parts = ::ActiveJob::Serializers.deserialize(hash[parts_key]) + + klass.new(value, parts) + end + + def key + "_aj_activesupport_duration" + end + + private + + def klass + ::ActiveSupport::Duration + end + + def keys + super.push parts_key + end + + def parts_key + "parts" + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/global_id_serializer.rb b/activejob/lib/active_job/serializers/global_id_serializer.rb new file mode 100644 index 0000000000..1961e43fca --- /dev/null +++ b/activejob/lib/active_job/serializers/global_id_serializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize objects which mixes `GlobalID::Identification`, + # including `ActiveRecord::Base` models + class GlobalIDSerializer < ObjectSerializer + class << self + def serialize(object) + { key => object.to_global_id.to_s } + rescue URI::GID::MissingModelIdError + raise SerializationError, "Unable to serialize #{object.class} " \ + "without an id. (Maybe you forgot to call save?)" + end + + def deserialize(hash) + GlobalID::Locator.locate(hash[key]) + end + + def key + "_aj_globalid" + end + + private + + def klass + ::GlobalID::Identification + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/hash_serializer.rb b/activejob/lib/active_job/serializers/hash_serializer.rb new file mode 100644 index 0000000000..eee081de7c --- /dev/null +++ b/activejob/lib/active_job/serializers/hash_serializer.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize `Hash` (`{key: field, ...}`) + # Only `String` or `Symbol` can be used as a key. Values will be serialized by known serializers + class HashSerializer < BaseSerializer + class << self + def serialize(hash) + symbol_keys = hash.each_key.grep(Symbol).map(&:to_s) + result = serialize_hash(hash) + result[key] = symbol_keys + result + end + + def deserialize?(argument) + argument.is_a?(Hash) && argument[key] + end + + def deserialize(hash) + result = hash.transform_values { |v| ::ActiveJob::Serializers::deserialize(v) } + symbol_keys = result.delete(key) + transform_symbol_keys(result, symbol_keys) + end + + def key + "_aj_symbol_keys" + end + + private + + def serialize_hash(hash) + hash.each_with_object({}) do |(key, value), result| + result[serialize_hash_key(key)] = ::ActiveJob::Serializers.serialize(value) + end + end + + def serialize_hash_key(key) + raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") unless [String, Symbol].include?(key.class) + + raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") if ActiveJob::Base.reserved_serializers_keys.include?(key.to_s) + + key.to_s + 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 + + def klass + ::Hash + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb new file mode 100644 index 0000000000..50e80757cd --- /dev/null +++ b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize `ActiveSupport::HashWithIndifferentAccess` + # Values will be serialized by known serializers + class HashWithIndifferentAccessSerializer < HashSerializer + class << self + def serialize(hash) + result = serialize_hash(hash) + result[key] = ::ActiveJob::Serializers.serialize(true) + result + end + + def deserialize?(argument) + argument.is_a?(Hash) && argument[key] + end + + def deserialize(hash) + result = hash.transform_values { |v| ::ActiveJob::Serializers.deserialize(v) } + result.delete(key) + result.with_indifferent_access + end + + def key + "_aj_hash_with_indifferent_access" + end + + private + + def klass + ::ActiveSupport::HashWithIndifferentAccess + end + 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..075360b26e --- /dev/null +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + class ObjectSerializer < BaseSerializer + class << self + def serialize(object) + { key => object.class.name } + end + + def deserialize?(argument) + argument.respond_to?(:keys) && argument.keys == keys + end + + def deserialize(hash) + hash[key].constantize + end + + private + + def keys + [key] + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/standard_type_serializer.rb b/activejob/lib/active_job/serializers/standard_type_serializer.rb new file mode 100644 index 0000000000..8969b31d6b --- /dev/null +++ b/activejob/lib/active_job/serializers/standard_type_serializer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize standard types + # (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) + class StandardTypeSerializer < BaseSerializer + class << self + def serialize?(argument) + ::ActiveJob::Arguments::TYPE_WHITELIST.include? argument.class + end + + def serialize(argument) + argument + end + + alias_method :deserialize?, :serialize? + + def deserialize(argument) + object = GlobalID::Locator.locate(argument) if argument.is_a? String + object || argument + end + end + end + end +end diff --git a/activejob/lib/active_job/serializers/struct_serializer.rb b/activejob/lib/active_job/serializers/struct_serializer.rb new file mode 100644 index 0000000000..f6791611ed --- /dev/null +++ b/activejob/lib/active_job/serializers/struct_serializer.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize struct instances + # (`Struct.new('Rectangle', :width, :height).new(12, 20)`) + class StructSerializer < ObjectSerializer + class << self + def serialize(object) + super.merge values_key => ::ActiveJob::Serializers.serialize(object.values) + end + + def deserialize(hash) + values = ::ActiveJob::Serializers.deserialize(hash[values_key]) + super.new(*values) + end + + def key + "_aj_struct" + end + + private + + def klass + ::Struct + end + + def keys + super.push values_key + end + + def values_key + "values" + end + 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..f128ae8284 --- /dev/null +++ b/activejob/lib/active_job/serializers/symbol_serializer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module ActiveJob + module Serializers + # Provides methods to serialize and deserialize `Symbol` (`:foo`, `:bar`, ...) + class SymbolSerializer < ObjectSerializer + class << self + def serialize(symbol) + { key => symbol.to_s } + end + + def deserialize(hash) + hash[key].to_sym + end + + def key + "_aj_symbol" + end + + private + + def klass + ::Symbol + end + end + end + end +end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 69b4fe7d26..c940cd154c 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -30,7 +30,7 @@ module Rails # :nodoc: private def application_job_file_name @application_job_file_name ||= if mountable_engine? - "app/jobs/#{namespaced_path}/application_job.rb" + "app/jobs/#{namespaced_path}/application_job.rb" else "app/jobs/application_job.rb" end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 7e7f854da0..20296038a0 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -4,6 +4,7 @@ require "helper" require "active_job/arguments" require "models/person" require "active_support/core_ext/hash/indifferent_access" +require "active_support/duration" require "jobs/kwargs_job" class ArgumentSerializationTest < ActiveSupport::TestCase @@ -12,7 +13,8 @@ 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, self, 1.day, [ 1, "a" ], { "a" => 1 } ].each do |arg| @@ -21,7 +23,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end - [ :a, Object.new, self, Person.find("5").to_gid ].each do |arg| + [ Object.new, Person.find("5").to_gid ].each do |arg| test "does not serialize #{arg.class}" do assert_raises ActiveJob::SerializationError do ActiveJob::Arguments.serialize [ arg ] @@ -33,6 +35,10 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end + test "serializes Struct" do + assert_arguments_unchanged Struct.new("Rectangle", :width, :height).new(10, 15) + end + test "should convert records to Global IDs" do assert_arguments_roundtrip [@person] end diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 914ef2c327..a7067cb97d 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -339,8 +339,21 @@ UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto. ``` -GlobalID --------- +Supported types for arguments +---------------------------- + +ActiveJob supports the following types of arguments by default: + + - Basic types (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) + - `Symbol` (`:foo`, `:bar`, ...) + - `ActiveSupport::Duration` (`1.day`, `2.weeks`, ...) + - Classes constants (`ActiveRecord::Base`, `MySpecialService`, ...) + - Struct instances (`Struct.new('Rectangle', :width, :height).new(12, 20)`, ...) + - `Hash`. Keys should be of `String` or `Symbol` type + - `ActiveSupport::HashWithIndifferentAccess` + - `Array` + +### GlobalID Active Job supports GlobalID for parameters. This makes it possible to pass live Active Record objects to your job instead of class/id pairs, which you then have @@ -368,6 +381,54 @@ end This works with any class that mixes in `GlobalID::Identification`, which by default has been mixed into Active Record classes. +### Serializers + +You can extend list of supported types for arguments. You just need to define your own serializer. + +```ruby +class MySpecialSerializer + class << self + # Check if this object should be serialized using this serializer + def serialize?(argument) + argument.is_a? MySpecialValueObject + end + + # Convert an object to a simpler representative using supported object types. + # The recommended representative is a Hash with a specific key. Keys can be of basic types only + def serialize(object) + { + key => ActiveJob::Serializers.serialize(object.value) + 'another_attribute' => ActiveJob::Serializers.serialize(object.another_attribute) + } + end + + # Check if this serialized value be deserialized using this serializer + def deserialize?(argument) + argument.is_a?(Hash) && argument.keys == [key, 'another_attribute'] + end + + # Convert serialized value into a proper object + def deserialize(object) + value = ActiveJob::Serializers.deserialize(object[key]) + another_attribute = ActiveJob::Serializers.deserialize(object['another_attribute']) + MySpecialValueObject.new value, another_attribute + end + + # Define this method if you are using a hash as a representative. + # This key will be added to a list of restricted keys for hashes. Use basic types only + def key + "_aj_custom_dummy_value_object" + end + end +end +``` + +And now you just need to add this serializer to a list: + +```ruby +ActiveJob::Base.add_serializers(MySpecialSerializer) +``` + Exceptions ---------- -- cgit v1.2.3 From 3785a5729959a838bb13f2d298a59e12e1844f74 Mon Sep 17 00:00:00 2001 From: Evgenii Pecherkin Date: Mon, 23 Oct 2017 17:29:28 +0400 Subject: Remove non-default serializers --- activejob/README.md | 4 -- activejob/lib/active_job/serializers.rb | 21 ++----- .../lib/active_job/serializers/class_serializer.rb | 24 -------- .../active_job/serializers/duration_serializer.rb | 42 -------------- .../active_job/serializers/struct_serializer.rb | 38 ------------- .../active_job/serializers/symbol_serializer.rb | 28 ---------- .../lib/rails/generators/job/job_generator.rb | 2 +- .../test/cases/argument_serialization_test.rb | 8 +-- activejob/test/cases/serializers_test.rb | 64 ++++++++++++++++++++++ guides/source/active_job_basics.md | 4 -- 10 files changed, 72 insertions(+), 163 deletions(-) delete mode 100644 activejob/lib/active_job/serializers/class_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/duration_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/struct_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/symbol_serializer.rb create mode 100644 activejob/test/cases/serializers_test.rb diff --git a/activejob/README.md b/activejob/README.md index 152f924525..56562d870b 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -57,10 +57,6 @@ That's it! ActiveJob supports the following types of arguments by default: - Standard types (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) - - `Symbol` (`:foo`, `:bar`, ...) - - `ActiveSupport::Duration` (`1.day`, `2.weeks`, ...) - - Classes constants (`ActiveRecord::Base`, `MySpecialService`, ...) - - Struct instances (`Struct.new('Rectangle', :width, :height).new(12, 20)`, ...) - `Hash`. Keys should be of `String` or `Symbol` type - `ActiveSupport::HashWithIndifferentAccess` - `Array` diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index ec86065149..68ed94896e 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -27,15 +27,11 @@ module ActiveJob autoload :ArraySerializer autoload :BaseSerializer - autoload :ClassSerializer - autoload :DurationSerializer autoload :GlobalIDSerializer autoload :HashWithIndifferentAccessSerializer autoload :HashSerializer autoload :ObjectSerializer autoload :StandardTypeSerializer - autoload :StructSerializer - autoload :SymbolSerializer included do class_attribute :_additional_serializers, instance_accessor: false, instance_predicate: false @@ -46,14 +42,14 @@ module ActiveJob module ClassMethods # Returns list of known serializers def serializers - self._additional_serializers + SERIALIZERS + self._additional_serializers + ActiveJob::Serializers::SERIALIZERS end # Adds a new serializer to a list of known serializers - def add_serializers(*serializers) - check_duplicate_serializer_keys!(serializers) + def add_serializers(*new_serializers) + check_duplicate_serializer_keys!(new_serializers) - @_additional_serializers = serializers + @_additional_serializers + self._additional_serializers = new_serializers + self._additional_serializers end # Returns a list of reserved keys, which cannot be used as keys for a hash @@ -66,7 +62,7 @@ module ActiveJob def check_duplicate_serializer_keys!(serializers) keys_to_add = serializers.select { |s| s.respond_to?(:key) }.map(&:key) - duplicate_keys = reserved_keys & keys_to_add + duplicate_keys = reserved_serializers_keys & keys_to_add raise ArgumentError.new("Can't add serializers because of keys duplication: #{duplicate_keys}") if duplicate_keys.any? end @@ -75,21 +71,16 @@ module ActiveJob # :nodoc: SERIALIZERS = [ ::ActiveJob::Serializers::GlobalIDSerializer, - ::ActiveJob::Serializers::DurationSerializer, - ::ActiveJob::Serializers::StructSerializer, - ::ActiveJob::Serializers::SymbolSerializer, - ::ActiveJob::Serializers::ClassSerializer, ::ActiveJob::Serializers::StandardTypeSerializer, ::ActiveJob::Serializers::HashWithIndifferentAccessSerializer, ::ActiveJob::Serializers::HashSerializer, ::ActiveJob::Serializers::ArraySerializer ].freeze - private_constant :SERIALIZERS class << self # Returns serialized representative of the passed object. # Will look up through all known serializers. - # Raises `SerializationError` if it can't find a proper serializer. + # Raises `ActiveJob::SerializationError` if it can't find a proper serializer. def serialize(argument) serializer = ::ActiveJob::Base.serializers.detect { |s| s.serialize?(argument) } raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer diff --git a/activejob/lib/active_job/serializers/class_serializer.rb b/activejob/lib/active_job/serializers/class_serializer.rb deleted file mode 100644 index d36e8c0ebc..0000000000 --- a/activejob/lib/active_job/serializers/class_serializer.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize `Class` (`ActiveRecord::Base`, `MySpecialService`, ...) - class ClassSerializer < ObjectSerializer - class << self - def serialize(argument_klass) - { key => "::#{argument_klass.name}" } - end - - def key - "_aj_class" - end - - private - - def klass - ::Class - end - end - end - end -end diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb deleted file mode 100644 index 72b7b9528a..0000000000 --- a/activejob/lib/active_job/serializers/duration_serializer.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize `ActiveSupport::Duration` (`1.day`, `2.weeks`, ...) - class DurationSerializer < ObjectSerializer - class << self - def serialize(duration) - { - key => duration.value, - parts_key => ::ActiveJob::Serializers.serialize(duration.parts) - } - end - - def deserialize(hash) - value = hash[key] - parts = ::ActiveJob::Serializers.deserialize(hash[parts_key]) - - klass.new(value, parts) - end - - def key - "_aj_activesupport_duration" - end - - private - - def klass - ::ActiveSupport::Duration - end - - def keys - super.push parts_key - end - - def parts_key - "parts" - end - end - end - end -end diff --git a/activejob/lib/active_job/serializers/struct_serializer.rb b/activejob/lib/active_job/serializers/struct_serializer.rb deleted file mode 100644 index f6791611ed..0000000000 --- a/activejob/lib/active_job/serializers/struct_serializer.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize struct instances - # (`Struct.new('Rectangle', :width, :height).new(12, 20)`) - class StructSerializer < ObjectSerializer - class << self - def serialize(object) - super.merge values_key => ::ActiveJob::Serializers.serialize(object.values) - end - - def deserialize(hash) - values = ::ActiveJob::Serializers.deserialize(hash[values_key]) - super.new(*values) - end - - def key - "_aj_struct" - end - - private - - def klass - ::Struct - end - - def keys - super.push values_key - end - - def values_key - "values" - end - end - end - end -end diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb deleted file mode 100644 index f128ae8284..0000000000 --- a/activejob/lib/active_job/serializers/symbol_serializer.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize `Symbol` (`:foo`, `:bar`, ...) - class SymbolSerializer < ObjectSerializer - class << self - def serialize(symbol) - { key => symbol.to_s } - end - - def deserialize(hash) - hash[key].to_sym - end - - def key - "_aj_symbol" - end - - private - - def klass - ::Symbol - end - end - end - end -end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index c940cd154c..69b4fe7d26 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -30,7 +30,7 @@ module Rails # :nodoc: private def application_job_file_name @application_job_file_name ||= if mountable_engine? - "app/jobs/#{namespaced_path}/application_job.rb" + "app/jobs/#{namespaced_path}/application_job.rb" else "app/jobs/application_job.rb" end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 20296038a0..13e6fcb727 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -4,7 +4,6 @@ require "helper" require "active_job/arguments" require "models/person" require "active_support/core_ext/hash/indifferent_access" -require "active_support/duration" require "jobs/kwargs_job" class ArgumentSerializationTest < ActiveSupport::TestCase @@ -14,7 +13,6 @@ class ArgumentSerializationTest < ActiveSupport::TestCase [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, "a", true, false, BigDecimal.new(5), - :a, self, 1.day, [ 1, "a" ], { "a" => 1 } ].each do |arg| @@ -23,7 +21,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end - [ Object.new, Person.find("5").to_gid ].each do |arg| + [ :a, 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 ] @@ -35,10 +33,6 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end - test "serializes Struct" do - assert_arguments_unchanged Struct.new("Rectangle", :width, :height).new(10, 15) - end - test "should convert records to Global IDs" do assert_arguments_roundtrip [@person] end diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb new file mode 100644 index 0000000000..90d4155b3b --- /dev/null +++ b/activejob/test/cases/serializers_test.rb @@ -0,0 +1,64 @@ +# 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 + end + + class DummySerializer < ActiveJob::Serializers::ObjectSerializer + class << self + def serialize(object) + { key => object.value } + end + + def deserialize(hash) + DummyValueObject.new(hash[key]) + end + + def key + "_dummy_serializer" + end + + private + + def klass + DummyValueObject + end + end + end + + setup do + @value_object = DummyValueObject.new 123 + ActiveJob::Base._additional_serializers = [] + end + + test "can't serialize unknown object" do + assert_raises ActiveJob::SerializationError do + ActiveJob::Serializers.serialize @value_object + end + end + + test "won't deserialize unknown hash" do + hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] } + assert ActiveJob::Serializers.deserialize(hash), hash.except("_aj_symbol_keys") + end + + test "adds new serializer" do + ActiveJob::Base.add_serializers DummySerializer + assert ActiveJob::Base.serializers.include?(DummySerializer) + end + + test "can't add serializer with the same key twice" do + ActiveJob::Base.add_serializers DummySerializer + assert_raises ArgumentError do + ActiveJob::Base.add_serializers DummySerializer + end + end +end diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index a7067cb97d..eea64f9367 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -345,10 +345,6 @@ Supported types for arguments ActiveJob supports the following types of arguments by default: - Basic types (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) - - `Symbol` (`:foo`, `:bar`, ...) - - `ActiveSupport::Duration` (`1.day`, `2.weeks`, ...) - - Classes constants (`ActiveRecord::Base`, `MySpecialService`, ...) - - Struct instances (`Struct.new('Rectangle', :width, :height).new(12, 20)`, ...) - `Hash`. Keys should be of `String` or `Symbol` type - `ActiveSupport::HashWithIndifferentAccess` - `Array` -- cgit v1.2.3 From ec686a471e0a54194fc9ec72e639785606597704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 14:24:55 -0500 Subject: Simplify the implementation of custom serialziers Right now it is only possible to define serializers globally so we don't need to use a class attribute in the job class. --- activejob/lib/active_job/serializers.rb | 62 ++++++++++------------ .../lib/active_job/serializers/hash_serializer.rb | 2 +- activejob/test/cases/serializers_test.rb | 14 +++-- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index 68ed94896e..41113c521c 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -33,16 +33,31 @@ module ActiveJob autoload :ObjectSerializer autoload :StandardTypeSerializer - included do - class_attribute :_additional_serializers, instance_accessor: false, instance_predicate: false - self._additional_serializers = [] - end + mattr_accessor :_additional_serializers + self._additional_serializers = [] + + 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 = serializers.detect { |s| s.deserialize?(argument) } + raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" unless serializer + serializer.deserialize(argument) + end - # Includes the method to list known serializers and to add new ones - module ClassMethods # Returns list of known serializers def serializers - self._additional_serializers + ActiveJob::Serializers::SERIALIZERS + self._additional_serializers end # Adds a new serializer to a list of known serializers @@ -68,33 +83,10 @@ module ActiveJob end end - # :nodoc: - SERIALIZERS = [ - ::ActiveJob::Serializers::GlobalIDSerializer, - ::ActiveJob::Serializers::StandardTypeSerializer, - ::ActiveJob::Serializers::HashWithIndifferentAccessSerializer, - ::ActiveJob::Serializers::HashSerializer, - ::ActiveJob::Serializers::ArraySerializer - ].freeze - - 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 = ::ActiveJob::Base.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 = ::ActiveJob::Base.serializers.detect { |s| s.deserialize?(argument) } - raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" unless serializer - serializer.deserialize(argument) - end - end + add_serializers GlobalIDSerializer, + StandardTypeSerializer, + HashWithIndifferentAccessSerializer, + HashSerializer, + ArraySerializer end end diff --git a/activejob/lib/active_job/serializers/hash_serializer.rb b/activejob/lib/active_job/serializers/hash_serializer.rb index eee081de7c..c4dcfaf094 100644 --- a/activejob/lib/active_job/serializers/hash_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_serializer.rb @@ -38,7 +38,7 @@ module ActiveJob def serialize_hash_key(key) raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") unless [String, Symbol].include?(key.class) - raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") if ActiveJob::Base.reserved_serializers_keys.include?(key.to_s) + raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") if ActiveJob::Serializers.reserved_serializers_keys.include?(key.to_s) key.to_s end diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb index 90d4155b3b..3b526c932b 100644 --- a/activejob/test/cases/serializers_test.rb +++ b/activejob/test/cases/serializers_test.rb @@ -36,7 +36,11 @@ class SerializersTest < ActiveSupport::TestCase setup do @value_object = DummyValueObject.new 123 - ActiveJob::Base._additional_serializers = [] + @original_serializers = ActiveJob::Serializers.serializers + end + + teardown do + ActiveJob::Serializers._additional_serializers = @original_serializers end test "can't serialize unknown object" do @@ -51,14 +55,14 @@ class SerializersTest < ActiveSupport::TestCase end test "adds new serializer" do - ActiveJob::Base.add_serializers DummySerializer - assert ActiveJob::Base.serializers.include?(DummySerializer) + ActiveJob::Serializers.add_serializers DummySerializer + assert ActiveJob::Serializers.serializers.include?(DummySerializer) end test "can't add serializer with the same key twice" do - ActiveJob::Base.add_serializers DummySerializer + ActiveJob::Serializers.add_serializers DummySerializer assert_raises ArgumentError do - ActiveJob::Base.add_serializers DummySerializer + ActiveJob::Serializers.add_serializers DummySerializer end end end -- cgit v1.2.3 From 803f4385c6c30217e3d2cf81cbaba92c7bc58476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 14:31:00 -0500 Subject: Remove unnecessary qualified constant lookups --- activejob/lib/active_job/serializers/array_serializer.rb | 6 +++--- activejob/lib/active_job/serializers/global_id_serializer.rb | 2 +- activejob/lib/active_job/serializers/hash_serializer.rb | 6 +++--- .../serializers/hash_with_indifferent_access_serializer.rb | 6 +++--- activejob/lib/active_job/serializers/standard_type_serializer.rb | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/activejob/lib/active_job/serializers/array_serializer.rb b/activejob/lib/active_job/serializers/array_serializer.rb index f0254f4488..1b3c3b2ce3 100644 --- a/activejob/lib/active_job/serializers/array_serializer.rb +++ b/activejob/lib/active_job/serializers/array_serializer.rb @@ -8,17 +8,17 @@ module ActiveJob alias_method :deserialize?, :serialize? def serialize(array) - array.map { |arg| ::ActiveJob::Serializers.serialize(arg) } + array.map { |arg| Serializers.serialize(arg) } end def deserialize(array) - array.map { |arg| ::ActiveJob::Serializers.deserialize(arg) } + array.map { |arg| Serializers.deserialize(arg) } end private def klass - ::Array + Array end end end diff --git a/activejob/lib/active_job/serializers/global_id_serializer.rb b/activejob/lib/active_job/serializers/global_id_serializer.rb index 1961e43fca..ec20cf04f7 100644 --- a/activejob/lib/active_job/serializers/global_id_serializer.rb +++ b/activejob/lib/active_job/serializers/global_id_serializer.rb @@ -24,7 +24,7 @@ module ActiveJob private def klass - ::GlobalID::Identification + GlobalID::Identification end end end diff --git a/activejob/lib/active_job/serializers/hash_serializer.rb b/activejob/lib/active_job/serializers/hash_serializer.rb index c4dcfaf094..ca39a81ae9 100644 --- a/activejob/lib/active_job/serializers/hash_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_serializer.rb @@ -18,7 +18,7 @@ module ActiveJob end def deserialize(hash) - result = hash.transform_values { |v| ::ActiveJob::Serializers::deserialize(v) } + result = hash.transform_values { |v| Serializers::deserialize(v) } symbol_keys = result.delete(key) transform_symbol_keys(result, symbol_keys) end @@ -31,7 +31,7 @@ module ActiveJob def serialize_hash(hash) hash.each_with_object({}) do |(key, value), result| - result[serialize_hash_key(key)] = ::ActiveJob::Serializers.serialize(value) + result[serialize_hash_key(key)] = Serializers.serialize(value) end end @@ -54,7 +54,7 @@ module ActiveJob end def klass - ::Hash + Hash end end end diff --git a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb index 50e80757cd..b0fb29d58b 100644 --- a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb @@ -8,7 +8,7 @@ module ActiveJob class << self def serialize(hash) result = serialize_hash(hash) - result[key] = ::ActiveJob::Serializers.serialize(true) + result[key] = Serializers.serialize(true) result end @@ -17,7 +17,7 @@ module ActiveJob end def deserialize(hash) - result = hash.transform_values { |v| ::ActiveJob::Serializers.deserialize(v) } + result = hash.transform_values { |v| Serializers.deserialize(v) } result.delete(key) result.with_indifferent_access end @@ -29,7 +29,7 @@ module ActiveJob private def klass - ::ActiveSupport::HashWithIndifferentAccess + ActiveSupport::HashWithIndifferentAccess end end end diff --git a/activejob/lib/active_job/serializers/standard_type_serializer.rb b/activejob/lib/active_job/serializers/standard_type_serializer.rb index 8969b31d6b..efc02adcf0 100644 --- a/activejob/lib/active_job/serializers/standard_type_serializer.rb +++ b/activejob/lib/active_job/serializers/standard_type_serializer.rb @@ -7,7 +7,7 @@ module ActiveJob class StandardTypeSerializer < BaseSerializer class << self def serialize?(argument) - ::ActiveJob::Arguments::TYPE_WHITELIST.include? argument.class + Arguments::TYPE_WHITELIST.include? argument.class end def serialize(argument) -- cgit v1.2.3 From 9bc8b4bbde4634e0e4bddcffa25e0bf8d74d19cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 14:34:29 -0500 Subject: Define the interface of a Serializer --- .../lib/active_job/serializers/base_serializer.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/activejob/lib/active_job/serializers/base_serializer.rb b/activejob/lib/active_job/serializers/base_serializer.rb index 98f7852fd6..8b891cca48 100644 --- a/activejob/lib/active_job/serializers/base_serializer.rb +++ b/activejob/lib/active_job/serializers/base_serializer.rb @@ -7,6 +7,24 @@ module ActiveJob def serialize?(argument) argument.is_a?(klass) end + + def deserialize?(_argument) + raise NotImplementedError + end + + def serialize(_argument) + raise NotImplementedError + end + + def deserialize(_argument) + raise NotImplementedError + end + + private + + def klass + raise NotImplementedError + end end end end -- cgit v1.2.3 From ea615332452e6860872020aa161c5d34e81f1eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 15:23:05 -0500 Subject: Only add one more custom key in the serialized hash Now custom serialziers can register itself in the serialized hash using the "_aj_serialized" key that constains the serializer name. This way we can avoid poluting the hash with many reserved keys. --- activejob/lib/active_job/serializers.rb | 37 +++++++++++++--------- .../active_job/serializers/global_id_serializer.rb | 10 +++--- .../lib/active_job/serializers/hash_serializer.rb | 6 ++-- .../hash_with_indifferent_access_serializer.rb | 10 ++---- .../active_job/serializers/object_serializer.rb | 16 ++-------- activejob/test/cases/serializers_test.rb | 31 +++++++++++++----- 6 files changed, 59 insertions(+), 51 deletions(-) diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index 41113c521c..12458ea572 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "set" + module ActiveJob # Raised when an exception is raised during job arguments deserialization. # @@ -34,7 +36,7 @@ module ActiveJob autoload :StandardTypeSerializer mattr_accessor :_additional_serializers - self._additional_serializers = [] + self._additional_serializers = Set.new class << self # Returns serialized representative of the passed object. @@ -62,27 +64,32 @@ module ActiveJob # Adds a new serializer to a list of known serializers def add_serializers(*new_serializers) - check_duplicate_serializer_keys!(new_serializers) - - self._additional_serializers = new_serializers + self._additional_serializers + self._additional_serializers += new_serializers end # Returns a list of reserved keys, which cannot be used as keys for a hash def reserved_serializers_keys - serializers.select { |s| s.respond_to?(:key) }.map(&:key) + RESERVED_KEYS end - - private - - def check_duplicate_serializer_keys!(serializers) - keys_to_add = serializers.select { |s| s.respond_to?(:key) }.map(&:key) - - duplicate_keys = reserved_serializers_keys & keys_to_add - - raise ArgumentError.new("Can't add serializers because of keys duplication: #{duplicate_keys}") if duplicate_keys.any? - end end + # :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 + # :nodoc: + OBJECT_SERIALIZER_KEY = "_aj_serialized" + + # :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 + add_serializers GlobalIDSerializer, StandardTypeSerializer, HashWithIndifferentAccessSerializer, diff --git a/activejob/lib/active_job/serializers/global_id_serializer.rb b/activejob/lib/active_job/serializers/global_id_serializer.rb index ec20cf04f7..84ed33ef99 100644 --- a/activejob/lib/active_job/serializers/global_id_serializer.rb +++ b/activejob/lib/active_job/serializers/global_id_serializer.rb @@ -4,21 +4,21 @@ module ActiveJob module Serializers # Provides methods to serialize and deserialize objects which mixes `GlobalID::Identification`, # including `ActiveRecord::Base` models - class GlobalIDSerializer < ObjectSerializer + class GlobalIDSerializer < BaseSerializer class << self def serialize(object) - { key => object.to_global_id.to_s } + { GLOBALID_KEY => object.to_global_id.to_s } rescue URI::GID::MissingModelIdError raise SerializationError, "Unable to serialize #{object.class} " \ "without an id. (Maybe you forgot to call save?)" end def deserialize(hash) - GlobalID::Locator.locate(hash[key]) + GlobalID::Locator.locate(hash[GLOBALID_KEY]) end - def key - "_aj_globalid" + def deserialize?(argument) + argument.is_a?(Hash) && argument[GLOBALID_KEY] end private diff --git a/activejob/lib/active_job/serializers/hash_serializer.rb b/activejob/lib/active_job/serializers/hash_serializer.rb index ca39a81ae9..2bbb31946d 100644 --- a/activejob/lib/active_job/serializers/hash_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_serializer.rb @@ -23,12 +23,12 @@ module ActiveJob transform_symbol_keys(result, symbol_keys) end + private + def key - "_aj_symbol_keys" + SYMBOL_KEYS_KEY end - private - def serialize_hash(hash) hash.each_with_object({}) do |(key, value), result| result[serialize_hash_key(key)] = Serializers.serialize(value) diff --git a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb index b0fb29d58b..af3576dd57 100644 --- a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb @@ -12,22 +12,18 @@ module ActiveJob result end - def deserialize?(argument) - argument.is_a?(Hash) && argument[key] - end - def deserialize(hash) result = hash.transform_values { |v| Serializers.deserialize(v) } result.delete(key) result.with_indifferent_access end + private + def key - "_aj_hash_with_indifferent_access" + WITH_INDIFFERENT_ACCESS_KEY end - private - def klass ActiveSupport::HashWithIndifferentAccess end diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb index 075360b26e..d5ff8c91f1 100644 --- a/activejob/lib/active_job/serializers/object_serializer.rb +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -4,22 +4,12 @@ module ActiveJob module Serializers class ObjectSerializer < BaseSerializer class << self - def serialize(object) - { key => object.class.name } + def serialize(hash) + { OBJECT_SERIALIZER_KEY => self.name }.merge!(hash) end def deserialize?(argument) - argument.respond_to?(:keys) && argument.keys == keys - end - - def deserialize(hash) - hash[key].constantize - end - - private - - def keys - [key] + argument.is_a?(Hash) && argument[OBJECT_SERIALIZER_KEY] == self.name end end end diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb index 3b526c932b..fb0e6ecae6 100644 --- a/activejob/test/cases/serializers_test.rb +++ b/activejob/test/cases/serializers_test.rb @@ -10,20 +10,20 @@ class SerializersTest < ActiveSupport::TestCase def initialize(value) @value = value end + + def ==(other) + self.value == other.value + end end class DummySerializer < ActiveJob::Serializers::ObjectSerializer class << self def serialize(object) - { key => object.value } + super({ "value" => object.value }) end def deserialize(hash) - DummyValueObject.new(hash[key]) - end - - def key - "_dummy_serializer" + DummyValueObject.new(hash["value"]) end private @@ -49,9 +49,24 @@ class SerializersTest < ActiveSupport::TestCase 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" => [] } - assert ActiveJob::Serializers.deserialize(hash), hash.except("_aj_symbol_keys") + assert_equal({ "_dummy_serializer" => 123 }, ActiveJob::Serializers.deserialize(hash)) + 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 @@ -61,7 +76,7 @@ class SerializersTest < ActiveSupport::TestCase test "can't add serializer with the same key twice" do ActiveJob::Serializers.add_serializers DummySerializer - assert_raises ArgumentError do + assert_no_difference(-> { ActiveJob::Serializers.serializers.size } ) do ActiveJob::Serializers.add_serializers DummySerializer end end -- cgit v1.2.3 From b098584f63521d214c1107e6eaa24f292b8e4df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 15:45:11 -0500 Subject: Add symbol and duration serializers --- activejob/lib/active_job/serializers.rb | 6 +++++- .../active_job/serializers/duration_serializer.rb | 24 ++++++++++++++++++++++ .../active_job/serializers/symbol_serializer.rb | 21 +++++++++++++++++++ .../test/cases/argument_serialization_test.rb | 3 ++- 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 activejob/lib/active_job/serializers/duration_serializer.rb create mode 100644 activejob/lib/active_job/serializers/symbol_serializer.rb diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index 12458ea572..9e3fcda28d 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -34,6 +34,8 @@ module ActiveJob autoload :HashSerializer autoload :ObjectSerializer autoload :StandardTypeSerializer + autoload :SymbolSerializer + autoload :DurationSerializer mattr_accessor :_additional_serializers self._additional_serializers = Set.new @@ -94,6 +96,8 @@ module ActiveJob StandardTypeSerializer, HashWithIndifferentAccessSerializer, HashSerializer, - ArraySerializer + ArraySerializer, + SymbolSerializer, + DurationSerializer 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..46543cc30d --- /dev/null +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -0,0 +1,24 @@ +module ActiveJob + module Serializers + class DurationSerializer < ObjectSerializer + class << self + def serialize(duration) + super("value" => duration.value, "parts" => Serializers.serialize(duration.parts)) + end + + def deserialize(hash) + value = hash["value"] + parts = Serializers.deserialize(hash["parts"]) + + klass.new(value, parts) + end + + private + + def klass + ActiveSupport::Duration + end + 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..ec27f6828a --- /dev/null +++ b/activejob/lib/active_job/serializers/symbol_serializer.rb @@ -0,0 +1,21 @@ +module ActiveJob + module Serializers + class SymbolSerializer < ObjectSerializer + class << self + 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 +end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 13e6fcb727..442384f2c3 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -13,6 +13,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, "a", true, false, BigDecimal.new(5), + :a, 1.day, [ 1, "a" ], { "a" => 1 } ].each do |arg| @@ -21,7 +22,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 ] -- cgit v1.2.3 From d2d98d69468bf34a39794496beb8f9f7b69088c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 16:32:35 -0500 Subject: Allow serializers to be used either as classes or objects --- .../lib/active_job/serializers/array_serializer.rb | 20 ++++++------- .../lib/active_job/serializers/base_serializer.rb | 32 +++++++++++--------- .../active_job/serializers/duration_serializer.rb | 22 +++++++------- .../active_job/serializers/global_id_serializer.rb | 30 +++++++++---------- .../lib/active_job/serializers/hash_serializer.rb | 34 ++++++++++------------ .../hash_with_indifferent_access_serializer.rb | 26 ++++++++--------- .../active_job/serializers/object_serializer.rb | 12 ++++---- .../serializers/standard_type_serializer.rb | 24 +++++++-------- .../active_job/serializers/symbol_serializer.rb | 18 +++++------- activejob/test/cases/serializers_test.rb | 16 +++++----- 10 files changed, 110 insertions(+), 124 deletions(-) diff --git a/activejob/lib/active_job/serializers/array_serializer.rb b/activejob/lib/active_job/serializers/array_serializer.rb index 1b3c3b2ce3..9db4edea99 100644 --- a/activejob/lib/active_job/serializers/array_serializer.rb +++ b/activejob/lib/active_job/serializers/array_serializer.rb @@ -3,24 +3,22 @@ module ActiveJob module Serializers # Provides methods to serialize and deserialize `Array` - class ArraySerializer < BaseSerializer - class << self - alias_method :deserialize?, :serialize? + class ArraySerializer < BaseSerializer # :nodoc: + alias_method :deserialize?, :serialize? - def serialize(array) - array.map { |arg| Serializers.serialize(arg) } - end + def serialize(array) + array.map { |arg| Serializers.serialize(arg) } + end - def deserialize(array) - array.map { |arg| Serializers.deserialize(arg) } - end + def deserialize(array) + array.map { |arg| Serializers.deserialize(arg) } + end - private + private def klass Array end - end end end end diff --git a/activejob/lib/active_job/serializers/base_serializer.rb b/activejob/lib/active_job/serializers/base_serializer.rb index 8b891cca48..2e510781cf 100644 --- a/activejob/lib/active_job/serializers/base_serializer.rb +++ b/activejob/lib/active_job/serializers/base_serializer.rb @@ -3,29 +3,33 @@ module ActiveJob module Serializers class BaseSerializer + include Singleton + class << self - def serialize?(argument) - argument.is_a?(klass) - end + delegate :serialize?, :deserialize?, :serialize, :deserialize, to: :instance + end - def deserialize?(_argument) - raise NotImplementedError - end + def serialize?(argument) + argument.is_a?(klass) + end - def serialize(_argument) - raise NotImplementedError - end + def deserialize?(_argument) + raise NotImplementedError + end - def deserialize(_argument) - raise NotImplementedError - end + def serialize(_argument) + raise NotImplementedError + end - private + def deserialize(_argument) + raise NotImplementedError + end + + protected def klass raise NotImplementedError end - end end end end diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb index 46543cc30d..94b0d0407a 100644 --- a/activejob/lib/active_job/serializers/duration_serializer.rb +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -1,24 +1,22 @@ module ActiveJob module Serializers - class DurationSerializer < ObjectSerializer - class << self - def serialize(duration) - super("value" => duration.value, "parts" => Serializers.serialize(duration.parts)) - end + class DurationSerializer < ObjectSerializer # :nodoc: + def serialize(duration) + super("value" => duration.value, "parts" => Serializers.serialize(duration.parts)) + end - def deserialize(hash) - value = hash["value"] - parts = Serializers.deserialize(hash["parts"]) + def deserialize(hash) + value = hash["value"] + parts = Serializers.deserialize(hash["parts"]) - klass.new(value, parts) - end + klass.new(value, parts) + end - private + private def klass ActiveSupport::Duration end - end end end end diff --git a/activejob/lib/active_job/serializers/global_id_serializer.rb b/activejob/lib/active_job/serializers/global_id_serializer.rb index 84ed33ef99..2d8629ef02 100644 --- a/activejob/lib/active_job/serializers/global_id_serializer.rb +++ b/activejob/lib/active_job/serializers/global_id_serializer.rb @@ -4,29 +4,27 @@ module ActiveJob module Serializers # Provides methods to serialize and deserialize objects which mixes `GlobalID::Identification`, # including `ActiveRecord::Base` models - class GlobalIDSerializer < BaseSerializer - class << self - def serialize(object) - { GLOBALID_KEY => object.to_global_id.to_s } - rescue URI::GID::MissingModelIdError - raise SerializationError, "Unable to serialize #{object.class} " \ - "without an id. (Maybe you forgot to call save?)" - end + class GlobalIDSerializer < BaseSerializer # :nodoc: + def serialize(object) + { GLOBALID_KEY => object.to_global_id.to_s } + rescue URI::GID::MissingModelIdError + raise SerializationError, "Unable to serialize #{object.class} " \ + "without an id. (Maybe you forgot to call save?)" + end - def deserialize(hash) - GlobalID::Locator.locate(hash[GLOBALID_KEY]) - end + def deserialize(hash) + GlobalID::Locator.locate(hash[GLOBALID_KEY]) + end - def deserialize?(argument) - argument.is_a?(Hash) && argument[GLOBALID_KEY] - end + def deserialize?(argument) + argument.is_a?(Hash) && argument[GLOBALID_KEY] + end - private + private def klass GlobalID::Identification end - end end end end diff --git a/activejob/lib/active_job/serializers/hash_serializer.rb b/activejob/lib/active_job/serializers/hash_serializer.rb index 2bbb31946d..e569fe7501 100644 --- a/activejob/lib/active_job/serializers/hash_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_serializer.rb @@ -4,26 +4,25 @@ module ActiveJob module Serializers # Provides methods to serialize and deserialize `Hash` (`{key: field, ...}`) # Only `String` or `Symbol` can be used as a key. Values will be serialized by known serializers - class HashSerializer < BaseSerializer - class << self - def serialize(hash) - symbol_keys = hash.each_key.grep(Symbol).map(&:to_s) - result = serialize_hash(hash) - result[key] = symbol_keys - result - end + class HashSerializer < BaseSerializer # :nodoc: + def serialize(hash) + symbol_keys = hash.each_key.grep(Symbol).map(&:to_s) + result = serialize_hash(hash) + result[key] = symbol_keys + result + end - def deserialize?(argument) - argument.is_a?(Hash) && argument[key] - end + def deserialize?(argument) + argument.is_a?(Hash) && argument[key] + end - def deserialize(hash) - result = hash.transform_values { |v| Serializers::deserialize(v) } - symbol_keys = result.delete(key) - transform_symbol_keys(result, symbol_keys) - end + def deserialize(hash) + result = hash.transform_values { |v| Serializers::deserialize(v) } + symbol_keys = result.delete(key) + transform_symbol_keys(result, symbol_keys) + end - private + private def key SYMBOL_KEYS_KEY @@ -56,7 +55,6 @@ module ActiveJob def klass Hash end - end end end end diff --git a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb index af3576dd57..3b812ba304 100644 --- a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb +++ b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb @@ -4,21 +4,20 @@ module ActiveJob module Serializers # Provides methods to serialize and deserialize `ActiveSupport::HashWithIndifferentAccess` # Values will be serialized by known serializers - class HashWithIndifferentAccessSerializer < HashSerializer - class << self - def serialize(hash) - result = serialize_hash(hash) - result[key] = Serializers.serialize(true) - result - end + class HashWithIndifferentAccessSerializer < HashSerializer # :nodoc: + def serialize(hash) + result = serialize_hash(hash) + result[key] = Serializers.serialize(true) + result + end - def deserialize(hash) - result = hash.transform_values { |v| Serializers.deserialize(v) } - result.delete(key) - result.with_indifferent_access - end + def deserialize(hash) + result = hash.transform_values { |v| Serializers.deserialize(v) } + result.delete(key) + result.with_indifferent_access + end - private + private def key WITH_INDIFFERENT_ACCESS_KEY @@ -27,7 +26,6 @@ module ActiveJob def klass ActiveSupport::HashWithIndifferentAccess end - end end end end diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb index d5ff8c91f1..318eabebdf 100644 --- a/activejob/lib/active_job/serializers/object_serializer.rb +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -3,14 +3,12 @@ module ActiveJob module Serializers class ObjectSerializer < BaseSerializer - class << self - def serialize(hash) - { OBJECT_SERIALIZER_KEY => self.name }.merge!(hash) - end + def serialize(hash) + { OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) + end - def deserialize?(argument) - argument.is_a?(Hash) && argument[OBJECT_SERIALIZER_KEY] == self.name - end + def deserialize?(argument) + argument.is_a?(Hash) && argument[OBJECT_SERIALIZER_KEY] == self.class.name end end end diff --git a/activejob/lib/active_job/serializers/standard_type_serializer.rb b/activejob/lib/active_job/serializers/standard_type_serializer.rb index efc02adcf0..1db4f3937d 100644 --- a/activejob/lib/active_job/serializers/standard_type_serializer.rb +++ b/activejob/lib/active_job/serializers/standard_type_serializer.rb @@ -4,22 +4,20 @@ module ActiveJob module Serializers # Provides methods to serialize and deserialize standard types # (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) - class StandardTypeSerializer < BaseSerializer - class << self - def serialize?(argument) - Arguments::TYPE_WHITELIST.include? argument.class - end + class StandardTypeSerializer < BaseSerializer # :nodoc: + def serialize?(argument) + Arguments::TYPE_WHITELIST.include? argument.class + end - def serialize(argument) - argument - end + def serialize(argument) + argument + end - alias_method :deserialize?, :serialize? + alias_method :deserialize?, :serialize? - def deserialize(argument) - object = GlobalID::Locator.locate(argument) if argument.is_a? String - object || argument - end + def deserialize(argument) + object = GlobalID::Locator.locate(argument) if argument.is_a? String + object || argument end end end diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb index ec27f6828a..c8900de9d6 100644 --- a/activejob/lib/active_job/serializers/symbol_serializer.rb +++ b/activejob/lib/active_job/serializers/symbol_serializer.rb @@ -1,21 +1,19 @@ module ActiveJob module Serializers - class SymbolSerializer < ObjectSerializer - class << self - def serialize(argument) - super("value" => argument.to_s) - end + class SymbolSerializer < ObjectSerializer # :nodoc: + def serialize(argument) + super("value" => argument.to_s) + end - def deserialize(argument) - argument["value"].to_sym - end + def deserialize(argument) + argument["value"].to_sym + end - private + private def klass Symbol end - end end end end diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb index fb0e6ecae6..207ae55b49 100644 --- a/activejob/test/cases/serializers_test.rb +++ b/activejob/test/cases/serializers_test.rb @@ -17,21 +17,19 @@ class SerializersTest < ActiveSupport::TestCase end class DummySerializer < ActiveJob::Serializers::ObjectSerializer - class << self - def serialize(object) - super({ "value" => object.value }) - end + def serialize(object) + super({ "value" => object.value }) + end - def deserialize(hash) - DummyValueObject.new(hash["value"]) - end + def deserialize(hash) + DummyValueObject.new(hash["value"]) + end - private + private def klass DummyValueObject end - end end setup do -- cgit v1.2.3 From d9a5c7011f62dd771a2fa430090e068b1f9785f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 16:50:57 -0500 Subject: Add serializers for Time, Date and DateTime --- activejob/lib/active_job/serializers.rb | 8 +++++++- .../lib/active_job/serializers/date_serializer.rb | 21 +++++++++++++++++++++ .../active_job/serializers/date_time_serializer.rb | 21 +++++++++++++++++++++ .../active_job/serializers/duration_serializer.rb | 2 ++ .../lib/active_job/serializers/symbol_serializer.rb | 2 ++ .../lib/active_job/serializers/time_serializer.rb | 21 +++++++++++++++++++++ activejob/test/cases/argument_serialization_test.rb | 4 +++- 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 activejob/lib/active_job/serializers/date_serializer.rb create mode 100644 activejob/lib/active_job/serializers/date_time_serializer.rb create mode 100644 activejob/lib/active_job/serializers/time_serializer.rb diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index 9e3fcda28d..dfd654175d 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -36,6 +36,9 @@ module ActiveJob autoload :StandardTypeSerializer autoload :SymbolSerializer autoload :DurationSerializer + autoload :DateSerializer + autoload :TimeSerializer + autoload :DateTimeSerializer mattr_accessor :_additional_serializers self._additional_serializers = Set.new @@ -98,6 +101,9 @@ module ActiveJob HashSerializer, ArraySerializer, SymbolSerializer, - DurationSerializer + 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 index 94b0d0407a..a3c4c5d1c2 100644 --- a/activejob/lib/active_job/serializers/duration_serializer.rb +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveJob module Serializers class DurationSerializer < ObjectSerializer # :nodoc: diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb index c8900de9d6..7e1f9553a2 100644 --- a/activejob/lib/active_job/serializers/symbol_serializer.rb +++ b/activejob/lib/active_job/serializers/symbol_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveJob module Serializers class SymbolSerializer < ObjectSerializer # :nodoc: 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 442384f2c3..8f51a2d238 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -13,7 +13,9 @@ class ArgumentSerializationTest < ActiveSupport::TestCase [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, "a", true, false, BigDecimal.new(5), - :a, 1.day, + :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| -- cgit v1.2.3 From 2fe467091b3743627d52a3e2ae357f0b5fd6d157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 17:26:35 -0500 Subject: No need to require a autoloaded constant --- activejob/lib/active_job/base.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index 8275776820..6194f89956 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_job/core" -require "active_job/serializers" require "active_job/queue_adapter" require "active_job/queue_name" require "active_job/queue_priority" -- cgit v1.2.3 From a5f7357a3dff2617ba13a274feb8d8ac2492f26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 17:27:01 -0500 Subject: Add configuration to set custom serializers --- activejob/lib/active_job/railtie.rb | 6 ++++++ guides/source/configuring.md | 2 ++ 2 files changed, 8 insertions(+) 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/guides/source/configuring.md b/guides/source/configuring.md index b0f39e7ab5..fd747c1686 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -741,6 +741,8 @@ There are a few configuration options available in Active Support: * `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging. +* `config.active_job.custom_serializers` allows to set custom argument serializers. Defaults to `[]`. + ### Configuring Action Cable * `config.action_cable.url` accepts a string for the URL for where -- cgit v1.2.3 From 71721dc1c9b769d3c06317122dc88cad4a346580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 9 Feb 2018 17:27:39 -0500 Subject: Improve documentation on custom serializers --- activejob/README.md | 58 +--------------------- .../lib/active_job/serializers/base_serializer.rb | 6 +++ .../active_job/serializers/object_serializer.rb | 19 +++++++ guides/source/active_job_basics.md | 43 ++++++++-------- 4 files changed, 47 insertions(+), 79 deletions(-) diff --git a/activejob/README.md b/activejob/README.md index 56562d870b..f1ebb76e08 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -52,17 +52,8 @@ MyJob.set(wait: 1.week).perform_later(record) # Enqueue a job to be performed 1 That's it! -## Supported types for arguments -ActiveJob supports the following types of arguments by default: - - - Standard types (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) - - `Hash`. Keys should be of `String` or `Symbol` type - - `ActiveSupport::HashWithIndifferentAccess` - - `Array` - - -### GlobalID support +## GlobalID support Active Job supports [GlobalID serialization](https://github.com/rails/globalid/) for parameters. This makes it possible to pass live Active Record objects to your job instead of class/id pairs, which @@ -90,53 +81,6 @@ end This works with any class that mixes in GlobalID::Identification, which by default has been mixed into Active Record classes. -### Serializers - -You can extend list of supported types for arguments. You just need to define your own serializer. - -```ruby -class MySpecialSerializer - class << self - # Check if this object should be serialized using this serializer - def serialize?(argument) - object.is_a? MySpecialValueObject - end - - # Convert an object to a simpler representative using supported object types. - # The recommended representative is a Hash with a specific key. Keys can be of basic types only - def serialize(object) - { - key => ActiveJob::Serializers.serialize(object.value) - 'another_attribute' => ActiveJob::Serializers.serialize(object.another_attribute) - } - end - - # Check if this serialized value be deserialized using this serializer - def deserialize?(argument) - object.is_a?(Hash) && object.keys == [key, 'another_attribute'] - end - - # Convert serialized value into a proper object - def deserialize(object) - value = ActiveJob::Serializers.deserialize(object[key]) - another_attribute = ActiveJob::Serializers.deserialize(object['another_attribute']) - MySpecialValueObject.new value, another_attribute - end - - # Define this method if you are using a hash as a representative. - # This key will be added to a list of restricted keys for hashes. Use basic types only - def key - "_aj_custom_dummy_value_object" - end - end -end -``` - -And now you just need to add this serializer to a list: - -```ruby -ActiveJob::Base.add_serializers(MySpecialSerializer) -``` ## Supported queueing systems diff --git a/activejob/lib/active_job/serializers/base_serializer.rb b/activejob/lib/active_job/serializers/base_serializer.rb index 2e510781cf..155eeb29c3 100644 --- a/activejob/lib/active_job/serializers/base_serializer.rb +++ b/activejob/lib/active_job/serializers/base_serializer.rb @@ -2,6 +2,7 @@ module ActiveJob module Serializers + # Implement the basic interface for Active Job arguments serializers. class BaseSerializer include Singleton @@ -9,24 +10,29 @@ module ActiveJob delegate :serialize?, :deserialize?, :serialize, :deserialize, to: :instance end + # Determines if an argument should be serialized by a serializer. def serialize?(argument) argument.is_a?(klass) end + # Determines if an argument should be deserialized by a serializer. def deserialize?(_argument) raise NotImplementedError end + # Serializes an argument to a JSON primitive type. def serialize(_argument) raise NotImplementedError 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 diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb index 318eabebdf..940b6ff95d 100644 --- a/activejob/lib/active_job/serializers/object_serializer.rb +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -2,6 +2,25 @@ 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 < BaseSerializer def serialize(hash) { OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index eea64f9367..0ee522e23d 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -345,6 +345,12 @@ Supported types for arguments ActiveJob supports the following types of arguments by default: - Basic types (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) + - `Symbol + - `ActiveSupport::Duration` + - `Date` + - `Time` + - `DateTime` + - `ActiveSupport::TimeWithZone` - `Hash`. Keys should be of `String` or `Symbol` type - `ActiveSupport::HashWithIndifferentAccess` - `Array` @@ -382,38 +388,31 @@ by default has been mixed into Active Record classes. You can extend list of supported types for arguments. You just need to define your own serializer. ```ruby -class MySpecialSerializer - class << self - # Check if this object should be serialized using this serializer +class MoneySerializer < ActiveJob::Serializers::ObjectSerializer + # Check if this object should be serialized using this serializer. def serialize?(argument) - argument.is_a? MySpecialValueObject + argument.is_a? Money end # Convert an object to a simpler representative using supported object types. - # The recommended representative is a Hash with a specific key. Keys can be of basic types only + # The recommended representative is a Hash with a specific key. Keys can be of basic types only. + # You should call `super` to add the custom serializer type to the hash def serialize(object) - { - key => ActiveJob::Serializers.serialize(object.value) - 'another_attribute' => ActiveJob::Serializers.serialize(object.another_attribute) - } + super( + "cents" => object.cents, + "currency" => object.currency + ) end - # Check if this serialized value be deserialized using this serializer + # Check if this serialized value be deserialized using this serializer. + # ActiveJob::Serializers::ObjectSerializer#deserialize? already take care of this. def deserialize?(argument) - argument.is_a?(Hash) && argument.keys == [key, 'another_attribute'] + super end # Convert serialized value into a proper object - def deserialize(object) - value = ActiveJob::Serializers.deserialize(object[key]) - another_attribute = ActiveJob::Serializers.deserialize(object['another_attribute']) - MySpecialValueObject.new value, another_attribute - end - - # Define this method if you are using a hash as a representative. - # This key will be added to a list of restricted keys for hashes. Use basic types only - def key - "_aj_custom_dummy_value_object" + def deserialize(hash) + Money.new hash["cents"], hash["currency"] end end end @@ -422,7 +421,7 @@ end And now you just need to add this serializer to a list: ```ruby -ActiveJob::Base.add_serializers(MySpecialSerializer) +Rails.application.config.active_job.custom_serializers << MySpecialSerializer ``` -- cgit v1.2.3 From 69645cba727dfa1c18c666d2a2f1c0dedffde938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 12 Feb 2018 14:16:41 -0500 Subject: Simplify the implementation of custom argument serializers We can speed up things for the supported types by keeping the code in the way it was. We can also avoid to loop trough all serializers in the deserialization by trying to access the class already in the Hash. We could also speed up the custom serialization if we define the class that is going to be serialized when registering the serializers, but that will remove the possibility of defining a serialzer for a superclass and have the subclass serialized using it. --- activejob/lib/active_job/arguments.rb | 141 ++++++++++++++++++++- activejob/lib/active_job/serializers.rb | 61 +-------- .../lib/active_job/serializers/array_serializer.rb | 24 ---- .../lib/active_job/serializers/base_serializer.rb | 41 ------ .../active_job/serializers/duration_serializer.rb | 4 +- .../active_job/serializers/global_id_serializer.rb | 30 ----- .../lib/active_job/serializers/hash_serializer.rb | 60 --------- .../hash_with_indifferent_access_serializer.rb | 31 ----- .../active_job/serializers/object_serializer.rb | 28 +++- .../serializers/standard_type_serializer.rb | 24 ---- .../test/cases/argument_serialization_test.rb | 2 +- activejob/test/cases/serializers_test.rb | 21 ++- guides/source/active_job_basics.md | 6 - 13 files changed, 192 insertions(+), 281 deletions(-) delete mode 100644 activejob/lib/active_job/serializers/array_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/base_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/global_id_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/hash_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb delete mode 100644 activejob/lib/active_job/serializers/standard_type_serializer.rb diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 9d47131864..e6ada163e8 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -3,6 +3,24 @@ require "active_support/core_ext/hash" module ActiveJob + # Raised when an exception is raised during job arguments deserialization. + # + # Wraps the original exception raised as +cause+. + class DeserializationError < StandardError + def initialize #:nodoc: + super("Error while trying to deserialize arguments: #{$!.message}") + set_backtrace $!.backtrace + end + end + + # Raised when an unsupported argument type is set as a job argument. We + # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass, + # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). + # Raised if you set the key for a Hash something else than a string or + # a symbol. Also raised when trying to serialize an object which can't be + # identified with a Global ID - such as an unpersisted Active Record model. + class SerializationError < ArgumentError; end + module Arguments extend self # :nodoc: @@ -13,16 +31,135 @@ module ActiveJob # as-is. Arrays/Hashes are serialized element by element. # All other types are serialized using GlobalID. def serialize(arguments) - ActiveJob::Serializers.serialize(arguments) + arguments.map { |argument| serialize_argument(argument) } end # Deserializes a set of arguments. Whitelisted types are returned # as-is. Arrays/Hashes are deserialized element by element. # All other types are deserialized using GlobalID. def deserialize(arguments) - ActiveJob::Serializers.deserialize(arguments) + arguments.map { |argument| deserialize_argument(argument) } rescue raise DeserializationError 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 + # :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 + when *TYPE_WHITELIST + argument + when GlobalID::Identification + convert_to_global_id_hash(argument) + when Array + argument.map { |arg| serialize_argument(arg) } + when ActiveSupport::HashWithIndifferentAccess + result = serialize_hash(argument) + result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true) + result + when Hash + symbol_keys = argument.each_key.grep(Symbol).map(&:to_s) + result = serialize_hash(argument) + result[SYMBOL_KEYS_KEY] = symbol_keys + result + else + Serializers.serialize(argument) + end + end + + def deserialize_argument(argument) + case argument + when String + GlobalID::Locator.locate(argument) || argument + when *TYPE_WHITELIST + argument + when Array + argument.map { |arg| deserialize_argument(arg) } + when Hash + if serialized_global_id?(argument) + deserialize_global_id argument + elsif custom_serialized?(argument) + Serializers.deserialize(argument) + else + deserialize_hash(argument) + end + else + raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" + end + end + + def serialized_global_id?(hash) + hash.size == 1 && hash.include?(GLOBALID_KEY) + end + + def deserialize_global_id(hash) + 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) + end + end + + 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 + + def serialize_hash_key(key) + case key + when *RESERVED_KEYS + raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") + when String, Symbol + key.to_s + else + raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") + 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 + + def convert_to_global_id_hash(argument) + { GLOBALID_KEY => argument.to_global_id.to_s } + rescue URI::GID::MissingModelIdError + raise SerializationError, "Unable to serialize #{argument.class} " \ + "without an id. (Maybe you forgot to call save?)" + end end end diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index dfd654175d..d9a130fa73 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -3,37 +3,13 @@ require "set" module ActiveJob - # Raised when an exception is raised during job arguments deserialization. - # - # Wraps the original exception raised as +cause+. - class DeserializationError < StandardError - def initialize #:nodoc: - super("Error while trying to deserialize arguments: #{$!.message}") - set_backtrace $!.backtrace - end - end - - # Raised when an unsupported argument type is set as a job argument. We - # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass, - # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). - # Raised if you set the key for a Hash something else than a string or - # a symbol. Also raised when trying to serialize an object which can't be - # identified with a Global ID - such as an unpersisted Active Record model. - class SerializationError < ArgumentError; end - # The ActiveJob::Serializers 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 :ArraySerializer - autoload :BaseSerializer - autoload :GlobalIDSerializer - autoload :HashWithIndifferentAccessSerializer - autoload :HashSerializer autoload :ObjectSerializer - autoload :StandardTypeSerializer autoload :SymbolSerializer autoload :DurationSerializer autoload :DateSerializer @@ -57,8 +33,12 @@ module ActiveJob # Will look up through all known serializers. # If no serializers found will raise `ArgumentError` def deserialize(argument) - serializer = serializers.detect { |s| s.deserialize?(argument) } - raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" unless serializer + 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 @@ -71,36 +51,9 @@ module ActiveJob def add_serializers(*new_serializers) self._additional_serializers += new_serializers end - - # Returns a list of reserved keys, which cannot be used as keys for a hash - def reserved_serializers_keys - RESERVED_KEYS - end end - # :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 - # :nodoc: - OBJECT_SERIALIZER_KEY = "_aj_serialized" - - # :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 - - add_serializers GlobalIDSerializer, - StandardTypeSerializer, - HashWithIndifferentAccessSerializer, - HashSerializer, - ArraySerializer, - SymbolSerializer, + add_serializers SymbolSerializer, DurationSerializer, DateTimeSerializer, DateSerializer, diff --git a/activejob/lib/active_job/serializers/array_serializer.rb b/activejob/lib/active_job/serializers/array_serializer.rb deleted file mode 100644 index 9db4edea99..0000000000 --- a/activejob/lib/active_job/serializers/array_serializer.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize `Array` - class ArraySerializer < BaseSerializer # :nodoc: - alias_method :deserialize?, :serialize? - - def serialize(array) - array.map { |arg| Serializers.serialize(arg) } - end - - def deserialize(array) - array.map { |arg| Serializers.deserialize(arg) } - end - - private - - def klass - Array - end - end - end -end diff --git a/activejob/lib/active_job/serializers/base_serializer.rb b/activejob/lib/active_job/serializers/base_serializer.rb deleted file mode 100644 index 155eeb29c3..0000000000 --- a/activejob/lib/active_job/serializers/base_serializer.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Implement the basic interface for Active Job arguments serializers. - class BaseSerializer - include Singleton - - class << self - delegate :serialize?, :deserialize?, :serialize, :deserialize, to: :instance - end - - # Determines if an argument should be serialized by a serializer. - def serialize?(argument) - argument.is_a?(klass) - end - - # Determines if an argument should be deserialized by a serializer. - def deserialize?(_argument) - raise NotImplementedError - end - - # Serializes an argument to a JSON primitive type. - def serialize(_argument) - raise NotImplementedError - 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/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb index a3c4c5d1c2..715fe27a5c 100644 --- a/activejob/lib/active_job/serializers/duration_serializer.rb +++ b/activejob/lib/active_job/serializers/duration_serializer.rb @@ -4,12 +4,12 @@ module ActiveJob module Serializers class DurationSerializer < ObjectSerializer # :nodoc: def serialize(duration) - super("value" => duration.value, "parts" => Serializers.serialize(duration.parts)) + super("value" => duration.value, "parts" => Arguments.serialize(duration.parts)) end def deserialize(hash) value = hash["value"] - parts = Serializers.deserialize(hash["parts"]) + parts = Arguments.deserialize(hash["parts"]) klass.new(value, parts) end diff --git a/activejob/lib/active_job/serializers/global_id_serializer.rb b/activejob/lib/active_job/serializers/global_id_serializer.rb deleted file mode 100644 index 2d8629ef02..0000000000 --- a/activejob/lib/active_job/serializers/global_id_serializer.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize objects which mixes `GlobalID::Identification`, - # including `ActiveRecord::Base` models - class GlobalIDSerializer < BaseSerializer # :nodoc: - def serialize(object) - { GLOBALID_KEY => object.to_global_id.to_s } - rescue URI::GID::MissingModelIdError - raise SerializationError, "Unable to serialize #{object.class} " \ - "without an id. (Maybe you forgot to call save?)" - end - - def deserialize(hash) - GlobalID::Locator.locate(hash[GLOBALID_KEY]) - end - - def deserialize?(argument) - argument.is_a?(Hash) && argument[GLOBALID_KEY] - end - - private - - def klass - GlobalID::Identification - end - end - end -end diff --git a/activejob/lib/active_job/serializers/hash_serializer.rb b/activejob/lib/active_job/serializers/hash_serializer.rb deleted file mode 100644 index e569fe7501..0000000000 --- a/activejob/lib/active_job/serializers/hash_serializer.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize `Hash` (`{key: field, ...}`) - # Only `String` or `Symbol` can be used as a key. Values will be serialized by known serializers - class HashSerializer < BaseSerializer # :nodoc: - def serialize(hash) - symbol_keys = hash.each_key.grep(Symbol).map(&:to_s) - result = serialize_hash(hash) - result[key] = symbol_keys - result - end - - def deserialize?(argument) - argument.is_a?(Hash) && argument[key] - end - - def deserialize(hash) - result = hash.transform_values { |v| Serializers::deserialize(v) } - symbol_keys = result.delete(key) - transform_symbol_keys(result, symbol_keys) - end - - private - - def key - SYMBOL_KEYS_KEY - end - - def serialize_hash(hash) - hash.each_with_object({}) do |(key, value), result| - result[serialize_hash_key(key)] = Serializers.serialize(value) - end - end - - def serialize_hash_key(key) - raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") unless [String, Symbol].include?(key.class) - - raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") if ActiveJob::Serializers.reserved_serializers_keys.include?(key.to_s) - - key.to_s - 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 - - def klass - Hash - end - end - end -end diff --git a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb b/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb deleted file mode 100644 index 3b812ba304..0000000000 --- a/activejob/lib/active_job/serializers/hash_with_indifferent_access_serializer.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize `ActiveSupport::HashWithIndifferentAccess` - # Values will be serialized by known serializers - class HashWithIndifferentAccessSerializer < HashSerializer # :nodoc: - def serialize(hash) - result = serialize_hash(hash) - result[key] = Serializers.serialize(true) - result - end - - def deserialize(hash) - result = hash.transform_values { |v| Serializers.deserialize(v) } - result.delete(key) - result.with_indifferent_access - end - - private - - def key - WITH_INDIFFERENT_ACCESS_KEY - end - - def klass - ActiveSupport::HashWithIndifferentAccess - end - end - end -end diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb index 940b6ff95d..9f59e8236f 100644 --- a/activejob/lib/active_job/serializers/object_serializer.rb +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -21,14 +21,34 @@ module ActiveJob # Money # end # end - class ObjectSerializer < BaseSerializer + 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) - { OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) + { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) end - def deserialize?(argument) - argument.is_a?(Hash) && argument[OBJECT_SERIALIZER_KEY] == self.class.name + # 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/standard_type_serializer.rb b/activejob/lib/active_job/serializers/standard_type_serializer.rb deleted file mode 100644 index 1db4f3937d..0000000000 --- a/activejob/lib/active_job/serializers/standard_type_serializer.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module ActiveJob - module Serializers - # Provides methods to serialize and deserialize standard types - # (`NilClass`, `String`, `Integer`, `Fixnum`, `Bignum`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`) - class StandardTypeSerializer < BaseSerializer # :nodoc: - def serialize?(argument) - Arguments::TYPE_WHITELIST.include? argument.class - end - - def serialize(argument) - argument - end - - alias_method :deserialize?, :serialize? - - def deserialize(argument) - object = GlobalID::Locator.locate(argument) if argument.is_a? String - object || argument - end - end - end -end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 8f51a2d238..4e26b9a178 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -14,7 +14,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, "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'), + 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 } diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb index 207ae55b49..a86f168d03 100644 --- a/activejob/test/cases/serializers_test.rb +++ b/activejob/test/cases/serializers_test.rb @@ -58,7 +58,24 @@ class SerializersTest < ActiveSupport::TestCase test "won't deserialize unknown hash" do hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] } - assert_equal({ "_dummy_serializer" => 123 }, ActiveJob::Serializers.deserialize(hash)) + 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 @@ -74,7 +91,7 @@ class SerializersTest < ActiveSupport::TestCase test "can't add serializer with the same key twice" do ActiveJob::Serializers.add_serializers DummySerializer - assert_no_difference(-> { ActiveJob::Serializers.serializers.size } ) do + assert_no_difference(-> { ActiveJob::Serializers.serializers.size }) do ActiveJob::Serializers.add_serializers DummySerializer end end diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 0ee522e23d..92a04c585f 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -404,12 +404,6 @@ class MoneySerializer < ActiveJob::Serializers::ObjectSerializer ) end - # Check if this serialized value be deserialized using this serializer. - # ActiveJob::Serializers::ObjectSerializer#deserialize? already take care of this. - def deserialize?(argument) - super - end - # Convert serialized value into a proper object def deserialize(hash) Money.new hash["cents"], hash["currency"] -- cgit v1.2.3 From b59c7c7e69144bafd6d45f1be68f885e8995b6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 12 Feb 2018 22:36:51 -0500 Subject: Add tests to serialize and deserialze individually This will make easier to be backwards compatible when changing the serialization implementation. --- .../test/cases/argument_serialization_test.rb | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 4e26b9a178..ff6ac6fc43 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -49,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 } -- cgit v1.2.3 From 25a14bf2bde90224debee343ebfbb882c02b9588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Wed, 14 Feb 2018 13:13:51 -0500 Subject: Add CHANGELOG entry --- activejob/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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. -- cgit v1.2.3