Navigation Menu

Skip to content

Commit

Permalink
Merge pull request #168 from gfx/timestamp_type
Browse files Browse the repository at this point in the history
MessagePack timestamp type
  • Loading branch information
tagomoris committed Jun 20, 2019
2 parents 7bfc227 + 7566db5 commit ceab59e
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Expand Up @@ -5,6 +5,9 @@
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

AllCops:
TargetRubyVersion: 2.3

# Offense count: 3
Lint/AmbiguousOperator:
Enabled: false
Expand Down
16 changes: 16 additions & 0 deletions README.rdoc
Expand Up @@ -121,6 +121,22 @@ being serialized altogether by throwing an exception:

[1, :symbol, 'string'].to_msgpack # => RuntimeError: Serialization of symbols prohibited

= Serializing and deserializing Time instances

There are the timestamp extension type in MessagePack,
but it is not registered by default.

To map Ruby's Time to MessagePack's timestamp for the default factory:

MessagePack::DefaultFactory.register_type(
MessagePack::Timestamp::TYPE, # or just -1
Time,
packer: MessagePack::Time::Packer,
unpacker: MessagePack::Time::Unpacker
)

See {API reference}[http://ruby.msgpack.org/] for details.

= Extension Types

Packer and Unpacker support {Extension types of MessagePack}[https://github.com/msgpack/msgpack/blob/master/spec.md#types-extension-type].
Expand Down
22 changes: 22 additions & 0 deletions doclib/msgpack/time.rb
@@ -0,0 +1,22 @@
module MessagePack

# MessagePack::Time provides packer and unpacker functions for a timestamp type.
# @example Setup for DefaultFactory
# MessagePack::DefaultFactory.register_type(
# MessagePack::Timestamp::TYPE,
# Time,
# packer: MessagePack::Time::Packer,
# unpacker: MessagePack::Time::Unpacker
# )
class Time
# A packer function that packs a Time instance to a MessagePack timestamp.
Packer = lambda { |payload|
# ...
}

# An unpacker function that unpacks a MessagePack timestamp to a Time instance.
Unpcker = lambda { |time|
# ...
}
end
end
44 changes: 44 additions & 0 deletions doclib/msgpack/timestamp.rb
@@ -0,0 +1,44 @@
module MessagePack
# A utility class for MessagePack timestamp type
class Timestamp
#
# The timestamp extension type defined in the MessagePack spec.
#
# See https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type for details.
#
TYPE = -1

# @return [Integer] Second part of the timestamp.
attr_reader :sec

# @return [Integer] Nanosecond part of the timestamp.
attr_reader :nsec

# @param [Integer] sec
# @param [Integer] nsec
def initialize(sec, nsec)
end

# @example An unpacker implementation for the Time class
# lambda do |payload|
# tv = MessagePack::Timestamp.from_msgpack_ext(payload)
# Time.at(tv.sec, tv.nsec, :nanosecond)
# end
#
# @param [String] data
# @return [MessagePack::Timestamp]
def self.from_msgpack_ext(data)
end

# @example A packer implementation for the Time class
# unpacker = lambda do |time|
# MessagePack::Timestamp.to_msgpack_ext(time.tv_sec, time.tv_nsec)
# end
#
# @param [Integer] sec
# @param [Integer] nsec
# @return [String]
def self.to_msgpack_ext(sec, nsec)
end
end
end
2 changes: 2 additions & 0 deletions lib/msgpack.rb
Expand Up @@ -17,6 +17,8 @@
require "msgpack/factory"
require "msgpack/symbol"
require "msgpack/core_ext"
require "msgpack/timestamp"
require "msgpack/time"

module MessagePack
DefaultFactory = MessagePack::Factory.new
Expand Down
29 changes: 29 additions & 0 deletions lib/msgpack/time.rb
@@ -0,0 +1,29 @@
# frozen_string_literal: true

# MessagePack extention packer and unpacker for built-in Time class
module MessagePack
module Time
# 3-arg Time.at is available Ruby >= 2.5
TIME_AT_3_AVAILABLE = begin
!!::Time.at(0, 0, :nanosecond)
rescue ArgumentError
false
end

Unpacker = if TIME_AT_3_AVAILABLE
lambda do |payload|
tv = MessagePack::Timestamp.from_msgpack_ext(payload)
::Time.at(tv.sec, tv.nsec, :nanosecond)
end
else
lambda do |payload|
tv = MessagePack::Timestamp.from_msgpack_ext(payload)
::Time.at(tv.sec, tv.nsec / 1000.0)
end
end

Packer = lambda { |time|
MessagePack::Timestamp.to_msgpack_ext(time.tv_sec, time.tv_nsec)
}
end
end
76 changes: 76 additions & 0 deletions lib/msgpack/timestamp.rb
@@ -0,0 +1,76 @@
# frozen_string_literal: true

module MessagePack
class Timestamp # a.k.a. "TimeSpec"
# Because the byte-order of MessagePack is big-endian in,
# pack() and unpack() specifies ">".
# See https://docs.ruby-lang.org/en/trunk/Array.html#method-i-pack for details.

# The timestamp extension type defined in the MessagePack spec.
# See https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type for details.
TYPE = -1

TIMESTAMP32_MAX_SEC = (1 << 32) - 1
TIMESTAMP64_MAX_SEC = (1 << 34) - 1

# @return [Integer]
attr_reader :sec

# @return [Integer]
attr_reader :nsec

# @param [Integer] sec
# @param [Integer] nsec
def initialize(sec, nsec)
@sec = sec
@nsec = nsec
end

def self.from_msgpack_ext(data)
case data.length
when 4
# timestamp32 (sec: uint32be)
sec, = data.unpack('L>')
new(sec, 0)
when 8
# timestamp64 (nsec: uint30be, sec: uint34be)
n, s = data.unpack('L>2')
sec = ((n & 0b11) << 32) | s
nsec = n >> 2
new(sec, nsec)
when 12
# timestam96 (nsec: uint32be, sec: int64be)
nsec, sec = data.unpack('L>q>')
new(sec, nsec)
else
raise MalformedFormatError, "Invalid timestamp data size: #{data.length}"
end
end

def self.to_msgpack_ext(sec, nsec)
if sec >= 0 && nsec >= 0 && sec <= TIMESTAMP64_MAX_SEC
if nsec === 0 && sec <= TIMESTAMP32_MAX_SEC
# timestamp32 = (sec: uint32be)
[sec].pack('L>')
else
# timestamp64 (nsec: uint30be, sec: uint34be)
nsec30 = nsec << 2
sec_high2 = sec << 32 # high 2 bits (`x & 0b11` is redandunt)
sec_low32 = sec & 0xffffffff # low 32 bits
[nsec30 | sec_high2, sec_low32].pack('L>2')
end
else
# timestamp96 (nsec: uint32be, sec: int64be)
[nsec, sec].pack('L>q>')
end
end

def to_msgpack_ext
self.class.to_msgpack_ext(sec, nsec)
end

def ==(other)
other.class == self.class && sec == other.sec && nsec == other.nsec
end
end
end
90 changes: 90 additions & 0 deletions spec/timestamp_spec.rb
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require 'spec_helper'

describe MessagePack::Timestamp do
describe 'malformed format' do
it do
expect do
MessagePack::Timestamp.from_msgpack_ext([0xd4, 0x00].pack("C*"))
end.to raise_error(MessagePack::MalformedFormatError)
end
end

describe 'register_type with Time' do
let(:factory) do
factory = MessagePack::Factory.new
factory.register_type(
MessagePack::Timestamp::TYPE,
Time,
packer: MessagePack::Time::Packer,
unpacker: MessagePack::Time::Unpacker
)
factory
end

let(:time) { Time.local(2019, 6, 17, 1, 2, 3, 123_456) }
it 'serializes and deserializes Time' do
packed = factory.pack(time)
unpacked = factory.unpack(packed)
expect(unpacked).to eq(time)
end
end

describe 'register_type with MessagePack::Timestamp' do
let(:factory) do
factory = MessagePack::Factory.new
factory.register_type(MessagePack::Timestamp::TYPE, MessagePack::Timestamp)
factory
end

let(:timestamp) { MessagePack::Timestamp.new(Time.now.tv_sec, 123_456_789) }
it 'serializes and deserializes MessagePack::Timestamp' do
packed = factory.pack(timestamp)
unpacked = factory.unpack(packed)
expect(unpacked).to eq(timestamp)
end
end

describe 'timestamp32' do
it 'handles [1, 0]' do
t = MessagePack::Timestamp.new(1, 0)

payload = t.to_msgpack_ext
unpacked = MessagePack::Timestamp.from_msgpack_ext(payload)

expect(unpacked).to eq(t)
end
end

describe 'timestamp64' do
it 'handles [1, 1]' do
t = MessagePack::Timestamp.new(1, 1)

payload = t.to_msgpack_ext
unpacked = MessagePack::Timestamp.from_msgpack_ext(payload)

expect(unpacked).to eq(t)
end
end

describe 'timestamp96' do
it 'handles [-1, 0]' do
t = MessagePack::Timestamp.new(-1, 0)

payload = t.to_msgpack_ext
unpacked = MessagePack::Timestamp.from_msgpack_ext(payload)

expect(unpacked).to eq(t)
end

it 'handles [-1, 999_999_999]' do
t = MessagePack::Timestamp.new(-1, 999_999_999)

payload = t.to_msgpack_ext
unpacked = MessagePack::Timestamp.from_msgpack_ext(payload)

expect(unpacked).to eq(t)
end
end
end

0 comments on commit ceab59e

Please sign in to comment.