paquito 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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