-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 UUID
.
#2716
Closed
Closed
Add UUID
.
#2716
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
require "spec" | ||
|
||
describe "UUID" do | ||
it "has working zero UUID" do | ||
UUID.empty.should eq UUID.empty | ||
UUID.empty.to_s.should eq "00000000-0000-0000-0000-000000000000" | ||
UUID.empty.variant.should eq UUID::Variant::NCS | ||
end | ||
|
||
it "doesn't overwrite empty" do | ||
empty = UUID.empty | ||
empty.should eq empty | ||
empty.decode "a01a5a94-7b52-4ca8-b310-382436650336" | ||
UUID.empty.should_not eq empty | ||
end | ||
|
||
it "can be built from strings" do | ||
UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("c20335c37f464126aae9f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("C20335C37F464126AAE9F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
end | ||
|
||
it "should have correct variant and version" do | ||
UUID.new("C20335C37F464126AAE9F665434AD12B").variant.should eq UUID::Variant::RFC4122 | ||
UUID.new("C20335C37F464126AAE9F665434AD12B").version.should eq UUID::Version::V4 | ||
end | ||
|
||
it "supports different string formats" do | ||
UUID.new("ee843b2656d8472bb3430b94ed9077ff").to_s.should eq "ee843b26-56d8-472b-b343-0b94ed9077ff" | ||
UUID.new("3e806983-eca4-4fc5-b581-f30fb03ec9e5").to_s(UUID::Format::Hexstring).should eq "3e806983eca44fc5b581f30fb03ec9e5" | ||
UUID.new("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").to_s(UUID::Format::URN).should eq "urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892" | ||
end | ||
|
||
it "compares to strings" do | ||
uuid = UUID.new "c3b46146eb794e18877b4d46a10d1517" | ||
->{ uuid == "c3b46146eb794e18877b4d46a10d1517" }.call.should eq(true) | ||
->{ uuid == "c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) | ||
->{ uuid == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) | ||
->{ uuid == "urn:uuid:C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) | ||
->{ uuid == "urn:uuid:c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) | ||
->{ UUID.new == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(false) | ||
end | ||
|
||
it "fails on invalid arguments when creating" do | ||
expect_raises(ArgumentError) { UUID.new "" } | ||
expect_raises(ArgumentError) { UUID.new "25d6f843?cf8e-44fb-9f84-6062419c4330" } | ||
expect_raises(ArgumentError) { UUID.new "67dc9e24-0865 474b-9fe7-61445bfea3b5" } | ||
expect_raises(ArgumentError) { UUID.new "5942cde5-10d1-416b+85c4-9fc473fa1037" } | ||
expect_raises(ArgumentError) { UUID.new "0f02a229-4898-4029-926f=94be5628a7fd" } | ||
expect_raises(ArgumentError) { UUID.new "cda08c86-6413-474f-8822-a6646e0fb19G" } | ||
expect_raises(ArgumentError) { UUID.new "2b1bfW06368947e59ac07c3ffdaf514c" } | ||
end | ||
|
||
it "fails when comparing to invalid strings" do | ||
expect_raises(ArgumentError) { UUID.new == "" } | ||
expect_raises(ArgumentError) { UUID.new == "d1fb9189-7013-4915-a8b1-07cfc83bca3U" } | ||
expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e0 6c" } | ||
expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e 06c" } | ||
expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e-76c" } | ||
end | ||
|
||
it "should handle variant" do | ||
uuid = UUID.new | ||
expect_raises(ArgumentError) { uuid.variant = UUID::Variant::Unknown } | ||
{% for variant in %w(NCS RFC4122 Microsoft Future) %} | ||
uuid.variant = UUID::Variant::{{ variant.id }} | ||
uuid.variant.should eq UUID::Variant::{{ variant.id }} | ||
{% end %} | ||
end | ||
|
||
it "should handle version" do | ||
uuid = UUID.new | ||
expect_raises(ArgumentError) { uuid.version = UUID::Version::Unknown } | ||
{% for version in %w(1 2 3 4 5) %} | ||
uuid.version = UUID::Version::V{{ version.id }} | ||
uuid.version.should eq UUID::Version::V{{ version.id }} | ||
{% end %} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,4 +72,5 @@ require "thread" | |
require "time" | ||
require "tuple" | ||
require "union" | ||
require "uuid" | ||
require "value" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
require "secure_random" | ||
require "./uuid/*" | ||
|
||
# Universally Unique IDentifier. | ||
# | ||
# Supports custom variants with arbitrary 16 bytes as well as (RFC 4122)[https://www.ietf.org/rfc/rfc4122.txt] variant | ||
# versions. | ||
struct UUID | ||
# Internal representation. | ||
@data = StaticArray(UInt8, 16).new | ||
|
||
# Generates RFC 4122 v4 UUID. | ||
def initialize | ||
initialize Version::V4 | ||
end | ||
|
||
# Generates UUID from static 16-`bytes`. | ||
def initialize(bytes : StaticArray(UInt8, 16)) | ||
@data = bytes | ||
end | ||
|
||
# Creates UUID from 16-`bytes` slice. | ||
def initialize(bytes : Slice(UInt8)) | ||
raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 | ||
@data.to_unsafe.copy_from bytes | ||
end | ||
|
||
# Creates UUID from string `value`. See `UUID#decode(value : String)` for details on supported string formats. | ||
def initialize(value : String) | ||
decode value | ||
end | ||
|
||
# Returns 16-byte slice. | ||
def to_slice | ||
Slice(UInt8).new to_unsafe, 16 | ||
end | ||
|
||
# Returns unsafe pointer to 16-bytes. | ||
def to_unsafe | ||
@data.to_unsafe | ||
end | ||
|
||
# Writes hyphenated format string to `io`. | ||
def to_s(io : IO) | ||
io << to_s | ||
end | ||
|
||
# Returns `true` if `other` string represents the same UUID, `false` otherwise. | ||
def ==(other : String) | ||
self == UUID.new other | ||
end | ||
|
||
# Returns `true` if `other` 16-byte slice represents the same UUID, `false` otherwise. | ||
def ==(other : Slice(UInt8)) | ||
to_slice == other | ||
end | ||
|
||
# Returns `true` if `other` static 16 bytes represent the same UUID, `false` otherwise. | ||
def ==(other : StaticArray(UInt8, 16)) | ||
self.==(Slice(UInt8).new other.to_unsafe, 16) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
struct UUID | ||
# Empty UUID. | ||
@@empty_bytes = StaticArray(UInt8, 16).new { 0_u8 } | ||
|
||
# Returns empty UUID (aka nil UUID where all bytes are set to `0`). | ||
def self.empty | ||
UUID.new @@empty_bytes | ||
end | ||
|
||
# Resets UUID to an empty one. | ||
def empty! | ||
@bytes = @@empty_bytes | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Support for RFC 4122 UUID variant. | ||
struct UUID | ||
# RFC 4122 UUID variant versions. | ||
enum Version | ||
# Unknown version. | ||
Unknown = 0 | ||
|
||
# Version 1 - date-time and MAC address. | ||
V1 = 1 | ||
|
||
# Version 2 - DCE security. | ||
V2 = 2 | ||
|
||
# Version 3 - MD5 hash and namespace. | ||
V3 = 3 | ||
|
||
# Version 4 - random. | ||
V4 = 4 | ||
|
||
# Version 5 - SHA1 hash and namespace. | ||
V5 = 5 | ||
end | ||
|
||
# Generates RFC 4122 UUID `variant` with specified `version`. | ||
def initialize(version : Version) | ||
case version | ||
when Version::V4 | ||
@data = SecureRandom.random_bytes(16) | ||
variant = Variant::RFC4122 | ||
version = Version::V4 | ||
else | ||
raise ArgumentError.new "Creating #{version} not supported." | ||
end | ||
end | ||
|
||
# Returns version based on provided 6th `byte` (0-indexed). | ||
def self.byte_version(byte : UInt8) | ||
case byte >> 4 | ||
when 1 then Version::V1 | ||
when 2 then Version::V2 | ||
when 3 then Version::V3 | ||
when 4 then Version::V4 | ||
when 5 then Version::V5 | ||
else Version::Unknown | ||
end | ||
end | ||
|
||
# Returns byte with encoded `version` for provided 6th `byte` (0-indexed) for known versions. | ||
# For `Version::Unknown` `version` raises `ArgumentError`. | ||
def self.byte_version(byte : UInt8, version : Version) : UInt8 | ||
if version != Version::Unknown | ||
(byte & 0xf) | (version.to_u8 << 4) | ||
else | ||
raise ArgumentError.new "Can't set unknown version." | ||
end | ||
end | ||
|
||
# Returns version based on RFC 4122 format. See also `UUID#variant`. | ||
def version | ||
UUID.byte_version @data[6] | ||
end | ||
|
||
# Sets variant to a specified `value`. Doesn't set variant (see `UUID#variant=(value : Variant)`). | ||
def version=(value : Version) | ||
@data[6] = UUID.byte_version @data[6], value | ||
end | ||
|
||
{% for v in %w(1 2 3 4 5) %} | ||
|
||
# Returns `true` if UUID looks like V{{ v.id }}, `false` otherwise. | ||
def v{{ v.id }}? | ||
variant == Variant::RFC4122 && version == RFC4122::Version::V{{ v.id }} | ||
end | ||
|
||
# Returns `true` if UUID looks like V{{ v.id }}, raises `Error` otherwise. | ||
def v{{ v.id }}! | ||
unless v{{ v.id }}? | ||
raise Error.new("Invalid UUID variant #{variant} version #{version}, expected RFC 4122 V{{ v.id }}.") | ||
else | ||
true | ||
end | ||
end | ||
|
||
{% end %} | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
struct UUID | ||
# Raises `ArgumentError` if string `value` at index `i` doesn't contain hex digit followed by another hex digit. | ||
# TODO: Move to String#digits?(base, offset, size) or introduce strict String#[index, size].to_u8(base)! which doesn't | ||
# allow non-digits. The problem it solves is that " 1".to_u8(16) is fine but if it appears inside hexstring | ||
# it's not correct and there should be stdlib function to support it, without a need to build this kind of | ||
# helpers. | ||
def self.string_has_hex_pair_at!(value : String, i) | ||
unless value[i].hex? && value[i + 1].hex? | ||
raise ArgumentError.new [ | ||
"Invalid hex character at position #{i * 2} or #{i * 2 + 1}", | ||
"expected '0' to '9', 'a' to 'f' or 'A' to 'F'.", | ||
].join(", ") | ||
end | ||
end | ||
|
||
# String format. | ||
enum Format | ||
Hyphenated | ||
Hexstring | ||
URN | ||
end | ||
|
||
# Creates new UUID by decoding `value` string from hyphenated (ie. `ba714f86-cac6-42c7-8956-bcf5105e1b81`), | ||
# hexstring (ie. `89370a4ab66440c8add39e06f2bb6af6`) or URN (ie. `urn:uuid:3f9eaf9e-cdb0-45cc-8ecb-0e5b2bfb0c20`) | ||
# format. | ||
def decode(value : String) | ||
case value.size | ||
when 36 # Hyphenated | ||
[8, 13, 18, 23].each do |offset| | ||
if value[offset] != '-' | ||
raise ArgumentError.new "Invalid UUID string format, expected hyphen at char #{offset}." | ||
end | ||
end | ||
[0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34].each_with_index do |offset, i| | ||
::UUID.string_has_hex_pair_at! value, offset | ||
@data[i] = value[offset, 2].to_u8(16) | ||
end | ||
when 32 # Hexstring | ||
16.times do |i| | ||
::UUID.string_has_hex_pair_at! value, i * 2 | ||
@data[i] = value[i * 2, 2].to_u8(16) | ||
end | ||
when 45 # URN | ||
raise ArgumentError.new "Invalid URN UUID format, expected string starting with \":urn:uuid:\"." unless value.starts_with? "urn:uuid:" | ||
[9, 11, 13, 15, 18, 20, 23, 25, 28, 30, 33, 35, 37, 39, 41, 43].each_with_index do |offset, i| | ||
::UUID.string_has_hex_pair_at! value, offset | ||
@data[i] = value[offset, 2].to_u8(16) | ||
end | ||
else | ||
raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hexstring), 36 (hyphenated) or 46 (urn)." | ||
end | ||
end | ||
|
||
# Returns string in specified `format`. | ||
def to_s(format = Format::Hyphenated) | ||
slice = to_slice | ||
case format | ||
when Format::Hyphenated | ||
String.new(36) do |buffer| | ||
buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 | ||
slice[0, 4].hexstring(buffer + 0) | ||
slice[4, 2].hexstring(buffer + 9) | ||
slice[6, 2].hexstring(buffer + 14) | ||
slice[8, 2].hexstring(buffer + 19) | ||
slice[10, 6].hexstring(buffer + 24) | ||
{36, 36} | ||
end | ||
when Format::Hexstring | ||
slice.hexstring | ||
when Format::URN | ||
String.new(45) do |buffer| | ||
buffer.copy_from "urn:uuid:".to_unsafe, 9 | ||
(buffer + 9).copy_from to_s.to_unsafe, 36 | ||
{45, 45} | ||
end | ||
else | ||
raise ArgumentError.new "Unexpected format #{format}." | ||
end | ||
end | ||
|
||
# Same as `UUID#decode(value : String)`, returns `self`. | ||
def <<(value : String) | ||
decode value | ||
self | ||
end | ||
|
||
# Same as `UUID#variant=(value : Variant)`, returns `self`. | ||
def <<(value : Variant) | ||
variant = value | ||
self | ||
end | ||
|
||
# Same as `UUID#version=(value : Version)`, returns `self`. | ||
def <<(value : Version) | ||
version = value | ||
self | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this perhaps validate the format?