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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +20 -1
- data/lib/paquito/codec_factory.rb +2 -2
- data/lib/paquito/types.rb +159 -39
- data/lib/paquito/version.rb +1 -1
- data/lib/paquito.rb +5 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0724a150e6626e863f78a24b665cd7cc8284ff612539ccbd3c58efc2117974f7
|
4
|
+
data.tar.gz: a2e2d693ed46a3fdb2c57f07fa0e60c11ce1a838eb262bcf19c0c95744b6ad4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95b0aa7760f86aa056ff07d909dd3b4c03a2f4016b00dbdc7ceb0e31e5619c7aea822b20946ee8ded630111670211b95ff47e1a799944946bc4d9e7df18563bb
|
7
|
+
data.tar.gz: 4fa26995582cde6675e79c81382ef72fe981d2d335daabb33a44e280c76f69df5fd95171bd2dff4c5070296ab95151393470685f6d43ea3afe7571ae51272a8d
|
data/.rubocop.yml
CHANGED
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
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::
|
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
|
-
|
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
|
-
|
87
|
+
{
|
83
88
|
code: 1,
|
89
|
+
class: "Time",
|
90
|
+
version: 0,
|
84
91
|
packer: ->(value) do
|
85
|
-
rational = value.
|
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
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
149
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
189
|
-
|
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
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
347
|
+
127,
|
228
348
|
Object,
|
229
349
|
packer: ->(value) do
|
230
350
|
packer = CustomTypesRegistry.packer(value)
|
data/lib/paquito/version.rb
CHANGED
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.
|
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:
|
11
|
+
date: 2023-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|