paquito 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: beedd98334166781382fbe4d773b19f27e58ea8cfe8c83fa211c745c7c752deb
4
- data.tar.gz: 2912c724ee66b3b9547477e56aeb2bc1a0622128c41bf06b0be221c54eb0e8b0
3
+ metadata.gz: 0724a150e6626e863f78a24b665cd7cc8284ff612539ccbd3c58efc2117974f7
4
+ data.tar.gz: a2e2d693ed46a3fdb2c57f07fa0e60c11ce1a838eb262bcf19c0c95744b6ad4a
5
5
  SHA512:
6
- metadata.gz: ba2c02fa73d7b1a27577e36c8f8e2a2ea73a57446939e0519ea3d1fbd1ababf6af4c0bdda47bfba074ce0af60ae0269c8ea838abcb5ad0cba75a5d2f81848297
7
- data.tar.gz: fb527d860bfd14dd71c42c75320fd694ad1335ca4e21b7edacf798612b55125bd19d1a5bf7126f8e4770ad0b68c448d003fe8fad450b7686d7e8a97ad4921dbb
6
+ metadata.gz: 95b0aa7760f86aa056ff07d909dd3b4c03a2f4016b00dbdc7ceb0e31e5619c7aea822b20946ee8ded630111670211b95ff47e1a799944946bc4d9e7df18563bb
7
+ data.tar.gz: 4fa26995582cde6675e79c81382ef72fe981d2d335daabb33a44e280c76f69df5fd95171bd2dff4c5070296ab95151393470685f6d43ea3afe7571ae51272a8d
data/.rubocop.yml CHANGED
@@ -6,3 +6,6 @@ AllCops:
6
6
 
7
7
  Style/ClassMethodsDefinitions:
8
8
  Enabled: false
9
+
10
+ Style/DateTime:
11
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.10.0
4
+
5
+ * Introduce a new version `1` format that better handles `Time` and `DateTime` objects. It can be enabled by setting `Paquito.format_version = 1`.
6
+ *IMPORTANT*: If you are upgrading from previous versions, you MUST first fully deploy the new version of the gem prior to enabling the new format.
7
+ If you don't you may notice some `UnpackError` during the code rollout, which may be fine if you only use Paquito for ephemeral cache data.
8
+
9
+ *This new format will be the default in paquito 1.0.*
3
10
 
4
11
  # 0.9.2
