Module: Paquito::Types

Defined in:
lib/paquito/types.rb,
lib/paquito/types/active_record_packer.rb

Defined Under Namespace

Classes: ActiveRecordPacker, CustomTypesRegistry

Constant Summary collapse

TIME_FORMAT =

Do not change those formats, this would break current codecs.

"q< L<"
TIME_WITH_ZONE_FORMAT =
"q< L< a*"
DATE_TIME_FORMAT =
"s< C C C C q< L< c C"
DATE_FORMAT =
"s< C C"
MAX_UINT32 =
(2**32) - 1
MAX_INT64 =
(2**63) - 1
SERIALIZE_METHOD =
:as_pack
SERIALIZE_PROC =
SERIALIZE_METHOD.to_proc
DESERIALIZE_METHOD =
:from_pack
TYPES =
[
  {
    code: 0,
    class: "Symbol",
    version: 0,
    packer: Symbol.method_defined?(:name) ? :name.to_proc : :to_s.to_proc,
    unpacker: :to_sym.to_proc,
    optimized_symbols_parsing: true,
  }.freeze,
  {
    code: 1,
    class: "Time",
    version: 0,
    packer: method(:time_pack_deprecated),
    unpacker: method(:time_unpack_deprecated),
  }.freeze,
  {
    code: 2,
    class: "DateTime",
    version: 0,
    packer: method(:datetime_pack_deprecated),
    unpacker: method(:datetime_unpack_deprecated),
  }.freeze,
  {
    code: 3,
    class: "Date",
    version: 0,
    packer: method(:date_pack),
    unpacker: method(:date_unpack),
  }.freeze,
  {
    code: 4,
    class: "BigDecimal",
    version: 0,
    packer: :_dump,
    unpacker: ::BigDecimal.method(:_load),
  }.freeze,
  # { code: 5, class: "Range" }, do not recycle that code
  {
    code: 6,
    class: "ActiveRecord::Base",
    version: 0,
    packer: ->(value) { ActiveRecordPacker.dump(value) },
    unpacker: ->(value) { ActiveRecordPacker.load(value) },
  }.freeze,
  {
    code: 7,
    class: "ActiveSupport::HashWithIndifferentAccess",
    version: 0,
    packer: method(:hash_with_indifferent_access_pack),
    unpacker: method(:hash_with_indifferent_access_unpack),
    recursive: true,
  }.freeze,
  {
    code: 8,
    class: "ActiveSupport::TimeWithZone",
    version: 0,
    packer: method(:time_with_zone_deprecated_pack),
    unpacker: method(:time_with_zone_deprecated_unpack),
  }.freeze,
  {
    code: 9,
    class: "Set",
    version: 0,
    packer: ->(value, packer) { packer.write(value.to_a) },
    unpacker: ->(unpacker) { unpacker.read.to_set },
    recursive: true,
  }.freeze,
  # { code: 10, class: "Integer" }, reserved for oversized Integer
  {
    code: 11,
    class: "Time",
    version: 1,
    recursive: true,
    packer: method(:time_pack),
    unpacker: method(:time_unpack),
  }.freeze,
  {
    code: 12,
    class: "DateTime",
    version: 1,
    recursive: true,
    packer: method(:datetime_pack),
    unpacker: method(:datetime_unpack),
  }.freeze,
  {
    code: 13,
    class: "ActiveSupport::TimeWithZone",
    version: 1,
    recursive: true,
    packer: method(:time_with_zone_pack),
    unpacker: method(:time_with_zone_unpack),
  }.freeze,
  # { code: 127, class: "Object" }, reserved for serializable Object type
]

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.date_pack(value) ⇒ Object



160
161
162
# File 'lib/paquito/types.rb', line 160

def date_pack(value)
  [value.year, value.month, value.day].pack(DATE_FORMAT)
end

.date_unpack(payload) ⇒ Object



164
165
166
167
# File 'lib/paquito/types.rb', line 164

def date_unpack(payload)
  year, month, day = payload.unpack(DATE_FORMAT)
  ::Date.new(year, month, day)
end

.datetime_pack(value, packer) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/paquito/types.rb', line 212

