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 +7 -0
- data/.github/workflows/ci.yml +26 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +5 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +78 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/Rakefile +19 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/dev.yml +12 -0
- data/lib/paquito/active_record_coder.rb +141 -0
- data/lib/paquito/allow_nil.rb +19 -0
- data/lib/paquito/codec_factory.rb +39 -0
- data/lib/paquito/coder_chain.rb +21 -0
- data/lib/paquito/comment_prefix_version.rb +54 -0
- data/lib/paquito/conditional_compressor.rb +42 -0
- data/lib/paquito/deflater.rb +17 -0
- data/lib/paquito/errors.rb +19 -0
- data/lib/paquito/safe_yaml.rb +74 -0
- data/lib/paquito/serialized_column.rb +52 -0
- data/lib/paquito/single_byte_prefix_version.rb +27 -0
- data/lib/paquito/struct.rb +87 -0
- data/lib/paquito/translate_errors.rb +25 -0
- data/lib/paquito/typed_struct.rb +53 -0
- data/lib/paquito/types/active_record_packer.rb +51 -0
- data/lib/paquito/types.rb +242 -0
- data/lib/paquito/version.rb +5 -0
- data/lib/paquito.rb +47 -0
- data/paquito.gemspec +32 -0
- metadata +90 -0
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
data/.rubocop.yml
ADDED
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
data/dev.yml
ADDED
@@ -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
|