paquito 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fdea3e761f0c5cf3d5a99a42f31e55b645921e6597fb027c560de421b55f7ae3
4
+ data.tar.gz: f3dc6d6b15d149bb1d00115b79ef7f337af01197b5961a14fa9baead46e2586a
5
+ SHA512:
6
+ metadata.gz: ac2d37fe39cace2096b041200eb2c2fac5fa2b6024747420a69645097642bce3ef35d66dce1c53d0ff801a921c46f84ca68fdb5a123ffd5c71e52f2a492bc0b8
7
+ data.tar.gz: e0e6e8329734bfed770fc1cf8b1c964e7c7902a55686d7182cd078c26a3255d4e7af91e17e2898e35ef9c3515df1d38c5243417d18190cd968f4232e85231e59
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ rubies:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: [ ruby-head, '3.0', '2.7' ]
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v2
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ - name: Install dependencies
20
+ run: |
21
+ rm Gemfile.lock
22
+ bundle install
23
+ - name: Run test
24
+ run: bundle exec rake
25
+ - name: Install gem
26
+ run: bundle exec rake install
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.7
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in paquito.gemspec
6
+ gemspec
7
+
8
+ gem "msgpack", github: "msgpack/msgpack-ruby" # Waiting for release of https://github.com/msgpack/msgpack-ruby/pull/220
9
+
10
+ gem "rake", "~> 13.0"
11
+ gem "activesupport", ">= 7.0.0.alpha2"
12
+ gem "activerecord", ">= 7.0.0.alpha2"
13
+ gem "sqlite3"
14
+
15
+ gem "minitest", "~> 5.0"
16
+
17
+ gem "rubocop"
18
+ gem "rubocop-shopify", "~> 2.0", require: false
19
+ gem "byebug"
20
+
21
+ gem "sorbet-runtime"
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ GIT
2
+ remote: https://github.com/msgpack/msgpack-ruby.git
3
+ revision: ea20842ea732f4d625fd30c7b8f67655d73652f2
4
+ specs:
5
+ msgpack (1.4.2)
6
+
7
+ PATH
8
+ remote: .
9
+ specs:
10
+ paquito (0.1.0)
11
+ msgpack
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activemodel (7.0.0.alpha2)
17
+ activesupport (= 7.0.0.alpha2)
18
+ activerecord (7.0.0.alpha2)
19
+ activemodel (= 7.0.0.alpha2)
20
+ activesupport (= 7.0.0.alpha2)
21
+ activesupport (7.0.0.alpha2)
22
+ concurrent-ruby (~> 1.0, >= 1.0.2)
23
+ i18n (>= 1.6, < 2)
24
+ minitest (>= 5.1)
25
+ tzinfo (~> 2.0)
26
+ ast (2.4.2)
27
+ byebug (11.1.3)
28
+ concurrent-ruby (1.1.9)
29
+ i18n (1.8.10)
30
+ concurrent-ruby (~> 1.0)
31
+ minitest (5.14.4)
32
+ parallel (1.21.0)
33
+ parser (3.0.2.0)
34
+ ast (~> 2.4.1)
35
+ rainbow (3.0.0)
36
+ rake (13.0.6)
37
+ regexp_parser (2.1.1)
38
+ rexml (3.2.5)
39
+ rubocop (1.22.1)
40
+ parallel (~> 1.10)
41
+ parser (>= 3.0.0.0)
42
+ rainbow (>= 2.2.2, < 4.0)
43
+ regexp_parser (>= 1.8, < 3.0)
44
+ rexml
45
+ rubocop-ast (>= 1.12.0, < 2.0)
46
+ ruby-progressbar (~> 1.7)
47
+ unicode-display_width (>= 1.4.0, < 3.0)
48
+ rubocop-ast (1.12.0)
49
+ parser (>= 3.0.1.1)
50
+ rubocop-shopify (2.3.0)
51
+ rubocop (~> 1.22)
52
+ ruby-progressbar (1.11.0)
53
+ sorbet-runtime (0.5.9209)
54
+ sqlite3 (1.4.2)
55
+ tzinfo (2.0.4)
56
+ concurrent-ruby (~> 1.0)
57
+ unicode-display_width (2.1.0)
58
+
59
+ PLATFORMS
60
+ ruby
61
+ x86_64-darwin-20
62
+ x86_64-linux
63
+
64
+ DEPENDENCIES
65
+ activerecord (>= 7.0.0.alpha2)
66
+ activesupport (>= 7.0.0.alpha2)
67
+ byebug
68
+ minitest (~> 5.0)
69
+ msgpack!
70
+ paquito!
71
+ rake (~> 13.0)
72
+ rubocop
73
+ rubocop-shopify (~> 2.0)
74
+ sorbet-runtime
75
+ sqlite3
76
+
77
+ BUNDLED WITH
78
+ 2.2.22
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Paquito
2
+
3
+ `Paquito` provies utility classes to define optimized and evolutive serializers.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'paquito'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install paquito
20
+
21
+ ## Usage
22
+
23
+ ### `chain`
24
+
25
+ `Paquito::CoderChain` allows to combine two or more serializers into one.
26
+
27
+ Example:
28
+
29
+ ```ruby
30
+ compressed_yaml_coder = Paquito.chain(YAML, Zlib)
31
+ payload = compressed_yaml_coder.dump({ foo: 42 }) # YAML compressed with gzip
32
+ compressed_yaml_coder.load(payload) # => { foo: 42 }
33
+ ```
34
+
35
+ ### `ConditionalCompressor`
36
+
37
+ `Paquito::ConditionalCompressor` compress payloads if they are over a defined size.
38
+
39
+ Example:
40
+ ```ruby
41
+ coder = Paquito::ConditionalCompressor.new(Zlib, 256)
42
+ coder.dump("foo") # => "\x00foo"
43
+ coder.dump("foo" * 500) # => "\x01<compressed-data....>"
44
+ ```
45
+
46
+ ### `SingleBytePrefixVersion`
47
+
48
+ `Paquito::SingleBytePrefixVersion` prepends a version prefix to the payloads, which then allow to seemlessly transition from
49
+ different serialization methods.
50
+
51
+ The first argument is the current version used for newly generated payloads.
52
+
53
+ Example:
54
+
55
+ ```ruby
56
+ coder = Paquito::SingleBytePrefixVersion.new(1,
57
+ 0 => YAML,
58
+ 1 => JSON,
59
+ 2 => MessagePack,
60
+ )
61
+ coder.dump([1]) # => "\x01[1]"
62
+ coder.load("\x00---\n:foo: 42") # => { foo: 42 }
63
+ ```
64
+
65
+ ### `CommentPrefixVersion`
66
+
67
+ Similar to the single byte prefix, but meant to be human readable and to allow for migrating unversionned payloads.
68
+
69
+ Payload without a version prefix are assumed to be version `0`.
70
+
71
+ The first argument is the current version used for newly generated payloads.
72
+
73
+ Example:
74
+
75
+ ```ruby
76
+ coder = Paquito::CommentPrefixVersion.new(1,
77
+ 0 => YAML,
78
+ 1 => JSON,
79
+ )
80
+
81
+ coder.load("---\n:foo: 42") # => { foo: 42 }
82
+ coder.dump([1]) # => "#☠1☢\n[1]"
83
+ ```
84
+
85
+ ### `allow_nil`
86
+
87
+ In some situation where you'd rather not serialize `nil`, you can use the `Paquito.allow_nil` shorthand:
88
+
89
+ ```ruby
90
+ coder = Paquito.allow_nil(Marshal)
91
+ coder.dump(nil) # => nil
92
+ coder.load(nil) # => nil
93
+ ```
94
+
95
+ ### `TranslateErrors`
96
+
97
+ If you do need to handle serialization or deserialization errors, for instance to fallback to acting like a cache miss,
98
+ `Paquito::TranslateErrors` translate all underlying exceptions into `Paquito::Error` descendants.
99
+
100
+ Example:
101
+
102
+ ```ruby
103
+ coder = Paquito::TranslateErrors.new(Paquito::CoderChain.new(YAML, Zlib))
104
+ coder.load("\x00") # => Paquito::UnpackError (buffer error)
105
+ ```
106
+
107
+ ### `CodecFactory`
108
+
109
+ `Paquito::CodecFactory` is a utility facade to create advanced `MessagePack` factories with support for common Ruby
110
+ and Rails types.
111
+
112
+ Example
113
+ ```ruby
114
+ coder = Paquito::CodecFactory.build([Symbol, Set])
115
+ coder.load(coder.dump(%i(foo bar).to_set)) # => #<Set: {:foo, :bar}>
116
+ ```
117
+
118
+ ### `TypedStruct`
119
+
120
+ `Paquito::TypedStruct` is a opt-in Sorbet runtime plugin that allows `T::Struct` classes to be serializable. You need to explicitly include the module in the `T::Struct` classes that you will be serializing.
121
+
122
+ Example
123
+ ```ruby
124
+ class MyStruct < T::Struct
125
+ include Paquito::TypedStruct
126
+
127
+ prop :foo, String
128
+ prop :bar, Integer
129
+ end
130
+
131
+ my_struct = MyStruct.new(foo: "foo", bar: 1)
132
+
133
+ my_struct.as_pack # => [26450, "foo", 1]
134
+ MyStruct.from_pack([26450, "foo", 1]) # => <MyStruct bar=1, foo="foo">
135
+ ```
136
+
137
+ ## Active Record utilities
138
+
139
+ `paquito` doesn't not depend on `rails` nor any of it's componement, however it does provide some optional utilities for it.
140
+
141
+ ### `SerializedColumn`
142
+
143
+ `Paquito::SerializedColumn` allows to decorate any encoder to behave like Rails's builtin `YAMLColumn`
144
+
145
+ Example:
146
+
147
+ ```ruby
148
+ class Shop < ActiveRecord::Base
149
+ serialize :settings, Paquito::SerializedColumn.new(
150
+ Paquito::CommentPrefixVersion.new(
151
+ 1,
152
+ 0 => YAML,
153
+ 1 => Paquito::CodecFactory.build([Symbol]),
154
+ ),
155
+ Hash,
156
+ attribute_name: :settings,
157
+ )
158
+ end
159
+
160
+ Shop.new.settings # => {}
161
+ ```
162
+
163
+ ## Development
164
+
165
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
166
+
167
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
168
+
169
+ ## Contributing
170
+
171
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/paquito.
172
+
173
+ ## License
174
+
175
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ suites = [:vanilla, :activesupport, :activerecord]
7
+ namespace :test do
8
+ suites.each do |suite|
9
+ Rake::TestTask.new(suite) do |t|
10
+ t.libs << "test/#{suite}"
11
+ t.libs << "lib"
12
+ t.test_files = FileList["test/#{suite}/**/*_test.rb"]
13
+ end
14
+ end
15
+ end
16
+
17
+ task test: suites.map { |s| "test:#{s}" }
18
+
19
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "paquito"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dev.yml ADDED
@@ -0,0 +1,12 @@
1
+ name: paquito
2
+
3
+ up:
4
+ - ruby:
5
+ version: 2.7.4
6
+ - bundler
7
+
8
+ commands:
9
+ test:
10
+ syntax: ""
11
+ desc: 'run all the tests'
12
+ run: bundle exec rake test
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paquito/errors"
4
+
5
+ module Paquito
6
+ class ActiveRecordCoder
7
+ class << self
8
+ def dump(record)
9
+ instances = InstanceTracker.new
10
+ serialized_associations = serialize_associations(record, instances)
11
+ serialized_records = instances.map { |r| serialize_record(r) }
12
+ [serialized_associations, *serialized_records]
13
+ end
14
+
15
+ def load(payload)
16
+ instances = InstanceTracker.new
17
+ serialized_associations, *serialized_records = payload
18
+ serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) }
19
+ deserialize_associations(serialized_associations, instances)
20
+ end
21
+
22
+ private
23
+
24
+ # Records without associations, or which have already been visited before,
25
+ # are serialized by their id alone.
26
+ #
27
+ # Records with associations are serialized as a two-element array including
28
+ # their id and the record's association cache.
29
+ #
30
+ def serialize_associations(record, instances)
31
+ return unless record
32
+
33
+ if (id = instances.lookup(record))
34
+ payload = id
35
+ else
36
+ payload = instances.push(record)
37
+
38
+ cached_associations = record.class.reflect_on_all_associations.select do |reflection|
39
+ record.association_cached?(reflection.name)
40
+ end
41
+
42
+ unless cached_associations.empty?
43
+ serialized_associations = cached_associations.map do |reflection|
44
+ association = record.association(reflection.name)
45
+
46
+ serialized_target = if reflection.collection?
47
+ association.target.map { |target_record| serialize_associations(target_record, instances) }
48
+ else
49
+ serialize_associations(association.target, instances)
50
+ end
51
+
52
+ [reflection.name, serialized_target]
53
+ end
54
+
55
+ payload = [payload, serialized_associations]
56
+ end
57
+ end
58
+
59
+ payload
60
+ end
61
+
62
+ def deserialize_associations(payload, instances)
63
+ return unless payload
64
+
65
+ id, associations = payload
66
+ record = instances.fetch(id)
67
+
68
+ associations&.each do |name, serialized_target|
69
+ begin
70
+ association = record.association(name)
71
+ rescue ActiveRecord::AssociationNotFoundError
72
+ raise AssociationMissingError, "undefined association: #{name}"
73
+ end
74
+
75
+ target = if association.reflection.collection?
76
+ serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) }
77
+ else
78
+ deserialize_associations(serialized_target, instances)
79
+ end
80
+
81
+ association.target = target
82
+ end
83
+
84
+ record
85
+ end
86
+
87
+ def serialize_record(record)
88
+ [record.class.name, attributes_for_database(record)]
89
+ end
90
+
91
+ def attributes_for_database(record)
92
+ attributes = record.attributes_for_database
93
+ attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
94
+ attributes
95
+ end
96
+
97
+ def deserialize_record(class_name, attributes_from_database)
98
+ begin
99
+ klass = Object.const_get(class_name)
100
+ rescue NameError
101
+ raise ClassMissingError, "undefined class: #{class_name}"
102
+ end
103
+ klass.instantiate(attributes_from_database)
104
+ end
105
+ end
106
+
107
+ class Error < ::Paquito::Error
108
+ end
109
+
110
+ class ClassMissingError < Error
111
+ end
112
+
113
+ class AssociationMissingError < Error
114
+ end
115
+
116
+ class InstanceTracker
117
+ def initialize
118
+ @instances = []
119
+ @ids = {}.compare_by_identity
120
+ end
121
+
122
+ def map(...)
123
+ @instances.map(...)
124
+ end
125
+
126
+ def fetch(...)
127
+ @instances.fetch(...)
128
+ end
129
+
130
+ def push(instance)
131
+ id = @ids[instance] = @instances.size
132
+ @instances << instance
133
+ id
134
+ end
135
+
136
+ def lookup(instance)
137
+ @ids[instance]
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paquito
4
+ class AllowNil
5
+ def initialize(coder)
6
+ @coder = Paquito.cast(coder)
7
+ end
8
+
9
+ def dump(object)
10
+ return nil if object.nil?
11
+ @coder.dump(object)
12
+ end
13
+
14
+ def load(payload)
15
+ return nil if payload.nil?
16
+ @coder.load(payload)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paquito/types"
4
+ require "paquito/coder_chain"
5
+
6
+ module Paquito
7
+ class CodecFactory
8
+ def self.build(types, freeze: false, serializable_type: false)
9
+ factory =
10
+ if types.empty? && !serializable_type
11
+ MessagePack
12
+ else
13
+ MessagePack::Factory.new
14
+ end
15
+ Types.register(factory, types) unless types.empty?
16
+ Types.register_serializable_type(factory) if serializable_type
17
+ MessagePackCodec.new(factory, freeze: freeze)
18
+ end
19
+
20
+ class MessagePackCodec
21
+ def initialize(factory, freeze: false)
22
+ @factory = factory
23
+ @freeze = freeze
24
+ end
25
+
26
+ def dump(object)
27
+ @factory.dump(object)
28
+ rescue NoMethodError => error
29
+ raise PackError.new(error.message, error.receiver)
30
+ end
31
+
32
+ def load(payload)
33
+ @factory.load(payload, freeze: @freeze)
34
+ rescue MessagePack::UnpackError => error
35
+ raise UnpackError, error.message
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paquito
4
+ class CoderChain
5
+ def initialize(*coders)
6
+ @coders = coders.flatten.map { |c| Paquito.cast(c) }
7
+ end
8
+
9
+ def dump(object)
10
+ payload = object
11
+ @coders.each { |c| payload = c.dump(payload) }
12
+ payload
13
+ end
14
+
15
+ def load(payload)
16
+ object = payload
17
+ @coders.reverse_each { |c| object = c.load(object) }
18
+ object
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paquito
4
+ class CommentPrefixVersion
5
+ PREFIX = "#\u2620"
6
+ SUFFIX = "\u2622\n"
7
+ VERSION_POSITION = PREFIX.bytesize
8
+
9
+ HEADER_SLICE = (0..(PREFIX.bytesize + SUFFIX.bytesize))
10
+ PAYLOAD_SLICE = (PREFIX.bytesize + 1 + SUFFIX.bytesize)..-1
11
+ DEFAULT_VERSION = 0
12
+
13
+ def initialize(current_version, coders)
14
+ unless (0..9).cover?(current_version) && coders.keys.all? { |version| (0..9).cover?(version) }
15
+ raise ArgumentError, "CommentPrefixVersion versions must be between 0 and 9"
16
+ end
17
+
18
+ @current_version = current_version
19
+ @coders = coders.transform_values { |c| Paquito.cast(c) }.freeze
20
+ @current_coder = coders.fetch(current_version)
21
+ end
22
+
23
+ def dump(object)
24
+ prefix = +"#{PREFIX}#{@current_version}#{SUFFIX}"
25
+ payload = @current_coder.dump(object)
26
+ if payload.encoding == Encoding::BINARY
27
+ prefix.b << payload
28
+ else
29
+ prefix << payload
30
+ end
31
+ end
32
+
33
+ def load(payload)
34
+ payload_version, serial = extract_version(payload)
35
+
36
+ coder = @coders.fetch(payload_version) do
37
+ raise UnsupportedCodec, "Unsupported packer version #{payload_version}"
38
+ end
39
+ coder.load(serial)
40
+ end
41
+
42
+ private
43
+
44
+ def extract_version(serial)
45
+ header = serial.byteslice(HEADER_SLICE)&.force_encoding(Encoding::UTF_8)
46
+ unless header.start_with?(PREFIX) && header.end_with?(SUFFIX)
47
+ return [DEFAULT_VERSION, serial]
48
+ end
49
+
50
+ version = header.getbyte(VERSION_POSITION) - 48 # ASCII byte to number
51
+ [version, serial.byteslice(PAYLOAD_SLICE) || ""]
52
+ end
53
+ end
54
+ end