paquito 0.9.2 → 0.11.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: caa49c77308b2c81f81faa1308ab462dd0bcc15f91244ab080800ffe0e2ebd9f
4
+ data.tar.gz: 8909a37d22acd65c197c2c38dbc2e8f2e8e717d61f848996058e1c764cdadba7
5
5
  SHA512:
6
- metadata.gz: ba2c02fa73d7b1a27577e36c8f8e2a2ea73a57446939e0519ea3d1fbd1ababf6af4c0bdda47bfba074ce0af60ae0269c8ea838abcb5ad0cba75a5d2f81848297
7
- data.tar.gz: fb527d860bfd14dd71c42c75320fd694ad1335ca4e21b7edacf798612b55125bd19d1a5bf7126f8e4770ad0b68c448d003fe8fad450b7686d7e8a97ad4921dbb
6
+ metadata.gz: 25f2d1f9e05ddf14b7edbb798acd253e0590089a6d70296c493d8f8cbb63a2abdbe2b4fcfee7e72d201226b1a729b31c8b3cd1cf240d000abd3db21a4e0ff2d4
7
+ data.tar.gz: 034dea4d9c9e276af49d5aa892e2231eeda24d275a32f846da246334fec167a8a3568972908ac75d8c413f794edfb418253732e903d5e7073e0625a9cff8019d
@@ -23,7 +23,7 @@ jobs:
23
23
  strategy:
24
24
  fail-fast: false
25
25
  matrix:
26
- ruby: [ ruby-head, '3.1', '3.0', '2.7' ]
26
+ ruby: [ ruby-head, '3.2', '3.1', '3.0', '2.7' ]
27
27
  steps:
28
28
  - name: Checkout
29
29
  uses: actions/checkout@v3
@@ -0,0 +1,22 @@
1
+ name: Contributor License Agreement (CLA)
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, synchronize]
6
+ issue_comment:
7
+ types: [created]
8
+
9
+ jobs:
10
+ cla:
11
+ runs-on: ubuntu-latest
12
+ if: |
13
+ (github.event.issue.pull_request
14
+ && !github.event.issue.pull_request.merged_at
15
+ && contains(github.event.comment.body, 'signed')
16
+ )
17
+ || (github.event.pull_request && !github.event.pull_request.merged)
18
+ steps:
19
+ - uses: Shopify/shopify-cla-action@v1
20
+ with:
21
+ github-token: ${{ secrets.GITHUB_TOKEN }}
22
+ cla-token: ${{ secrets.CLA_TOKEN }}
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,17 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.11.0
4
+
5
+ * Convert some lambdas into proper methods to make them more discoverable by profilers.
6
+ * Optimize `Time` and `ActiveSupport::TimeWithZone` serializers when `active_support/core_ext/time/calculations` is loaded.
7
+
8
+ # 0.10.0
9
+
10
+ * Introduce a new version `1` format that better handles `Time` and `DateTime` objects. It can be enabled by setting `Paquito.format_version = 1`.
11
+ *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.
12
+ 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.
13
+
14
+ *This new format will be the default in paquito 1.0.*
3
15
 
4
16
  # 0.9.2
5
17
 
data/Gemfile CHANGED
@@ -6,8 +6,8 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
- gem "activesupport"
10
- gem "activerecord"
9
+ gem "activesupport", ">= 7"
10
+ gem "activerecord", ">= 7"
11
11
  gem "sqlite3"
12
12
  gem "benchmark-ips"
13
13
 
data/Gemfile.lock CHANGED
@@ -1,71 +1,67 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- paquito (0.9.2)
4
+ paquito (0.11.0)
5
5
  msgpack (>= 1.5.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.0.4)
11
- activesupport (= 7.0.4)
12
- activerecord (7.0.4)
13
- activemodel (= 7.0.4)
14
- activesupport (= 7.0.4)
15
- activesupport (7.0.4)
10
+ activemodel (7.0.7.1)
11
+ activesupport (= 7.0.7.1)
12
+ activerecord (7.0.7.1)
13
+ activemodel (= 7.0.7.1)
14
+ activesupport (= 7.0.7.1)
15
+ activesupport (7.0.7.1)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
17
  i18n (>= 1.6, < 2)
18
18
  minitest (>= 5.1)
19
19
  tzinfo (~> 2.0)
20
20
  ast (2.4.2)
21
- benchmark-ips (2.10.0)
21
+ benchmark-ips (2.12.0)
22
22
  byebug (11.1.3)
