Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for raw bytes, UUID and int128 forms #11

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/ulid/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ def encode32
output
end

# returns the binary uint128 in base16 UUID format
def encode16
self.bytes.unpack("H8H4H4H4H*").join("-")
end

def encode10
(hi, lo) = self.bytes.unpack('Q>Q>')
(hi << 64) | lo
end

def random_bytes
SecureRandom.random_bytes(10)
end
Expand Down
57 changes: 47 additions & 10 deletions lib/ulid/identifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Identifier
include Generate
include Compare

attr_reader :seed, :bytes, :time, :ulid
attr_reader :seed, :bytes, :time

# Create a new instance of a ULID::Identifier.
#
Expand All @@ -35,15 +35,38 @@ def initialize(start = nil, seed = nil)
@seed = seed
end
when String
if start.size != 26
raise ArgumentError.new("invalid ULID string, must be 26 characters")
case start.size
when 16
# assume byte form of ULID (Should be Encoding::ASCII_8BIT)
@bytes = start.b
when 26
# parse ULID string into bytes
@ulid = start.upcase
@bytes = decode(@ulid)
when 36
# parse UUID string into bytes
@bytes = decode16(start)
else
raise ArgumentError.new("invalid ULID or UUID string - must be 16, 26, or 36 characters")
end

# parse string into bytes
@ulid = start.upcase
@bytes = decode(@ulid)
raise ArgumentError.new("invalid ULID or UUID") if @bytes.size != 16

@time, @seed = unpack_ulid_bytes(@bytes)
when Integer
# parse integer (BigNum) into bytes
@bytes = decode10(start)

raise ArgumentError.new("invalid ULID or UUID") if @bytes.size != 16

@time, @seed = unpack_ulid_bytes(@bytes)
when Array
# parse Array(16) into bytes
@bytes = start.pack("C*")

@time, @seed = unpack_decoded_bytes(@bytes)
raise ArgumentError.new("invalid Byte Array") if @bytes.size != 16

@time, @seed = unpack_ulid_bytes(@bytes)
else
# unrecognized initial values type given, just generate fresh ULID
@time = Time.now.utc
Expand All @@ -54,11 +77,25 @@ def initialize(start = nil, seed = nil)
# an ASCII_8BIT encoded string, should be 16 bytes
@bytes = time_bytes + @seed
end
end

if @ulid.nil?
def ulid
# the lexically sortable Base32 string we actually care about
@ulid = encode32
end
@ulid ||= encode32
end

def to_uuid
@uuid ||= encode16
end

def to_i
@int128 ||= encode10
end

alias_method :to_s, :ulid
alias_method :to_str, :ulid
alias_method :inspect, :ulid
alias_method :b, :bytes

end
end
10 changes: 9 additions & 1 deletion lib/ulid/parse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ def decode(v)
out
end

def unpack_decoded_bytes(packed_bytes)
def decode16(input)
[input.delete("-")].pack("H*")
end

def decode10(input)
[input >> 64, input & 0xFFFFFFFFFFFFFFFF].pack("Q>Q>")
end

def unpack_ulid_bytes(packed_bytes)
time_bytes = packed_bytes[0..5].bytes.map(&:to_i)
seed = packed_bytes[6..-1]

Expand Down
94 changes: 92 additions & 2 deletions spec/ulid_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
# --------------------------
# 0DHWZ1DT60
# 60ZVCSJFXBTG0J2N
KNOWN_STRING = '01ARYZ6RR0T8CNRGXPSBZSA1PY'
KNOWN_TIME = Time.parse('2016-07-30 22:36:16 UTC')
KNOWN_STRING = "01ARYZ6RR0T8CNRGXPSBZSA1PY"
KNOWN_TIME = Time.parse("2016-07-30 22:36:16 UTC")
KNOWN_UUID = "01563df3-6300-d219-5c43-b6caff9506de"
KNOWN_BYTES = "\x01V=\xF3c\x00\xD2\x19\\C\xB6\xCA\xFF\x95\x06\xDE".b
KNOWN_UINT128 = 1777022035688232904178850488005232350

describe ULID do
it "has a version number" do
Expand Down Expand Up @@ -76,6 +79,21 @@
end
end

describe '.to_uuid' do
it 'generates a valid UUID' do
ulid = ULID.new KNOWN_STRING
expect(ulid.to_uuid).to eq(KNOWN_UUID)
end
end

describe '.bytes' do
it 'from a valid UUID' do
ulid = ULID.new KNOWN_UUID
expect(ulid.bytes).to eq(KNOWN_BYTES)
end
end


describe ULID::Identifier do
context 'with no initialization value' do
it 'generates a random value for the current time' do
Expand Down Expand Up @@ -133,6 +151,78 @@
expect(other.bytes).to eq(first.bytes)
expect(other.time).to eq(first.time)
end

it 'raises an error for invalid ULID string' do
expect { ULID.new("not a valid ULID!") }.to raise_error(ArgumentError)
expect { ULID.new("") }.to raise_error(ArgumentError)
expect { ULID.new(KNOWN_STRING + KNOWN_STRING) }.to raise_error(ArgumentError)
end
end

context 'with Bytes string arg' do
it 'supports byteform' do
first = ULID.new
other = ULID.new first.bytes

expect(other.ulid).to eq(first.ulid)
expect(other.seed).to eq(first.seed)
expect(other.bytes).to eq(first.bytes)
expect(other.time).to be_within(0.001).of(first.time)
end
end

context 'with UUID string arg' do
it 'supports UUID' do
first = ULID.new KNOWN_UUID
other = ULID.new KNOWN_STRING

expect(other.ulid).to eq(first.ulid)
expect(other.seed).to eq(first.seed)
expect(other.bytes).to eq(first.bytes)
expect(other.time).to eq(first.time)
end

it 'supports UUID case insensitive' do
first = ULID.new KNOWN_UUID.downcase
other = ULID.new KNOWN_UUID.upcase

expect(other.ulid).to eq(first.ulid)
expect(other.seed).to eq(first.seed)
expect(other.bytes).to eq(first.bytes)
expect(other.time).to eq(first.time)
end

it 'supports UUID self generated' do
first = ULID.new
other = ULID.new first.to_uuid

expect(other.ulid).to eq(first.ulid)
expect(other.seed).to eq(first.seed)
expect(other.bytes).to eq(first.bytes)
expect(other.time).to be_within(0.001).of(first.time)
end
end

context 'with uInt128 arg' do
it 'supports BigNum' do
first = ULID.new
other = ULID.new first.to_i

expect(other.ulid).to eq(first.ulid)
expect(other.seed).to eq(first.seed)
expect(other.bytes).to eq(first.bytes)
expect(other.time).to be_within(0.001).of(first.time)
end

it 'supports BigNum self generated' do
first = ULID.new KNOWN_UINT128
other = ULID.new KNOWN_STRING

expect(other.ulid).to eq(first.ulid)
expect(other.seed).to eq(first.seed)
expect(other.bytes).to eq(first.bytes)
expect(other.time).to eq(first.time)
end
end

describe 'compared to other ULIDs' do
Expand Down