messagepack 1.0.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/README.adoc +773 -0
- data/Rakefile +8 -0
- data/docs/Gemfile +7 -0
- data/docs/README.md +85 -0
- data/docs/_config.yml +137 -0
- data/docs/_guides/index.adoc +14 -0
- data/docs/_guides/io-streaming.adoc +226 -0
- data/docs/_guides/migration.adoc +218 -0
- data/docs/_guides/performance.adoc +189 -0
- data/docs/_pages/buffer.adoc +85 -0
- data/docs/_pages/extension-types.adoc +117 -0
- data/docs/_pages/factory-pattern.adoc +115 -0
- data/docs/_pages/index.adoc +20 -0
- data/docs/_pages/serialization.adoc +159 -0
- data/docs/_pages/streaming.adoc +97 -0
- data/docs/_pages/symbol-extension.adoc +69 -0
- data/docs/_pages/timestamp-extension.adoc +88 -0
- data/docs/_references/api.adoc +360 -0
- data/docs/_references/extensions.adoc +198 -0
- data/docs/_references/format.adoc +301 -0
- data/docs/_references/index.adoc +14 -0
- data/docs/_tutorials/extension-types.adoc +170 -0
- data/docs/_tutorials/getting-started.adoc +165 -0
- data/docs/_tutorials/index.adoc +14 -0
- data/docs/_tutorials/thread-safety.adoc +157 -0
- data/docs/index.adoc +77 -0
- data/docs/lychee.toml +42 -0
- data/lib/messagepack/bigint.rb +131 -0
- data/lib/messagepack/buffer.rb +534 -0
- data/lib/messagepack/core_ext.rb +34 -0
- data/lib/messagepack/error.rb +24 -0
- data/lib/messagepack/extensions/base.rb +55 -0
- data/lib/messagepack/extensions/registry.rb +154 -0
- data/lib/messagepack/extensions/symbol.rb +38 -0
- data/lib/messagepack/extensions/timestamp.rb +110 -0
- data/lib/messagepack/extensions/value.rb +38 -0
- data/lib/messagepack/factory.rb +349 -0
- data/lib/messagepack/format.rb +99 -0
- data/lib/messagepack/packer.rb +702 -0
- data/lib/messagepack/symbol.rb +4 -0
- data/lib/messagepack/time.rb +29 -0
- data/lib/messagepack/timestamp.rb +4 -0
- data/lib/messagepack/unpacker.rb +1418 -0
- data/lib/messagepack/version.rb +5 -0
- data/lib/messagepack.rb +81 -0
- metadata +94 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Messagepack
|
|
4
|
+
# Extension registries for packer and unpacker.
|
|
5
|
+
#
|
|
6
|
+
# The packer registry maps Ruby classes to extension type IDs and packing procs.
|
|
7
|
+
# The unpacker registry maps extension type IDs to unpacking procs.
|
|
8
|
+
#
|
|
9
|
+
module ExtensionRegistry
|
|
10
|
+
# Packer registry for serializing custom types.
|
|
11
|
+
#
|
|
12
|
+
# Uses hash-based lookup with ancestor chain caching.
|
|
13
|
+
#
|
|
14
|
+
class Packer
|
|
15
|
+
def initialize
|
|
16
|
+
@registry = {} # klass => [type_id, proc, flags]
|
|
17
|
+
@cache = {} # klass => [type_id, proc, flags] (ancestor cache)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dup
|
|
21
|
+
copy = self.class.new
|
|
22
|
+
copy.instance_variable_set(:@registry, @registry.dup)
|
|
23
|
+
copy.instance_variable_set(:@cache, {})
|
|
24
|
+
copy
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias clone dup
|
|
28
|
+
|
|
29
|
+
def register(type_id, klass, proc, flags: 0)
|
|
30
|
+
@registry[klass] = [type_id, proc, flags]
|
|
31
|
+
@cache.clear
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def lookup(value)
|
|
35
|
+
klass = value.class
|
|
36
|
+
return @registry[klass] if @registry.key?(klass)
|
|
37
|
+
|
|
38
|
+
# Check cache
|
|
39
|
+
return @cache[klass] if @cache.key?(klass)
|
|
40
|
+
|
|
41
|
+
# Search ancestors and modules
|
|
42
|
+
@registry.each do |registered_class, data|
|
|
43
|
+
# Check for inheritance (klass <= registered_class)
|
|
44
|
+
if klass <= registered_class
|
|
45
|
+
@cache[klass] = data
|
|
46
|
+
return data
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if registered_class is a module (but not a class) and klass includes it
|
|
50
|
+
# In Ruby, Class is not a subclass of Module, so we need to check the class
|
|
51
|
+
if registered_class.is_a?(Class)
|
|
52
|
+
# registered_class is a Class, already handled by inheritance check above
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# registered_class is a Module
|
|
57
|
+
# Check if klass includes the module
|
|
58
|
+
if klass.include?(registered_class)
|
|
59
|
+
@cache[klass] = data
|
|
60
|
+
return data
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if the object's singleton class includes the module
|
|
64
|
+
# This handles cases like: obj.extend(Mod)
|
|
65
|
+
# Some values don't support singleton_class:
|
|
66
|
+
# - Immediate values: nil, true, false, integers, floats, symbols
|
|
67
|
+
# - Frozen values (frozen_string_literal: true makes string literals frozen)
|
|
68
|
+
unless value.nil? || value == true || value == false ||
|
|
69
|
+
value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(Symbol) ||
|
|
70
|
+
value.frozen?
|
|
71
|
+
if value.singleton_class.include?(registered_class)
|
|
72
|
+
@cache[klass] = data
|
|
73
|
+
return data
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def registered_types
|
|
82
|
+
@registry.map { |klass, (type_id, proc, flags)|
|
|
83
|
+
{ type: type_id, class: klass, packer: proc }
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def type_registered?(klass_or_type)
|
|
88
|
+
if klass_or_type.is_a?(Integer)
|
|
89
|
+
@registry.any? { |_, (type_id, _, _)| type_id == klass_or_type }
|
|
90
|
+
else
|
|
91
|
+
@registry.key?(klass_or_type)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def clear
|
|
96
|
+
@registry.clear
|
|
97
|
+
@cache.clear
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Unpacker registry for deserializing custom types.
|
|
102
|
+
#
|
|
103
|
+
# Uses direct array for O(1) type ID lookup.
|
|
104
|
+
#
|
|
105
|
+
class Unpacker
|
|
106
|
+
ARRAY_SIZE = 256 # Signed byte range: -128 to 127
|
|
107
|
+
|
|
108
|
+
def initialize
|
|
109
|
+
@array = ::Array.new(ARRAY_SIZE) # [klass, proc, flags]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def dup
|
|
113
|
+
copy = self.class.new
|
|
114
|
+
copy.instance_variable_set(:@array, @array.dup)
|
|
115
|
+
copy
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
alias clone dup
|
|
119
|
+
|
|
120
|
+
def register(type_id, klass, proc, flags: 0)
|
|
121
|
+
index = type_id + 128 # Offset for signed byte
|
|
122
|
+
raise IndexError, "type_id out of range: #{type_id}" unless (0...ARRAY_SIZE).cover?(index)
|
|
123
|
+
@array[index] = [klass, proc, flags]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def lookup(type_id)
|
|
127
|
+
index = type_id + 128
|
|
128
|
+
return nil unless (0...ARRAY_SIZE).cover?(index)
|
|
129
|
+
@array[index]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def registered_types
|
|
133
|
+
@array.each_with_index.map { |(klass, proc, flags), index|
|
|
134
|
+
next unless proc # Include entries that have a proc, even if klass is nil
|
|
135
|
+
{ type: index - 128, class: klass, unpacker: proc }
|
|
136
|
+
}.compact
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def type_registered?(klass_or_type)
|
|
140
|
+
if klass_or_type.is_a?(Integer)
|
|
141
|
+
index = klass_or_type + 128
|
|
142
|
+
# Check if any data (klass, proc, or flags) exists at this type ID index
|
|
143
|
+
(0...ARRAY_SIZE).cover?(index) && @array[index]&.at(1)
|
|
144
|
+
else
|
|
145
|
+
@array.any? { |(klass, _, _)| klass == klass_or_type }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def clear
|
|
150
|
+
@array.fill(nil)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Symbol extension for MessagePack.
|
|
4
|
+
#
|
|
5
|
+
# This adds support for serializing Symbol objects directly.
|
|
6
|
+
# When registered, symbols are serialized as their string representation.
|
|
7
|
+
#
|
|
8
|
+
class Symbol
|
|
9
|
+
# to_msgpack_ext is supposed to return a binary string.
|
|
10
|
+
# The canonical way to do it for symbols would be:
|
|
11
|
+
# [to_s].pack('A*')
|
|
12
|
+
# However in this instance we can take a shortcut
|
|
13
|
+
if method_defined?(:name)
|
|
14
|
+
alias_method :to_msgpack_ext, :name
|
|
15
|
+
else
|
|
16
|
+
alias_method :to_msgpack_ext, :to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Reconstruct symbol from binary payload.
|
|
20
|
+
#
|
|
21
|
+
# @param data [String] Binary payload (symbol name as string)
|
|
22
|
+
# @return [Symbol] The reconstructed symbol
|
|
23
|
+
#
|
|
24
|
+
def self.from_msgpack_ext(data)
|
|
25
|
+
# from_msgpack_ext is supposed to parse a binary string.
|
|
26
|
+
# The canonical way to do it for symbols would be:
|
|
27
|
+
# data.unpack1('A*').to_sym
|
|
28
|
+
# However in this instance we can take a shortcut
|
|
29
|
+
|
|
30
|
+
# We assume the string encoding is UTF-8, and let Ruby create either
|
|
31
|
+
# an ASCII symbol or UTF-8 symbol.
|
|
32
|
+
data.force_encoding(Encoding::UTF_8).to_sym
|
|
33
|
+
rescue EncodingError
|
|
34
|
+
# If somehow the string wasn't valid UTF-8 not valid ASCII, we fallback
|
|
35
|
+
# to what has been the historical behavior of creating a binary symbol
|
|
36
|
+
data.force_encoding(Encoding::BINARY).to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'value'
|
|
4
|
+
|
|
5
|
+
module Messagepack
|
|
6
|
+
# Timestamp extension type (type -1).
|
|
7
|
+
#
|
|
8
|
+
# Represents a high-resolution timestamp with second and nanosecond precision.
|
|
9
|
+
# Implements the MessagePack timestamp specification:
|
|
10
|
+
# https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
|
|
11
|
+
#
|
|
12
|
+
class Timestamp
|
|
13
|
+
TYPE = -1
|
|
14
|
+
|
|
15
|
+
TIMESTAMP32_MAX_SEC = (1 << 32) - 1
|
|
16
|
+
TIMESTAMP64_MAX_SEC = (1 << 34) - 1
|
|
17
|
+
|
|
18
|
+
attr_reader :sec, :nsec
|
|
19
|
+
|
|
20
|
+
def initialize(sec, nsec)
|
|
21
|
+
@sec = sec
|
|
22
|
+
@nsec = nsec
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Deserialize from binary payload.
|
|
26
|
+
#
|
|
27
|
+
# @param data [String] Binary payload from MessagePack
|
|
28
|
+
# @return [Timestamp] Deserialized timestamp
|
|
29
|
+
#
|
|
30
|
+
def self.from_msgpack_ext(data)
|
|
31
|
+
case data.bytesize
|
|
32
|
+
when 4
|
|
33
|
+
# timestamp32 (sec: uint32be)
|
|
34
|
+
sec = data.unpack1('L>')
|
|
35
|
+
new(sec, 0)
|
|
36
|
+
when 8
|
|
37
|
+
# timestamp64 (nsec: uint30be, sec: uint34be)
|
|
38
|
+
n, s = data.unpack('L>2')
|
|
39
|
+
sec = ((n & 0b11) << 32) | s
|
|
40
|
+
nsec = n >> 2
|
|
41
|
+
new(sec, nsec)
|
|
42
|
+
when 12
|
|
43
|
+
# timestamp96 (nsec: uint32be, sec: int64be)
|
|
44
|
+
nsec, sec = data.unpack('L>q>')
|
|
45
|
+
new(sec, nsec)
|
|
46
|
+
else
|
|
47
|
+
raise MalformedFormatError, "Invalid timestamp data size: #{data.bytesize}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Serialize to binary payload.
|
|
52
|
+
#
|
|
53
|
+
# @return [String] Binary payload for MessagePack extension
|
|
54
|
+
#
|
|
55
|
+
def to_msgpack_ext
|
|
56
|
+
self.class.to_msgpack_ext(@sec, @nsec)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Class helper for serialization.
|
|
60
|
+
#
|
|
61
|
+
# @param sec [Integer] Seconds since epoch
|
|
62
|
+
# @param nsec [Integer] Nanoseconds within second (0-999999999)
|
|
63
|
+
# @return [String] Binary payload
|
|
64
|
+
#
|
|
65
|
+
def self.to_msgpack_ext(sec, nsec)
|
|
66
|
+
if sec >= 0 && nsec >= 0 && sec <= TIMESTAMP64_MAX_SEC
|
|
67
|
+
if nsec == 0 && sec <= TIMESTAMP32_MAX_SEC
|
|
68
|
+
# timestamp32: 4 bytes
|
|
69
|
+
[sec].pack('L>')
|
|
70
|
+
else
|
|
71
|
+
# timestamp64: 8 bytes
|
|
72
|
+
nsec30 = nsec << 2
|
|
73
|
+
sec_high2 = sec >> 32
|
|
74
|
+
sec_low32 = sec & 0xffffffff
|
|
75
|
+
[nsec30 | sec_high2, sec_low32].pack('L>2')
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
# timestamp96: 12 bytes
|
|
79
|
+
[nsec, sec].pack('L>q>')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Convert to Time object
|
|
84
|
+
def to_time
|
|
85
|
+
Time.at(@sec, @nsec, :nanosecond)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create from Time object
|
|
89
|
+
def self.from_time(time)
|
|
90
|
+
new(time.tv_sec, time.tv_nsec)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ==(other)
|
|
94
|
+
other.class == self.class && @sec == other.sec && @nsec == other.nsec
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
alias eql? ==
|
|
98
|
+
|
|
99
|
+
def hash
|
|
100
|
+
[@sec, @nsec].hash
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# String representation
|
|
104
|
+
def to_s
|
|
105
|
+
"Timestamp(#{@sec}, #{@nsec})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
alias inspect to_s
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Messagepack
|
|
4
|
+
# ExtensionValue represents a raw MessagePack extension type.
|
|
5
|
+
#
|
|
6
|
+
# This is used when unpacking extension types that don't have a
|
|
7
|
+
# registered handler, or for manual extension construction.
|
|
8
|
+
#
|
|
9
|
+
class ExtensionValue
|
|
10
|
+
attr_accessor :type, :payload
|
|
11
|
+
|
|
12
|
+
def initialize(type, payload)
|
|
13
|
+
@type = type
|
|
14
|
+
@payload = payload
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ==(other)
|
|
18
|
+
return false unless other.is_a?(ExtensionValue)
|
|
19
|
+
@type == other.type && @payload == other.payload
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
[@type, @payload].hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Convert back to MessagePack format
|
|
29
|
+
def to_msgpack(packer = nil)
|
|
30
|
+
if packer.is_a?(Messagepack::Packer)
|
|
31
|
+
packer.write_extension(@type, @payload)
|
|
32
|
+
packer
|
|
33
|
+
else
|
|
34
|
+
Messagepack::Packer.new.write_extension(@type, @payload).full_pack
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'packer'
|
|
4
|
+
require_relative 'unpacker'
|
|
5
|
+
require_relative 'extensions/registry'
|
|
6
|
+
|
|
7
|
+
module Messagepack
|
|
8
|
+
# Factory for creating packer/unpacker instances with custom type registration.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# factory = Messagepack::Factory.new
|
|
12
|
+
# factory.register_type(0x01, MyClass, packer: :to_msgpack, unpacker: :from_msgpack)
|
|
13
|
+
# packer = factory.packer
|
|
14
|
+
# unpacker = factory.unpacker
|
|
15
|
+
#
|
|
16
|
+
class Factory
|
|
17
|
+
attr_reader :packer_registry, :unpacker_registry
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@packer_registry = ExtensionRegistry::Packer.new
|
|
21
|
+
@unpacker_registry = ExtensionRegistry::Unpacker.new
|
|
22
|
+
@frozen = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def freeze
|
|
26
|
+
@frozen = true
|
|
27
|
+
@packer_registry.freeze
|
|
28
|
+
@unpacker_registry.freeze
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Ensure that when the factory is duplicated (for pool), the registries are also duplicated
|
|
33
|
+
def initialize_copy(other)
|
|
34
|
+
super
|
|
35
|
+
@packer_registry = @packer_registry.dup
|
|
36
|
+
@unpacker_registry = @unpacker_registry.dup
|
|
37
|
+
@frozen = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Register a custom type for packing and unpacking.
|
|
41
|
+
#
|
|
42
|
+
# @param type_id [Integer] Extension type ID (-128 to 127)
|
|
43
|
+
# @param klass [Class] Ruby class to register
|
|
44
|
+
# @param options [Hash] Registration options
|
|
45
|
+
# @option options [Proc, Method, Symbol, String] :packer Serialization proc/method
|
|
46
|
+
# @option options [Proc, Method, Symbol, String] :unpacker Deserialization proc/method
|
|
47
|
+
# @option options [Boolean] :recursive Whether packer/unpacker is passed to proc
|
|
48
|
+
#
|
|
49
|
+
def register_type(type_id, klass, options = {})
|
|
50
|
+
raise FrozenError, "can't modify frozen Messagepack::Factory" if frozen?
|
|
51
|
+
|
|
52
|
+
packer = normalize_packer(options[:packer], klass)
|
|
53
|
+
unpacker = normalize_unpacker(options[:unpacker], klass)
|
|
54
|
+
|
|
55
|
+
register_type_internal(type_id, klass, packer, unpacker, options)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get list of registered types.
|
|
59
|
+
#
|
|
60
|
+
# @param selector [Symbol] :packer, :unpacker, or :both
|
|
61
|
+
# @return [Array<Hash>] List of registered types
|
|
62
|
+
#
|
|
63
|
+
def registered_types(selector = :both)
|
|
64
|
+
case selector
|
|
65
|
+
when :packer
|
|
66
|
+
@packer_registry.registered_types
|
|
67
|
+
when :unpacker
|
|
68
|
+
@unpacker_registry.registered_types
|
|
69
|
+
when :both
|
|
70
|
+
packer_types = @packer_registry.registered_types
|
|
71
|
+
unpacker_types = @unpacker_registry.registered_types
|
|
72
|
+
|
|
73
|
+
# Merge by type_id
|
|
74
|
+
all_types = {}
|
|
75
|
+
packer_types.each { |t| all_types[t[:type]] = t.merge(unpacker: nil) }
|
|
76
|
+
unpacker_types.each do |t|
|
|
77
|
+
if all_types.key?(t[:type])
|
|
78
|
+
all_types[t[:type]][:unpacker] = t[:unpacker]
|
|
79
|
+
else
|
|
80
|
+
all_types[t[:type]] = t.merge(packer: nil)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
all_types.values
|
|
85
|
+
else
|
|
86
|
+
raise ArgumentError, "Invalid selector: #{selector}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if a type is registered.
|
|
91
|
+
#
|
|
92
|
+
# @param klass_or_type [Class, Integer] Class or type ID to check
|
|
93
|
+
# @param selector [Symbol] :packer, :unpacker, or :both
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
#
|
|
96
|
+
def type_registered?(klass_or_type, selector = :both)
|
|
97
|
+
case selector
|
|
98
|
+
when :packer
|
|
99
|
+
@packer_registry.type_registered?(klass_or_type)
|
|
100
|
+
when :unpacker
|
|
101
|
+
@unpacker_registry.type_registered?(klass_or_type)
|
|
102
|
+
when :both
|
|
103
|
+
@packer_registry.type_registered?(klass_or_type) ||
|
|
104
|
+
@unpacker_registry.type_registered?(klass_or_type)
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError, "Invalid selector: #{selector}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Create a new packer instance.
|
|
111
|
+
#
|
|
112
|
+
# @param io [IO] Optional IO object for streaming output
|
|
113
|
+
# @param options [Hash] Options to pass to Packer
|
|
114
|
+
# @return [Packer] New packer with registered types
|
|
115
|
+
#
|
|
116
|
+
def packer(io = nil, options = nil)
|
|
117
|
+
Packer.new(io, options).tap do |pk|
|
|
118
|
+
pk.extension_registry = @packer_registry.dup
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Create a new unpacker instance.
|
|
123
|
+
#
|
|
124
|
+
# @param io [IO] Optional IO object for streaming input
|
|
125
|
+
# @param options [Hash] Options to pass to Unpacker
|
|
126
|
+
# @return [Unpacker] New unpacker with registered types
|
|
127
|
+
#
|
|
128
|
+
def unpacker(io = nil, **options)
|
|
129
|
+
Unpacker.new(io, **options).tap do |uk|
|
|
130
|
+
uk.extension_registry = @unpacker_registry.dup
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Serialize an object to MessagePack binary.
|
|
135
|
+
#
|
|
136
|
+
# @param object Object to serialize
|
|
137
|
+
# @param io [IO] Optional IO to write to
|
|
138
|
+
# @param options [Hash] Options to pass to Packer
|
|
139
|
+
# @return [String, nil] Binary string if io is nil
|
|
140
|
+
#
|
|
141
|
+
def dump(object, *args)
|
|
142
|
+
io = args.first if args.first.respond_to?(:write)
|
|
143
|
+
options = args.last if args.last.is_a?(Hash)
|
|
144
|
+
packer(io, **(options || {})).tap { |pk| pk.write(object); pk.flush }.full_pack
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Deserialize MessagePack binary to Ruby object.
|
|
148
|
+
#
|
|
149
|
+
# @param data [String, IO] Binary data or IO to read from
|
|
150
|
+
# @param options [Hash] Options to pass to Unpacker
|
|
151
|
+
# @return [Object] Deserialized object
|
|
152
|
+
#
|
|
153
|
+
def load(data, options = nil)
|
|
154
|
+
# Check if data is an IO-like object
|
|
155
|
+
if data.respond_to?(:read)
|
|
156
|
+
unpacker(data, **(options || {})).full_unpack
|
|
157
|
+
else
|
|
158
|
+
unpacker(nil, **(options || {})).tap { |uk| uk.feed(data) }.full_unpack
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
alias :pack :dump
|
|
163
|
+
alias :unpack :load
|
|
164
|
+
|
|
165
|
+
# Create a pool of packers/unpackers for thread-safe reuse.
|
|
166
|
+
#
|
|
167
|
+
# @param size [Integer] Number of packers/unpackers in the pool
|
|
168
|
+
# @param options [Hash] Options for packer/unpacker creation
|
|
169
|
+
# @return [Pool] New pool instance
|
|
170
|
+
#
|
|
171
|
+
def pool(size = 1, **options)
|
|
172
|
+
Pool.new(frozen? ? self : dup.freeze, size, options)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def normalize_packer(packer, klass)
|
|
178
|
+
case packer
|
|
179
|
+
when nil
|
|
180
|
+
packer
|
|
181
|
+
when Proc
|
|
182
|
+
packer
|
|
183
|
+
when String, Symbol
|
|
184
|
+
# Create a proc that calls the method on the object being packed
|
|
185
|
+
->(obj) { obj.send(packer) }
|
|
186
|
+
when Method
|
|
187
|
+
packer.to_proc
|
|
188
|
+
else
|
|
189
|
+
if packer.respond_to?(:call)
|
|
190
|
+
packer.method(:call).to_proc
|
|
191
|
+
else
|
|
192
|
+
raise ::TypeError, "invalid packer: #{packer.inspect}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def normalize_unpacker(unpacker, klass)
|
|
198
|
+
case unpacker
|
|
199
|
+
when nil, Proc
|
|
200
|
+
unpacker
|
|
201
|
+
when String, Symbol
|
|
202
|
+
klass.method(unpacker).to_proc
|
|
203
|
+
when Method
|
|
204
|
+
unpacker.to_proc
|
|
205
|
+
else
|
|
206
|
+
if unpacker.respond_to?(:call)
|
|
207
|
+
unpacker.method(:call).to_proc
|
|
208
|
+
else
|
|
209
|
+
raise ::TypeError, "invalid unpacker: #{unpacker.inspect}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def register_type_internal(type_id, klass, packer, unpacker, options)
|
|
215
|
+
# Validate oversized_integer_extension option
|
|
216
|
+
if options[:oversized_integer_extension]
|
|
217
|
+
unless klass == Integer
|
|
218
|
+
raise ArgumentError, "oversized_integer_extension can only be used with Integer class"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Only use default methods when packer/unpacker are not specified at all
|
|
223
|
+
# (i.e., the keys don't exist in options)
|
|
224
|
+
has_packer = options.key?(:packer)
|
|
225
|
+
has_unpacker = options.key?(:unpacker)
|
|
226
|
+
|
|
227
|
+
if !has_packer && !has_unpacker && packer.nil?
|
|
228
|
+
packer = :to_msgpack_ext if klass.method_defined?(:to_msgpack_ext)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if !has_packer && !has_unpacker && unpacker.nil?
|
|
232
|
+
unpacker = :from_msgpack_ext if klass.respond_to?(:from_msgpack_ext)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Normalize packer/unpacker (convert symbols to procs)
|
|
236
|
+
if packer.is_a?(Symbol)
|
|
237
|
+
method_name = packer
|
|
238
|
+
packer = ->(obj) { obj.send(method_name) }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if unpacker.is_a?(Symbol)
|
|
242
|
+
unpacker = klass.method(unpacker).to_proc
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
flags = 0
|
|
246
|
+
flags |= 0x01 if options[:recursive]
|
|
247
|
+
flags |= 0x02 if options[:oversized_integer_extension]
|
|
248
|
+
|
|
249
|
+
@packer_registry.register(type_id, klass, packer, flags: flags) if packer
|
|
250
|
+
@unpacker_registry.register(type_id, klass, unpacker, flags: flags) if unpacker
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Pool for thread-safe packer/unpacker reuse.
|
|
254
|
+
#
|
|
255
|
+
class Pool
|
|
256
|
+
def initialize(factory, size, options)
|
|
257
|
+
@factory = factory
|
|
258
|
+
@size = size
|
|
259
|
+
@options = options.empty? ? nil : options
|
|
260
|
+
@packers = []
|
|
261
|
+
@unpackers = []
|
|
262
|
+
@mutex = Mutex.new
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Deserialize data.
|
|
266
|
+
#
|
|
267
|
+
# @param data [String] Binary data
|
|
268
|
+
# @return [Object] Deserialized object
|
|
269
|
+
#
|
|
270
|
+
def load(data)
|
|
271
|
+
with_unpacker { |uk| uk.feed(data); uk.full_unpack }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Serialize object.
|
|
275
|
+
#
|
|
276
|
+
# @param object Object to serialize
|
|
277
|
+
# @return [String] Binary data
|
|
278
|
+
#
|
|
279
|
+
def dump(object)
|
|
280
|
+
with_packer { |pk| pk.write(object); pk.full_pack }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
alias :pack :dump
|
|
284
|
+
alias :unpack :load
|
|
285
|
+
|
|
286
|
+
# Execute block with a packer from the pool.
|
|
287
|
+
#
|
|
288
|
+
def with_packer
|
|
289
|
+
packer = nil
|
|
290
|
+
@mutex.synchronize do
|
|
291
|
+
packer = @packers.pop || @factory.packer(**(@options || {}))
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
yield packer
|
|
295
|
+
ensure
|
|
296
|
+
@mutex.synchronize { @packers << packer.reset } if packer
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Execute block with an unpacker from the pool.
|
|
300
|
+
#
|
|
301
|
+
def with_unpacker
|
|
302
|
+
unpacker = nil
|
|
303
|
+
@mutex.synchronize do
|
|
304
|
+
unpacker = @unpackers.pop || @factory.unpacker(**(@options || {}))
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
yield unpacker
|
|
308
|
+
ensure
|
|
309
|
+
@mutex.synchronize { @unpackers << unpacker.reset } if unpacker
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Get a packer from the pool and yield it to the block.
|
|
313
|
+
#
|
|
314
|
+
# @return [Object] Result of the block
|
|
315
|
+
#
|
|
316
|
+
def packer
|
|
317
|
+
packer = nil
|
|
318
|
+
@mutex.synchronize do
|
|
319
|
+
packer = @packers.pop || @factory.packer(**(@options || {}))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Set frozen flag to prevent type registration
|
|
323
|
+
packer.freeze_for_pool
|
|
324
|
+
|
|
325
|
+
yield packer
|
|
326
|
+
ensure
|
|
327
|
+
@mutex.synchronize { @packers << packer.reset } if packer
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Get an unpacker from the pool and yield it to the block.
|
|
331
|
+
#
|
|
332
|
+
# @return [Object] Result of the block
|
|
333
|
+
#
|
|
334
|
+
def unpacker
|
|
335
|
+
unpacker = nil
|
|
336
|
+
@mutex.synchronize do
|
|
337
|
+
unpacker = @unpackers.pop || @factory.unpacker(**(@options || {}))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Set frozen flag to prevent type registration
|
|
341
|
+
unpacker.freeze_for_pool
|
|
342
|
+
|
|
343
|
+
yield unpacker
|
|
344
|
+
ensure
|
|
345
|
+
@mutex.synchronize { @unpackers << unpacker.reset } if unpacker
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|