23
- concurrent-ruby (1.1.10)
24
- i18n (1.12.0)
23
+ concurrent-ruby (1.2.2)
24
+ i18n (1.14.1)
25
25
  concurrent-ruby (~> 1.0)
26
26
  json (2.6.3)
27
- mini_portile2 (2.8.0)
28
- minitest (5.16.3)
29
- msgpack (1.6.0)
27
+ minitest (5.18.0)
28
+ msgpack (1.7.2)
30
29
  parallel (1.22.1)
31
- parser (3.1.3.0)
30
+ parser (3.2.1.1)
32
31
  ast (~> 2.4.1)
33
32
  rainbow (3.1.1)
34
33
  rake (13.0.6)
35
- regexp_parser (2.6.1)
34
+ regexp_parser (2.7.0)
36
35
  rexml (3.2.5)
37
- rubocop (1.40.0)
36
+ rubocop (1.48.1)
38
37
  json (~> 2.3)
39
38
  parallel (~> 1.10)
40
- parser (>= 3.1.2.1)
39
+ parser (>= 3.2.0.0)
41
40
  rainbow (>= 2.2.2, < 4.0)
42
41
  regexp_parser (>= 1.8, < 3.0)
43
42
  rexml (>= 3.2.5, < 4.0)
44
- rubocop-ast (>= 1.23.0, < 2.0)
43
+ rubocop-ast (>= 1.26.0, < 2.0)
45
44
  ruby-progressbar (~> 1.7)
46
- unicode-display_width (>= 1.4.0, < 3.0)
47
- rubocop-ast (1.24.0)
48
- parser (>= 3.1.1.0)
49
- rubocop-shopify (2.10.1)
50
- rubocop (~> 1.35)
51
- ruby-progressbar (1.11.0)
52
- sorbet-runtime (0.5.10577)
53
- sqlite3 (1.5.4)
54
- mini_portile2 (~> 2.8.0)
55
- sqlite3 (1.5.4-x86_64-darwin)
56
- sqlite3 (1.5.4-x86_64-linux)
57
- tzinfo (2.0.5)
45
+ unicode-display_width (>= 2.4.0, < 3.0)
46
+ rubocop-ast (1.27.0)
47
+ parser (>= 3.2.1.0)
48
+ rubocop-shopify (2.12.0)
49
+ rubocop (~> 1.44)
50
+ ruby-progressbar (1.13.0)
51
+ sorbet-runtime (0.5.10712)
52
+ sqlite3 (1.6.1-arm64-darwin)
53
+ sqlite3 (1.6.1-x86_64-linux)
54
+ tzinfo (2.0.6)
58
55
  concurrent-ruby (~> 1.0)
59
- unicode-display_width (2.3.0)
56
+ unicode-display_width (2.4.2)
60
57
 
61
58
  PLATFORMS
62
- ruby
63
- x86_64-darwin-20
59
+ arm64-darwin
64
60
  x86_64-linux
65
61
 
66
62
  DEPENDENCIES
67
- activerecord
68
- activesupport
63
+ activerecord (>= 7)
64
+ activesupport (>= 7)
69
65
  benchmark-ips
70
66
  byebug
71
67
  minitest (~> 5.0)
@@ -77,4 +73,4 @@ DEPENDENCIES
77
73
  sqlite3
78
74
 
79
75
  BUNDLED WITH
80
- 2.3.8
76
+ 2.3.22
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.
data/dev.yml CHANGED
@@ -2,7 +2,7 @@ name: paquito
2
2
 
3
3
  up:
4
4
  - ruby:
5
- version: 2.7.5
5
+ version: 2.7.8
6
6
  - bundler
7
7
 
8
8
  commands:
@@ -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)
@@ -4,6 +4,7 @@ module Paquito
4
4
  class CoderChain
5
5
  def initialize(*coders)
6
6
  @coders = coders.flatten.map { |c| Paquito.cast(c) }
7
+ @reverse_coders = @coders.reverse
7
8
  end
8
9
 
9
10
  def dump(object)
@@ -14,7 +15,7 @@ module Paquito
14
15
 
15
16
  def load(payload)
16
17
  object = payload
17
- @coders.reverse_each { |c| object = c.load(object) }
18
+ @reverse_coders.each { |c| object = c.load(object) }
18
19
  object
19
20
  end
20
21
  end
@@ -3,8 +3,17 @@
3
3
  module Paquito