def datetime_pack(value, packer)
  packer.write(value.year)
  packer.write(value.month)
  packer.write(value.day)
  packer.write(value.hour)
  packer.write(value.minute)

  sec = value.sec + value.sec_fraction
  packer.write(sec.numerator)
  packer.write(sec.denominator)

  offset = value.offset
  packer.write(offset.numerator)
  packer.write(offset.denominator)
end

.datetime_pack_deprecated(value) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/paquito/types.rb', line 107

def datetime_pack_deprecated(value)
  sec = value.sec + value.sec_fraction
  offset = value.offset

  if sec.numerator > MAX_INT64 || sec.denominator > MAX_UINT32
    raise PackError, "DateTime#sec_fraction out of bounds (#{sec.inspect}), see: https://github.com/Shopify/paquito/issues/26"
  end

  if offset.numerator > MAX_INT64 || offset.denominator > MAX_UINT32
    raise PackError, "DateTime#offset out of bounds (#{offset.inspect}), see: https://github.com/Shopify/paquito/issues/26"
  end

  [
    value.year,
    value.month,
    value.day,
    value.hour,
    value.minute,
    sec.numerator,
    sec.denominator,
    offset.numerator,
    offset.denominator,
  ].pack(DATE_TIME_FORMAT)
end

.datetime_unpack(unpacker) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
# File 'lib/paquito/types.rb', line 228

def datetime_unpack(unpacker)
  ::DateTime.new(
    unpacker.read, # year
    unpacker.read, # month
    unpacker.read, # day
    unpacker.read, # hour
    unpacker.read, # minute
    Rational(unpacker.read, unpacker.read), # sec fraction
    Rational(unpacker.read, unpacker.read), # offset fraction
  )
end

.datetime_unpack_deprecated(payload) ⇒ Object



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
# File 'lib/paquito/types.rb', line 132

def datetime_unpack_deprecated(payload)
  (
    year,
    month,
    day,
    hour,
    minute,
    sec_numerator,
    sec_denominator,
    offset_numerator,
    offset_denominator,
  ) = payload.unpack(DATE_TIME_FORMAT)

  begin
    ::DateTime.new(
      year,
      month,
      day,
      hour,
      minute,
      Rational(sec_numerator, sec_denominator),
      Rational(offset_numerator, offset_denominator),
    )
  rescue ZeroDivisionError
    raise UnpackError, "Corrupted DateTime object, see: https://github.com/Shopify/paquito/issues/26"
  end
end

.define_custom_type(klass, packer: nil, unpacker:) ⇒ Object



428
429
430
# File 'lib/paquito/types.rb', line 428

def define_custom_type(klass, packer: nil, unpacker:)
  CustomTypesRegistry.register(klass, packer: packer, unpacker: unpacker)
end

.hash_with_indifferent_access_pack(value, packer) ⇒ Object



169
170
171
172
173
174
175
# File 'lib/paquito/types.rb', line 169

def hash_with_indifferent_access_pack(value, packer)
  unless value.instance_of?(ActiveSupport::HashWithIndifferentAccess)
    raise PackError.new("cannot pack HashWithIndifferentClass subclass", value)
  end

  packer.write(value.to_h)
end

.hash_with_indifferent_access_unpack(unpacker) ⇒ Object



177
178
179
# File 'lib/paquito/types.rb', line 177

def hash_with_indifferent_access_unpack(unpacker)
  ActiveSupport::HashWithIndifferentAccess.new(unpacker.read)
end

.register(factory, types, format_version: Paquito.format_version) ⇒ Object



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/paquito/types.rb', line 375

def register(factory, types, format_version: Paquito.format_version)
  types.each do |type|
    # Up to Rails 7 ActiveSupport::TimeWithZone#name returns "Time"
    name = if defined?(ActiveSupport::TimeWithZone) && type == ActiveSupport::TimeWithZone
      "ActiveSupport::TimeWithZone"
    else
      type.name
    end

    matching_types = TYPES.select { |t| t[:class] == name }

    # If multiple types are registered for the same class, the last one will be used for
    # packing. So we sort all matching types so that the active one is registered last.
    past_types, future_types = matching_types.partition { |t| t.fetch(:version) <= format_version }
    if past_types.empty?
      raise KeyError, "No type found for #{name.inspect} with format_version=#{format_version}"
    end

    past_types.sort_by! { |t| t.fetch(:version) }
    (future_types + past_types).each do |type_attributes|
      factory.register_type(
        type_attributes.fetch(:code),
        type,
        type_attributes,
      )
    end
  end