5
12
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- paquito (0.9.2)
4
+ paquito (0.10.0)
5
5
  msgpack (>= 1.5.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -18,6 +18,25 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install paquito
20
20
 
21
+ ## Upgrade process
22
+
23
+ `Paquito` being a serialization library, takes extra care in always being able to deserialize payloads serialized from previous versions.
24
+
25
+ However the inverse may not always be true, so when upgrading `Paquito` it is essential to first upgrade the gem without any applicative code change, so
26
+ that all Ruby processes in production are able to read the new format.
27
+
28
+ Additionally format changes can be controlled through `Paquito.format_version` that directly maps with the gem major version.
29
+
30
+ For example, `paquito 0.10.0` introduce a new serialization format for `Time` and `DateTime` objects, but retain `Paquito.format_version = 0`.
31
+
32
+ The upgrade process is as follows:
33
+
34
+ - Upgrade to `paquito ~> 0.10`.
35
+ - Fully deploy that upgrade (if multiple applications are sharing Paquito payloads it means upgrading all the applications).
36
+ - Set `Paquito.format_version = 1` or upgrade to `paquito ~> 1.0`.
37
+
38
+ Generally speaking it's heavily recommended to carefully read the CHANGELOG and not to skip intermediary versions.
39
+
21
40
  ## Usage
22
41
 
23
42
  ### `chain`
@@ -75,7 +94,7 @@ Additionally, you can pass a distinct serializer for strings only:
75
94
  Example:
76
95
 
77
96
  ```ruby
78
- coder = Paquito::SingleBytePrefixVersion.new(
97
+ coder = Paquito::SingleBytePrefixVersionWithStringBypass.new(
79
98
  1,
80
99
  { 0 => YAML, 1 => JSON },
81
100
  Paquito::ConditionalCompressor.new(Zlib, 1024), # Large strings will be compressed but not serialized in JSON.
@@ -5,14 +5,14 @@ require "paquito/coder_chain"
5
5
 
6
6
  module Paquito
7
7
  class CodecFactory
8
- def self.build(types = [], freeze: false, serializable_type: false, pool: 1)
8
+ def self.build(types = [], freeze: false, serializable_type: false, pool: 1, format_version: Paquito.format_version)
9
9
  factory = if types.empty? && !serializable_type
10
10
  MessagePack::DefaultFactory
11
11
  else
12
12
  MessagePack::Factory.new
13
13
  end
14
14
 
15
- Types.register(factory, types) unless types.empty?
15
+ Types.register(factory, types, format_version: format_version) unless types.empty?
16
16
  Types.register_serializable_type(factory) if serializable_type
17
17
 
18
18
  if pool && pool > 0 && factory.respond_to?(:pool)
data/lib/paquito/types.rb CHANGED
@@ -12,6 +12,9 @@ module Paquito
12
12
  DATE_TIME_FORMAT = "s< C C C C q< L< c C"
13
13
  DATE_FORMAT = "s< C C"
14
14
 
15
+ MAX_UINT32 = (2**32) - 1
16
+ MAX_INT64 = (2**63) - 1
17
+
15
18
  SERIALIZE_METHOD = :as_pack
16
19
  SERIALIZE_PROC = SERIALIZE_METHOD.to_proc
17
20
  DESERIALIZE_METHOD = :from_pack
@@ -72,29 +75,53 @@ module Paquito
72
75
 
73
76
  # Do not change any #code, this would break current codecs.
74
77
  # New types can be added as long as they have unique #code.
75
- TYPES = {
76
- "Symbol" => {
78
+ TYPES = [
79
+ {
77
80
  code: 0,
81
+ class: "Symbol",
82
+ version: 0,
78
83
  packer: Symbol.method_defined?(:name) ? :name.to_proc : :to_s.to_proc,
79
84
  unpacker: :to_sym.to_proc,
80
85
  optimized_symbols_parsing: true,
81
86
  }.freeze,
82
- "Time" => {
87
+ {
83
88
  code: 1,
89
+ class: "Time",
90
+ version: 0,
84
91
  packer: ->(value) do
85
- rational = value.utc.to_r
92
+ rational = value.to_r
93
+ if rational.numerator > MAX_INT64 || rational.denominator > MAX_UINT32
94
+ raise PackError, "Time instance out of bounds (#{rational.inspect}), see: https://github.com/Shopify/paquito/issues/26"
95
+ end
96
+
86
97
  [rational.numerator, rational.denominator].pack(TIME_FORMAT)
87
98
  end,
88
99
  unpacker: ->(value) do
89
100
  numerator, denominator = value.unpack(TIME_FORMAT)
90
- Time.at(Rational(numerator, denominator)).utc
101
+ at = begin
102
+ Rational(numerator, denominator)
103
+ rescue ZeroDivisionError
104
+ raise UnpackError, "Corrupted Time object, see: https://github.com/Shopify/paquito/issues/26"
105
+ end
106
+ Time.at(at).utc
91
107
  end,
92
108
  }.freeze,
93
- "DateTime" => {
109
+ {
94
110
  code: 2,
111
+ class: "DateTime",
112
+ version: 0,
95
113
  packer: ->(value) do
96
114
  sec = value.sec + value.sec_fraction
97
115
  offset = value.offset
116
+
117
+ if sec.numerator > MAX_INT64 || sec.denominator > MAX_UINT32
118
+ raise PackError, "DateTime#sec_fraction out of bounds (#{sec.inspect}), see: https://github.com/Shopify/paquito/issues/26"
119
+ end
120
+
121
+ if offset.numerator > MAX_INT64 || offset.denominator > MAX_UINT32
122
+ raise PackError, "DateTime#offset out of bounds (#{offset.inspect}), see: https://github.com/Shopify/paquito/issues/26"
123
+ end
124
+
98
125
  [
99
126
  value.year,
100
127
  value.month,
@@ -119,40 +146,53 @@ module Paquito
119
146
  offset_numerator,
120
147
  offset_denominator,
121
148
  ) = value.unpack(DATE_TIME_FORMAT)
122
- DateTime.new( # rubocop:disable Style/DateTime
123
- year,
124
- month,
125
- day,
126
- hour,
127
- minute,
128
- Rational(sec_numerator, sec_denominator),
129
- Rational(offset_numerator, offset_denominator),
130
- )
149
+
150
+ begin
151
+ ::DateTime.new(
152
+ year,
153
+ month,
154
+ day,
155
+ hour,
156
+ minute,
157
+ Rational(sec_numerator, sec_denominator),
158
+ Rational(offset_numerator, offset_denominator),
159
+ )
160
+ rescue ZeroDivisionError
161
+ raise UnpackError, "Corrupted DateTime object, see: https://github.com/Shopify/paquito/issues/26"
162
+ end
131
163
  end,
132
164
  }.freeze,
133
- "Date" => {
165
+ {
134
166
  code: 3,
167
+ class: "Date",
168
+ version: 0,
135
169
  packer: ->(value) do
136
170
  [value.year, value.month, value.day].pack(DATE_FORMAT)
137
171
  end,
138
172
  unpacker: ->(value) do
139
173
  year, month, day = value.unpack(DATE_FORMAT)
140
- Date.new(year, month, day)
174
+ ::Date.new(year, month, day)
141
175
  end,
142
176
  }.freeze,
143
- "BigDecimal" => {
177
+ {
144
178
  code: 4,
179
+ class: "BigDecimal",
180
+ version: 0,
145
181
  packer: :_dump,
146
- unpacker: BigDecimal.method(:_load),
182
+ unpacker: ::BigDecimal.method(:_load),
147
183
  }.freeze,
148
- # Range => { code: 0x05 }, do not recycle that code
149
- "ActiveRecord::Base" => {
184
+ # { code: 5, class: "Range" }, do not recycle that code
185
+ {
150
186
  code: 6,
187
+ class: "ActiveRecord::Base",
188
+ version: 0,
151
189
  packer: ->(value) { ActiveRecordPacker.dump(value) },
152
190
  unpacker: ->(value) { ActiveRecordPacker.load(value) },
153
191
  }.freeze,
154
- "ActiveSupport::HashWithIndifferentAccess" => {
192
+ {
155
193
  code: 7,
194
+ class: "ActiveSupport::HashWithIndifferentAccess",
195
+ version: 0,
156
196
  packer: ->(value, packer) do
157
197
  unless value.instance_of?(ActiveSupport::HashWithIndifferentAccess)
158
198
  raise PackError.new("cannot pack HashWithIndifferentClass subclass", value)
@@ -162,9 +202,11 @@ module Paquito
162
202
  end,
163
203
  unpacker: ->(unpacker) { ActiveSupport::HashWithIndifferentAccess.new(unpacker.read) },
164
204
  recursive: true,
165
- },
166
- "ActiveSupport::TimeWithZone" => {
205
+ }.freeze,
206
+ {
167
207
  code: 8,
208
+ class: "ActiveSupport::TimeWithZone",
209
+ version: 0,
168
210
  packer: ->(value) do
169
211
  [
170
212
  value.utc.to_i,
@@ -178,21 +220,88 @@ module Paquito
178
220
  time_zone = ::Time.find_zone(time_zone_name)
179
221
  ActiveSupport::TimeWithZone.new(time, time_zone)
180
222
  end,
181
- },
182
- "Set" => {
223
+ }.freeze,
224
+ {
183
225
  code: 9,
226
+ class: "Set",
227
+ version: 0,
184
228
  packer: ->(value, packer) { packer.write(value.to_a) },
185
229
  unpacker: ->(unpacker) { unpacker.read.to_set },
186
230
  recursive: true,
187
- },
188
- # Integer => { code: 10 }, reserved for oversized Integer
189
- # Object => { code: 127 }, reserved for serializable Object type
190
- }
231
+ }.freeze,
232
+ # { code: 10, class: "Integer" }, reserved for oversized Integer
233
+ {
234
+ code: 11,
235
+ class: "Time",
236
+ version: 1,
237
+ recursive: true,
238
+ packer: ->(value, packer) do
239
+ packer.write(value.tv_sec)
240
+ packer.write(value.tv_nsec)
241
+ packer.write(value.utc_offset)
242
+ end,
243
+ unpacker: ->(unpacker) do
244
+ ::Time.at(unpacker.read, unpacker.read, :nanosecond, in: unpacker.read)
245
+ end,
246
+ }.freeze,
247
+ {
248
+ code: 12,
249
+ class: "DateTime",
250
+ version: 1,
251
+ recursive: true,
252
+ packer: ->(value, packer) do
253
+ packer.write(value.year)
254
+ packer.write(value.month)
255
+ packer.write(value.day)
256
+ packer.write(value.hour)
257
+ packer.write(value.minute)
258
+
259
+ sec = value.sec + value.sec_fraction
260
+ packer.write(sec.numerator)
261
+ packer.write(sec.denominator)
262
+
263
+ offset = value.offset
264
+ packer.write(offset.numerator)
265
+ packer.write(offset.denominator)
266
+ end,
267
+ unpacker: ->(unpacker) do
268
+ ::DateTime.new(
269
+ unpacker.read, # year
270
+ unpacker.read, # month
271
+ unpacker.read, # day
272
+ unpacker.read, # hour
273
+ unpacker.read, # minute
274
+ Rational(unpacker.read, unpacker.read), # sec fraction
275
+ Rational(unpacker.read, unpacker.read), # offset fraction
276
+ )
277
+ end,
278
+ }.freeze,
279
+ {
280
+ code: 13,
281
+ class: "ActiveSupport::TimeWithZone",
282
+ version: 1,
283
+ recursive: true,
284
+ packer: ->(value, packer) do
285
+ time = value.utc
286
+ packer.write(time.tv_sec)
287
+ packer.write(time.tv_nsec)
288
+ packer.write(value.time_zone.name)
289
+ end,
290
+ unpacker: ->(unpacker) do
291
+ utc = ::Time.at(unpacker.read, unpacker.read, :nanosecond, in: "UTC")
292
+ time_zone = ::Time.find_zone(unpacker.read)
293
+ ActiveSupport::TimeWithZone.new(utc, time_zone)
294
+ end,
295
+ }.freeze,
296
+ # { code: 127, class: "Object" }, reserved for serializable Object type
297
+ ]
191
298
  begin
192
299
  require "msgpack/bigint"
193
300
 
194
- TYPES["Integer"] = {
301
+ TYPES << {
195
302
  code: 10,
303
+ class: "Integer",
304
+ version: 0,
196
305
  packer: MessagePack::Bigint.method(:to_msgpack_ext),
197
306
  unpacker: MessagePack::Bigint.method(:from_msgpack_ext),
198
307
  oversized_integer_extension: true,
@@ -204,7 +313,7 @@ module Paquito
204
313
  TYPES.freeze
205
314
 
206
315
  class << self
207
- def register(factory, types)
316
+ def register(factory, types, format_version: Paquito.format_version)
208
317
  types.each do |type|
209
318
  # Up to Rails 7 ActiveSupport::TimeWithZone#name returns "Time"
210
319
  name = if defined?(ActiveSupport::TimeWithZone) && type == ActiveSupport::TimeWithZone
@@ -213,18 +322,29 @@ module Paquito
213
322
  type.name
214
323
  end
215
324
 
216
- type_attributes = TYPES.fetch(name)
217
- factory.register_type(
218
- type_attributes.fetch(:code),
219
- type,
220
- type_attributes,
221
- )
325
+ matching_types = TYPES.select { |t| t[:class] == name }
326
+
327
+ # If multiple types are registered for the same class, the last one will be used for
328
+ # packing. So we sort all matching types so that the active one is registered last.
329
+ past_types, future_types = matching_types.partition { |t| t.fetch(:version) <= format_version }
330
+ if past_types.empty?
331
+ raise KeyError, "No type found for #{name.inspect} with format_version=#{format_version}"
332
+ end
333
+
334
+ past_types.sort_by! { |t| t.fetch(:version) }
335
+ (future_types + past_types).each do |type_attributes|
336
+ factory.register_type(
337
+ type_attributes.fetch(:code),
338
+ type,
339
+ type_attributes,
340
+ )
341
+ end
222
342
  end
223
343
  end
224
344
 
225
345
  def register_serializable_type(factory)
226
346
  factory.register_type(
227
- 0x7f,
347
+ 127,
228
348
  Object,
229
349
  packer: ->(value) do
230
350
  packer = CustomTypesRegistry.packer(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paquito
4
- VERSION = "0.9.2"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/paquito.rb CHANGED
@@ -29,7 +29,12 @@ module Paquito
29
29
  autoload :FlatCacheEntryCoder, "paquito/flat_cache_entry_coder"
30
30
  autoload :ActiveRecordCoder, "paquito/active_record_coder"
31
31
 
32
+ DEFAULT_FORMAT_VERSION = 0
33
+ @format_version = DEFAULT_FORMAT_VERSION
34
+
32
35
  class << self
36
+ attr_accessor :format_version
37
+
33
38
  def cast(coder)
34
39
  if coder.respond_to?(:load) && coder.respond_to?(:dump)
35
40
  coder
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paquito
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-09 00:00:00.000000000 Z
11
+ date: 2023-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack