Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce custom serializers to ActiveJob arguments #30941

Merged
merged 15 commits into from Feb 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions 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.
1 change: 1 addition & 0 deletions activejob/lib/active_job.rb
Expand Up @@ -33,6 +33,7 @@ module ActiveJob

autoload :Base
autoload :QueueAdapters
autoload :Serializers
autoload :ConfiguredJob
autoload :TestCase
autoload :TestHelper
Expand Down
29 changes: 19 additions & 10 deletions activejob/lib/active_job/arguments.rb
Expand Up @@ -44,13 +44,24 @@ def deserialize(arguments)
end

private

# :nodoc:
GLOBALID_KEY = "_aj_globalid".freeze
# :nodoc:
SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze
# :nodoc:
WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze
private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
# :nodoc:
OBJECT_SERIALIZER_KEY = "_aj_serialized"

# :nodoc:
RESERVED_KEYS = [
GLOBALID_KEY, GLOBALID_KEY.to_sym,
SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym,
WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
]
private_constant :RESERVED_KEYS

def serialize_argument(argument)
case argument
Expand All @@ -70,7 +81,7 @@ def serialize_argument(argument)
result[SYMBOL_KEYS_KEY] = symbol_keys
result
else
raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
Serializers.serialize(argument)
end
end

Expand All @@ -85,6 +96,8 @@ def deserialize_argument(argument)
when Hash
if serialized_global_id?(argument)
deserialize_global_id argument
elsif custom_serialized?(argument)
Serializers.deserialize(argument)
else
deserialize_hash(argument)
end
Expand All @@ -101,6 +114,10 @@ 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)
Expand All @@ -117,14 +134,6 @@ def deserialize_hash(serialized_hash)
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
Expand Down
1 change: 1 addition & 0 deletions activejob/lib/active_job/base.rb
Expand Up @@ -59,6 +59,7 @@ module ActiveJob #:nodoc:
# * SerializationError - Error class for serialization errors.
class Base
include Core
include Serializers
include QueueAdapter
include QueueName
include QueuePriority
Expand Down
6 changes: 6 additions & 0 deletions activejob/lib/active_job/railtie.rb
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions activejob/lib/active_job/serializers.rb
@@ -0,0 +1,62 @@
# frozen_string_literal: true

require "set"

module ActiveJob
# The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
# and to add new ones. It also has helpers to serialize/deserialize objects
module Serializers
extend ActiveSupport::Autoload
extend ActiveSupport::Concern

autoload :ObjectSerializer
autoload :SymbolSerializer
autoload :DurationSerializer
autoload :DateSerializer
autoload :TimeSerializer
autoload :DateTimeSerializer

mattr_accessor :_additional_serializers
self._additional_serializers = Set.new

class << self
# Returns serialized representative of the passed object.
# Will look up through all known serializers.
# Raises `ActiveJob::SerializationError` if it can't find a proper serializer.
def serialize(argument)
serializer = serializers.detect { |s| s.serialize?(argument) }
raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
serializer.serialize(argument)
end

# Returns deserialized object.
# Will look up through all known serializers.
# If no serializers found will raise `ArgumentError`
def deserialize(argument)
serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name

serializer = serializer_name.safe_constantize
raise ArgumentError, "Serializer #{serializer_name} is not know" unless serializer

serializer.deserialize(argument)
end

# Returns list of known serializers
def serializers
self._additional_serializers
end

# Adds a new serializer to a list of known serializers
def add_serializers(*new_serializers)
self._additional_serializers += new_serializers
end
end

add_serializers SymbolSerializer,
DurationSerializer,
DateTimeSerializer,
DateSerializer,
TimeSerializer
end
end
21 changes: 21 additions & 0 deletions 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
21 changes: 21 additions & 0 deletions 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
24 changes: 24 additions & 0 deletions activejob/lib/active_job/serializers/duration_serializer.rb
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module ActiveJob
module Serializers
class DurationSerializer < ObjectSerializer # :nodoc:
def serialize(duration)
super("value" => duration.value, "parts" => Arguments.serialize(duration.parts))
end

def deserialize(hash)
value = hash["value"]
parts = Arguments.deserialize(hash["parts"])

klass.new(value, parts)
end

private

def klass
ActiveSupport::Duration
end
end
end
end
54 changes: 54 additions & 0 deletions activejob/lib/active_job/serializers/object_serializer.rb
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module ActiveJob
module Serializers
# Base class for serializing and deserializing custom times.
#
# Example
#
# class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
# def serialize(money)
# super("cents" => money.cents, "currency" => money.currency)
# end
#
# def deserialize(hash)
# Money.new(hash["cents"], hash["currency"])
# end
#
# private
#
# def klass
# Money
# end
# end
class ObjectSerializer
include Singleton

class << self
delegate :serialize?, :serialize, :deserialize, to: :instance
end

# Determines if an argument should be serialized by a serializer.
def serialize?(argument)
argument.is_a?(klass)
end

# Serializes an argument to a JSON primitive type.
def serialize(hash)
{ Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash)
end

# Deserilizes an argument form a JSON primiteve type.
def deserialize(_argument)
raise NotImplementedError
end

protected

# The class of the object that will be serialized.
def klass
raise NotImplementedError
end
end
end
end
21 changes: 21 additions & 0 deletions activejob/lib/active_job/serializers/symbol_serializer.rb
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module ActiveJob
module Serializers
class SymbolSerializer < ObjectSerializer # :nodoc:
def serialize(argument)
super("value" => argument.to_s)
end

def deserialize(argument)
argument["value"].to_sym
end

private

def klass
Symbol
end
end
end
end
21 changes: 21 additions & 0 deletions 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