end

.register_serializable_type(factory) ⇒ Object



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/paquito/types.rb', line 404

def register_serializable_type(factory)
  factory.register_type(
    127,
    Object,
    packer: ->(value) do
      packer = CustomTypesRegistry.packer(value)
      class_name = value.class.to_s
      factory.dump([packer.call(value), class_name])
    end,
    unpacker: ->(value) do
      payload, class_name = factory.load(value)

      begin
        klass = Object.const_get(class_name)
      rescue NameError
        raise ClassMissingError, "missing #{class_name} class"
      end

      unpacker = CustomTypesRegistry.unpacker(klass)
      unpacker.call(payload)
    end,
  )
end

.time_pack(value, packer) ⇒ Object



196
197
198
199
200
# File 'lib/paquito/types.rb', line 196

def time_pack(value, packer)
  packer.write(value.tv_sec)
  packer.write(value.tv_nsec)
  packer.write(value.utc_offset)
end

.time_pack_deprecated(value) ⇒ Object



88
89
90
91
92
93
94
95
# File 'lib/paquito/types.rb', line 88

def time_pack_deprecated(value)
  rational = value.to_r
  if rational.numerator > MAX_INT64 || rational.denominator > MAX_UINT32
    raise PackError, "Time instance out of bounds (#{rational.inspect}), see: https://github.com/Shopify/paquito/issues/26"
  end

  [rational.numerator, rational.denominator].pack(TIME_FORMAT)
end

.time_unpack_deprecated(payload) ⇒ Object



97
98
99
100
101
102
103
104
105
# File 'lib/paquito/types.rb', line 97

def time_unpack_deprecated(payload)
  numerator, denominator = payload.unpack(TIME_FORMAT)
  at = begin
    Rational(numerator, denominator)
  rescue ZeroDivisionError
    raise UnpackError, "Corrupted Time object, see: https://github.com/Shopify/paquito/issues/26"
  end
  Time.at(at).utc
end

.time_with_zone_deprecated_pack(value) ⇒ Object



181
182
183
184
185
186
187
# File 'lib/paquito/types.rb', line 181

def time_with_zone_deprecated_pack(value)
  [
    value.utc.to_i,
    (value.time.sec_fraction * 1_000_000_000).to_i,
    value.time_zone.name,
  ].pack(TIME_WITH_ZONE_FORMAT)
end

.time_with_zone_deprecated_unpack(payload) ⇒ Object



189
190
191
192
193
194
# File 'lib/paquito/types.rb', line 189

def time_with_zone_deprecated_unpack(payload)
  sec, nsec, time_zone_name = payload.unpack(TIME_WITH_ZONE_FORMAT)
  time = Time.at(sec, nsec, :nsec, in: 0).utc
  time_zone = ::Time.find_zone(time_zone_name)
  ActiveSupport::TimeWithZone.new(time, time_zone)
end

.time_with_zone_pack(value, packer) ⇒ Object



240
241
242
243
244
245
# File 'lib/paquito/types.rb', line 240

def time_with_zone_pack(value, packer)
  time = value.utc
  packer.write(time.tv_sec)
  packer.write(time.tv_nsec)
  packer.write(value.time_zone.name)
end

Instance Method Details

#time_unpack(unpacker) ⇒ Object



203
204
205
# File 'lib/paquito/types.rb', line 203

def time_unpack(unpacker)
  ::Time.at_without_coercion(unpacker.read, unpacker.read, :nanosecond, in: unpacker.read)
end

#time_with_zone_unpack(unpacker) ⇒ Object



248
249
250
251
252
# File 'lib/paquito/types.rb', line 248

def time_with_zone_unpack(unpacker)
  utc = ::Time.at_without_coercion(unpacker.read, unpacker.read, :nanosecond, in: "UTC")
  time_zone = ::Time.find_zone(unpacker.read)
  ActiveSupport::TimeWithZone.new(utc, time_zone)
end