1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
# frozen_string_literal: true
require "set"
module GraphQL
class Subscriptions
# Serialization helpers for passing subscription data around.
# @api private
module Serialize
GLOBALID_KEY = "__gid__"
SYMBOL_KEY = "__sym__"
SYMBOL_KEYS_KEY = "__sym_keys__"
TIMESTAMP_KEY = "__timestamp__"
TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%z" # eg '2020-01-01 23:59:59.123456789+05:00'
OPEN_STRUCT_KEY = "__ostruct__"
module_function
# @param str [String] A serialized object from {.dump}
# @return [Object] An object equivalent to the one passed to {.dump}
def load(str)
parsed_obj = JSON.parse(str)
load_value(parsed_obj)
end
# @param obj [Object] Some subscription-related data to dump
# @return [String] The stringified object
def dump(obj)
JSON.generate(dump_value(obj), quirks_mode: true)
end
# This is for turning objects into subscription scopes.
# It's a one-way transformation, can't reload this :'(
# @param obj [Object]
# @return [String]
def dump_recursive(obj)
case
when obj.is_a?(Array)
obj.map { |i| dump_recursive(i) }.join(':')
when obj.is_a?(Hash)
obj.map { |k, v| "#{dump_recursive(k)}:#{dump_recursive(v)}" }.join(":")
when obj.is_a?(GraphQL::Schema::InputObject)
dump_recursive(obj.to_h)
when obj.respond_to?(:to_gid_param)
obj.to_gid_param
when obj.respond_to?(:to_param)
obj.to_param
else
obj.to_s
end
end
class << self
private
# @param value [Object] A parsed JSON object
# @return [Object] An object that load Global::Identification recursive
def load_value(value)
if value.is_a?(Array)
is_gids = (v1 = value[0]).is_a?(Hash) && v1.size == 1 && v1[GLOBALID_KEY]
if is_gids
# Assume it's an array of global IDs
ids = value.map { |v| v[GLOBALID_KEY] }
GlobalID::Locator.locate_many(ids)
else
value.map { |item| load_value(item) }
end
elsif value.is_a?(Hash)
if value.size == 1
case value.keys.first # there's only 1 key
when GLOBALID_KEY
GlobalID::Locator.locate(value[GLOBALID_KEY])
when SYMBOL_KEY
value[SYMBOL_KEY].to_sym
when TIMESTAMP_KEY
timestamp_class_name, *timestamp_args = value[TIMESTAMP_KEY]
timestamp_class = Object.const_get(timestamp_class_name)
if defined?(ActiveSupport::TimeWithZone) && timestamp_class <= ActiveSupport::TimeWithZone
zone_name, timestamp_s = timestamp_args
zone = ActiveSupport::TimeZone[zone_name]
raise "Zone #{zone_name} not found, unable to deserialize" unless zone
zone.strptime(timestamp_s, TIMESTAMP_FORMAT)
else
timestamp_s = timestamp_args.first
timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
end
when OPEN_STRUCT_KEY
ostruct_values = load_value(value[OPEN_STRUCT_KEY])
OpenStruct.new(ostruct_values)
else
key = value.keys.first
{ key => load_value(value[key]) }
end
else
loaded_h = {}
sym_keys = value.fetch(SYMBOL_KEYS_KEY, [])
value.each do |k, v|
if k == SYMBOL_KEYS_KEY
next
end
if sym_keys.include?(k)
k = k.to_sym
end
loaded_h[k] = load_value(v)
end
loaded_h
end
else
value
end
end
# @param obj [Object] Some subscription-related data to dump
# @return [Object] The object that converted Global::Identification
def dump_value(obj)
if obj.is_a?(Array)
obj.map{|item| dump_value(item)}
elsif obj.is_a?(Hash)
symbol_keys = nil
dumped_h = {}
obj.each do |k, v|
dumped_h[k.to_s] = dump_value(v)
if k.is_a?(Symbol)
symbol_keys ||= Set.new
symbol_keys << k.to_s
end
end
if symbol_keys
dumped_h[SYMBOL_KEYS_KEY] = symbol_keys.to_a
end
dumped_h
elsif obj.is_a?(Symbol)
{ SYMBOL_KEY => obj.to_s }
elsif obj.respond_to?(:to_gid_param)
{GLOBALID_KEY => obj.to_gid_param}
elsif defined?(ActiveSupport::TimeWithZone) && obj.is_a?(ActiveSupport::TimeWithZone) && obj.class.name != Time.name
# This handles a case where Rails prior to 7 would
# make the class ActiveSupport::TimeWithZone return "Time" for
# its name. In Rails 7, it will now return "ActiveSupport::TimeWithZone",
# which happens to be incompatible with expectations we have
# with what a Time class supports ( notably, strptime in `load_value` ).
#
# This now passes along the name of the zone, such that a future deserialization
# of this string will use the correct time zone from the ActiveSupport TimeZone
# list to produce the time.
#
{ TIMESTAMP_KEY => [obj.class.name, obj.time_zone.name, obj.strftime(TIMESTAMP_FORMAT)] }
elsif obj.is_a?(Date) || obj.is_a?(Time)
# DateTime extends Date; for TimeWithZone, call `.utc` first.
{ TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
elsif obj.is_a?(OpenStruct)
{ OPEN_STRUCT_KEY => dump_value(obj.to_h) }
elsif defined?(ActiveRecord::Relation) && obj.is_a?(ActiveRecord::Relation)
dump_value(obj.to_a)
else
obj
end
end
end
end
end
end
|