4
4
  class SafeYAML
5
5
  ALL_SYMBOLS = [].freeze # Restricting symbols isn't really useful since symbols are no longer immortal
6
- BASE_PERMITTED_CLASSNAMES = ["TrueClass", "FalseClass", "NilClass", "Numeric", "String", "Array", "Hash",
7
- "Integer", "Float",].freeze
6
+ BASE_PERMITTED_CLASSNAMES = [
7
+ "TrueClass",
8
+ "FalseClass",
9
+ "NilClass",
10
+ "Numeric",
11
+ "String",
12
+ "Array",
13
+ "Hash",
14
+ "Integer",
15
+ "Float",
16
+ ].freeze
8
17
 
9
18
  def initialize(permitted_classes: [], deprecated_classes: [], aliases: false)
10
19
  permitted_classes += BASE_PERMITTED_CLASSNAMES
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,54 +75,66 @@ 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" => {
77
- code: 0,
78
- packer: Symbol.method_defined?(:name) ? :name.to_proc : :to_s.to_proc,
79
- unpacker: :to_sym.to_proc,
80
- optimized_symbols_parsing: true,
81
- }.freeze,
82
- "Time" => {
83
- code: 1,
84
- packer: ->(value) do
85
- rational = value.utc.to_r
86
- [rational.numerator, rational.denominator].pack(TIME_FORMAT)
87
- end,
88
- unpacker: ->(value) do
89
- numerator, denominator = value.unpack(TIME_FORMAT)
90
- Time.at(Rational(numerator, denominator)).utc
91
- end,
92
- }.freeze,
93
- "DateTime" => {
94
- code: 2,
95
- packer: ->(value) do
96
- sec = value.sec + value.sec_fraction
97
- offset = value.offset
98
- [
99
- value.year,
100
- value.month,
101
- value.day,
102
- value.hour,
103
- value.minute,
104
- sec.numerator,
105
- sec.denominator,
106
- offset.numerator,
107
- offset.denominator,
108
- ].pack(DATE_TIME_FORMAT)
109
- end,
110
- unpacker: ->(value) do
111
- (
112
- year,
113
- month,
114
- day,
115
- hour,
116
- minute,
117
- sec_numerator,
118
- sec_denominator,
119
- offset_numerator,
120
- offset_denominator,
121
- ) = value.unpack(DATE_TIME_FORMAT)
122
- DateTime.new( # rubocop:disable Style/DateTime
78
+ class << self
79
+ def time_pack_deprecated(value)
80
+ rational = value.to_r
81
+ if rational.numerator > MAX_INT64 || rational.denominator > MAX_UINT32
82
+ raise PackError, "Time instance out of bounds (#{rational.inspect}), see: https://github.com/Shopify/paquito/issues/26"
83
+ end
84
+
85
+ [rational.numerator, rational.denominator].pack(TIME_FORMAT)
86
+ end
87
+
88
+ def time_unpack_deprecated(payload)
89
+ numerator, denominator = payload.unpack(TIME_FORMAT)
90
+ at = begin
91
+ Rational(numerator, denominator)
92
+ rescue ZeroDivisionError
93
+ raise UnpackError, "Corrupted Time object, see: https://github.com/Shopify/paquito/issues/26"
94
+ end
95
+ Time.at(at).utc
96
+ end
97
+
98
+ def datetime_pack_deprecated(value)
99
+ sec = value.sec + value.sec_fraction
100
+ offset = value.offset
101
+
102
+ if sec.numerator > MAX_INT64 || sec.denominator > MAX_UINT32
103
+ raise PackError, "DateTime#sec_fraction out of bounds (#{sec.inspect}), see: https://github.com/Shopify/paquito/issues/26"
104
+ end
105
+
106
+ if offset.numerator > MAX_INT64 || offset.denominator > MAX_UINT32
107
+ raise PackError, "DateTime#offset out of bounds (#{offset.inspect}), see: https://github.com/Shopify/paquito/issues/26"
108
+ end
109
+
110
+ [
111
+ value.year,
112
+ value.month,
113
+ value.day,
114
+ value.hour,
115
+ value.minute,
116
+ sec.numerator,
117
+ sec.denominator,
118
+ offset.numerator,
119
+ offset.denominator,
120
+ ].pack(DATE_TIME_FORMAT)
121
+ end
122
+
123
+ def datetime_unpack_deprecated(payload)
124
+ (
125
+ year,
126
+ month,
127
+ day,
128
+ hour,
129
+ minute,
130
+ sec_numerator,
131
+ sec_denominator,
132
+ offset_numerator,
133
+ offset_denominator,
134
+ ) = payload.unpack(DATE_TIME_FORMAT)
135
+
136
+ begin
137
+ ::DateTime.new(
123
138
  year,
124
139
  month,
125
140
  day,
@@ -128,71 +143,215 @@ module Paquito
128
143
  Rational(sec_numerator, sec_denominator),
129
144
  Rational(offset_numerator, offset_denominator),
130
145
  )
131
- end,
146
+ rescue ZeroDivisionError
147
+ raise UnpackError, "Corrupted DateTime object, see: https://github.com/Shopify/paquito/issues/26"
148
+ end
149
+ end
150
+
151
+ def date_pack(value)
152
+ [value.year, value.month, value.day].pack(DATE_FORMAT)
153
+ end
154
+
155
+ def date_unpack(payload)
156
+ year, month, day = payload.unpack(DATE_FORMAT)
157
+ ::Date.new(year, month, day)
158
+ end
159
+
160
+ def hash_with_indifferent_access_pack(value, packer)
161
+ unless value.instance_of?(ActiveSupport::HashWithIndifferentAccess)
162
+ raise PackError.new("cannot pack HashWithIndifferentClass subclass", value)
163
+ end
164
+
165
+ packer.write(value.to_h)
166
+ end
167
+
168
+ def hash_with_indifferent_access_unpack(unpacker)
169
+ ActiveSupport::HashWithIndifferentAccess.new(unpacker.read)
170
+ end
171
+
172
+ def time_with_zone_deprecated_pack(value)
173
+ [
174
+ value.utc.to_i,
175
+ (value.time.sec_fraction * 1_000_000_000).to_i,
176
+ value.time_zone.name,
177
+ ].pack(TIME_WITH_ZONE_FORMAT)
178
+ end
179
+
180
+ def time_with_zone_deprecated_unpack(payload)
181
+ sec, nsec, time_zone_name = payload.unpack(TIME_WITH_ZONE_FORMAT)
182
+ time = Time.at(sec, nsec, :nsec, in: 0).utc
183
+ time_zone = ::Time.find_zone(time_zone_name)
184
+ ActiveSupport::TimeWithZone.new(time, time_zone)
185
+ end
186
+
187
+ def time_pack(value, packer)
188
+ packer.write(value.tv_sec)
189
+ packer.write(value.tv_nsec)
190
+ packer.write(value.utc_offset)
191
+ end
192
+
193
+ if ::Time.respond_to?(:at_without_coercion) # Ref: https://github.com/rails/rails/pull/50268
194
+ def time_unpack(unpacker)
195
+ ::Time.at_without_coercion(unpacker.read, unpacker.read, :nanosecond, in: unpacker.read)
196
+ end
197
+ else
198
+ def time_unpack(unpacker)
199
+ ::Time.at(unpacker.read, unpacker.read, :nanosecond, in: unpacker.read)
200
+ end
201
+ end
202
+
203
+ def datetime_pack(value, packer)
204
+ packer.write(value.year)
205
+ packer.write(value.month)
206
+ packer.write(value.day)
207
+ packer.write(value.hour)
208
+ packer.write(value.minute)
209
+
210
+ sec = value.sec + value.sec_fraction
211
+ packer.write(sec.numerator)
212
+ packer.write(sec.denominator)
213
+
214
+ offset = value.offset
215
+ packer.write(offset.numerator)
216
+ packer.write(offset.denominator)
217
+ end
218
+
219
+ def datetime_unpack(unpacker)
220
+ ::DateTime.new(
221
+ unpacker.read, # year
222
+ unpacker.read, # month
223
+ unpacker.read, # day
224
+ unpacker.read, # hour
225
+ unpacker.read, # minute
226
+ Rational(unpacker.read, unpacker.read), # sec fraction
227
+ Rational(unpacker.read, unpacker.read), # offset fraction
228
+ )
229
+ end
230
+
231
+ def time_with_zone_pack(value, packer)
232
+ time = value.utc
233
+ packer.write(time.tv_sec)
234
+ packer.write(time.tv_nsec)
235
+ packer.write(value.time_zone.name)
236
+ end
237
+
238
+ if ::Time.respond_to?(:at_without_coercion) # Ref: https://github.com/rails/rails/pull/50268
239
+ def time_with_zone_unpack(unpacker)
240
+ utc = ::Time.at_without_coercion(unpacker.read, unpacker.read, :nanosecond, in: "UTC")
241
+ time_zone = ::Time.find_zone(unpacker.read)
242
+ ActiveSupport::TimeWithZone.new(utc, time_zone)
243
+ end
244
+ else
245
+ def time_with_zone_unpack(unpacker)
246
+ utc = ::Time.at(unpacker.read, unpacker.read, :nanosecond, in: "UTC")
247
+ time_zone = ::Time.find_zone(unpacker.read)
248
+ ActiveSupport::TimeWithZone.new(utc, time_zone)
249
+ end
250
+ end
251
+ end
252
+
253
+ TYPES = [
254
+ {
255
+ code: 0,
256
+ class: "Symbol",
257
+ version: 0,
258
+ packer: Symbol.method_defined?(:name) ? :name.to_proc : :to_s.to_proc,
259
+ unpacker: :to_sym.to_proc,
260
+ optimized_symbols_parsing: true,
261
+ }.freeze,
262
+ {
263
+ code: 1,
264
+ class: "Time",
265
+ version: 0,
266
+ packer: method(:time_pack_deprecated),
267
+ unpacker: method(:time_unpack_deprecated),
268
+ }.freeze,
269
+ {
270
+ code: 2,
271
+ class: "DateTime",
272
+ version: 0,
273
+ packer: method(:datetime_pack_deprecated),
274
+ unpacker: method(:datetime_unpack_deprecated),
132
275
  }.freeze,
133
- "Date" => {
276
+ {
134
277
  code: 3,
135
- packer: ->(value) do
136
- [value.year, value.month, value.day].pack(DATE_FORMAT)
137
- end,
138
- unpacker: ->(value) do
139
- year, month, day = value.unpack(DATE_FORMAT)
140
- Date.new(year, month, day)
141
- end,
278
+ class: "Date",
279
+ version: 0,
280
+ packer: method(:date_pack),
281
+ unpacker: method(:date_unpack),
142
282
  }.freeze,
143
- "BigDecimal" => {
283
+ {
144
284
  code: 4,
285
+ class: "BigDecimal",
286
+ version: 0,
145
287
  packer: :_dump,
146
- unpacker: BigDecimal.method(:_load),
288
+ unpacker: ::BigDecimal.method(:_load),
147
289
  }.freeze,
148
- # Range => { code: 0x05 }, do not recycle that code
149
- "ActiveRecord::Base" => {
290
+ # { code: 5, class: "Range" }, do not recycle that code
291
+ {
150
292
  code: 6,
293
+ class: "ActiveRecord::Base",
294
+ version: 0,
151
295
  packer: ->(value) { ActiveRecordPacker.dump(value) },
152
296
  unpacker: ->(value) { ActiveRecordPacker.load(value) },
153
297
  }.freeze,
154
- "ActiveSupport::HashWithIndifferentAccess" => {
298
+ {
155
299
  code: 7,
156
- packer: ->(value, packer) do
157
- unless value.instance_of?(ActiveSupport::HashWithIndifferentAccess)
158
- raise PackError.new("cannot pack HashWithIndifferentClass subclass", value)
159
- end
160
-
161
- packer.write(value.to_h)
162
- end,
163
- unpacker: ->(unpacker) { ActiveSupport::HashWithIndifferentAccess.new(unpacker.read) },
300
+ class: "ActiveSupport::HashWithIndifferentAccess",
301
+ version: 0,
302
+ packer: method(:hash_with_indifferent_access_pack),
303
+ unpacker: method(:hash_with_indifferent_access_unpack),
164
304
  recursive: true,
165
- },
166
- "ActiveSupport::TimeWithZone" => {
305
+ }.freeze,
306
+ {
167
307
  code: 8,
168
- packer: ->(value) do
169
- [
170
- value.utc.to_i,
171
- (value.time.sec_fraction * 1_000_000_000).to_i,
172
- value.time_zone.name,
173
- ].pack(TIME_WITH_ZONE_FORMAT)
174
- end,
175
- unpacker: ->(value) do
176
- sec, nsec, time_zone_name = value.unpack(TIME_WITH_ZONE_FORMAT)
177
- time = Time.at(sec, nsec, :nsec, in: 0).utc
178
- time_zone = ::Time.find_zone(time_zone_name)
179
- ActiveSupport::TimeWithZone.new(time, time_zone)
180
- end,
181
- },
182
- "Set" => {
308
+ class: "ActiveSupport::TimeWithZone",
309
+ version: 0,
310
+ packer: method(:time_with_zone_deprecated_pack),
311
+ unpacker: method(:time_with_zone_deprecated_unpack),
312
+ }.freeze,
313
+ {
183
314
  code: 9,
315
+ class: "Set",
316
+ version: 0,
184
317
  packer: ->(value, packer) { packer.write(value.to_a) },
185
318
  unpacker: ->(unpacker) { unpacker.read.to_set },
186
319
  recursive: true,
187
- },
188
- # Integer => { code: 10 }, reserved for oversized Integer
189
- # Object => { code: 127 }, reserved for serializable Object type
190
- }
320
+ }.freeze,
321
+ # { code: 10, class: "Integer" }, reserved for oversized Integer
322
+ {
323
+ code: 11,
324
+ class: "Time",
325
+ version: 1,
326
+ recursive: true,
327
+ packer: method(:time_pack),
328
+ unpacker: method(:time_unpack),
329
+ }.freeze,
330
+ {
331
+ code: 12,
332
+ class: "DateTime",
333
+ version: 1,
334
+ recursive: true,
335
+ packer: method(:datetime_pack),
336
+ unpacker: method(:datetime_unpack),
337
+ }.freeze,
338
+ {
339
+ code: 13,
340
+ class: "ActiveSupport::TimeWithZone",
341
+ version: 1,
342
+ recursive: true,
343
+ packer: method(:time_with_zone_pack),
344
+ unpacker: method(:time_with_zone_unpack),
345
+ }.freeze,
346
+ # { code: 127, class: "Object" }, reserved for serializable Object type
347
+ ]
191
348
  begin
192
349
  require "msgpack/bigint"
193
350
 
194
- TYPES["Integer"] = {
351
+ TYPES << {
195
352
  code: 10,
353
+ class: "Integer",
354
+ version: 0,
196
355
  packer: MessagePack::Bigint.method(:to_msgpack_ext),
197
356
  unpacker: MessagePack::Bigint.method(:from_msgpack_ext),
198
357
  oversized_integer_extension: true,
@@ -204,7 +363,7 @@ module Paquito
204
363
  TYPES.freeze
205
364
 
206
365
  class << self
207
- def register(factory, types)
366
+ def register(factory, types, format_version: Paquito.format_version)
208
367
  types.each do |type|
209
368
  # Up to Rails 7 ActiveSupport::TimeWithZone#name returns "Time"
210
369
  name = if defined?(ActiveSupport::TimeWithZone) && type == ActiveSupport::TimeWithZone
@@ -213,18 +372,29 @@ module Paquito
213
372
  type.name
214
373
  end
215
374
 
216
- type_attributes = TYPES.fetch(name)
217
- factory.register_type(
218
- type_attributes.fetch(:code),
219
- type,
220
- type_attributes,
221
- )
375
+ matching_types = TYPES.select { |t| t[:class] == name }
376
+
377
+ # If multiple types are registered for the same class, the last one will be used for
378
+ # packing. So we sort all matching types so that the active one is registered last.
379
+ past_types, future_types = matching_types.partition { |t| t.fetch(:version) <= format_version }
380
+ if past_types.empty?
381
+ raise KeyError, "No type found for #{name.inspect} with format_version=#{format_version}"
382
+ end
383
+
384
+ past_types.sort_by! { |t| t.fetch(:version) }
385
+ (future_types + past_types).each do |type_attributes|
386
+ factory.register_type(
387
+ type_attributes.fetch(:code),
388
+ type,
389
+ type_attributes,
390
+ )
391
+ end
222
392
  end
223
393
  end
224
394
 
225
395
  def register_serializable_type(factory)
226
396
  factory.register_type(
227
- 0x7f,
397
+ 127,
228
398
  Object,
229
399
  packer: ->(value) do
230
400
  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.11.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.11.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-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -32,6 +32,7 @@ extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
34
  - ".github/workflows/ci.yml"
35
+ - ".github/workflows/cla.yml"
35
36
  - ".gitignore"
36
37
  - ".rubocop.yml"
37
38
  - CHANGELOG.md
@@ -91,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
94
  requirements: []
94
- rubygems_version: 3.3.3
95
+ rubygems_version: 3.4.22
95
96
  signing_key:
96
97
  specification_version: 4
97
98
  summary: Framework for defining efficient and extendable serializers