Skip to content

Instantly share code, notes, and snippets.

@wagenet
Created May 15, 2010 02:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wagenet/401942 to your computer and use it in GitHub Desktop.
Save wagenet/401942 to your computer and use it in GitHub Desktop.
require 'uuid'
module ActiveRecord
module UUID
def self.included(base)
base.send :extend, ClassMethods
base.send :include, InstanceMethods
class << base
alias_method :uuid_exists?, :exists?
end
base.class_eval do
set_primary_key 'uuid'
# Run this both times since we do want it before validation but we also want it even if validations aren't run
before_validation :generate_uuid, :on => :create
before_create :generate_uuid
validates_format_of :uuid, :with => /^[0-9A-Z]{8}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{12}$/
end
end
module ClassMethods
# Just a wrapper with upcasing
def generate_uuid
::UUID.create_random.to_s.upcase
end
end
module InstanceMethods
def initialize(attributes = nil)
uuid = if attributes.is_a?(Hash)
attributes.symbolize_keys!
attributes.delete(:uuid)
end
super
self.uuid = uuid
end
def generate_uuid
self.uuid ||= self.class.generate_uuid
end
private
# This should not ever happen, but we need to handle it just in case
def create
super
rescue ActiveRecord::StatementInvalid => e
if e.message =~ /Duplicate entry/
logger.error "Duplicate UUID for #{self.inspect}"
errors.add(:uuid, "is already in use")
false
else
raise e
end
end
end
end
end
#!/usr/bin/env ruby
# This copy from: github.com/spectra/ruby-uuid
### http://mput.dip.jp/mput/uuid.txt
# Copyright(c) 2005 URABE, Shyouhei.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this code, to deal in the code without restriction, including without
# limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the code, and to permit persons to whom the
# code is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the code.
#
# THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE CODE OR THE USE OR OTHER DEALINGS IN THE
# CODE.
#
# 2009-02-20: Modified by Pablo Lorenzoni <pablo@propus.com.br> to correctly
# include the version in the raw_bytes.
require 'digest/md5'
require 'digest/sha1'
require 'tmpdir'
# Pure ruby UUID generator, which is compatible with RFC4122
UUID = Struct.new :raw_bytes
class UUID
private_class_method :new
class << self
def mask19 v, str # :nodoc
nstr = str.bytes.to_a
version = [0, 16, 32, 48, 64, 80][v]
nstr[6] &= 0b00001111
nstr[6] |= version
nstr[7] &= 0b00001111
nstr[7] |= 0b01010000
nstr[8] &= 0b00111111
nstr[8] |= 0b10000000
str = ''
nstr.each { |s| str << s.chr }
str
end
def mask18 v, str # :nodoc
version = [0, 16, 32, 48, 64, 80][v]
str[6] &= 0b00001111
str[6] |= version
str[7] &= 0b00001111
str[7] |= 0b01010000
str[8] &= 0b00111111
str[8] |= 0b10000000
str
end
def mask v, str
if RUBY_VERSION >= "1.9.0"
return mask19 v, str
else
return mask18 v, str
end
end
private :mask, :mask18, :mask19
# UUID generation using SHA1. Recommended over create_md5.
# Namespace object is another UUID, some of them are pre-defined below.
def create_sha1 str, namespace
sha1 = Digest::SHA1.new
sha1.update namespace.raw_bytes
sha1.update str
sum = sha1.digest
raw = mask 5, sum[0..15]
ret = new raw
ret.freeze
ret
end
alias :create_v5 :create_sha1
# UUID generation using MD5 (for backward compat.)
def create_md5 str, namespace
md5 = Digest::MD5.new
md5.update namespace.raw_bytes
md5.update str
sum = md5.digest
raw = mask 3, sum[0..16]
ret = new raw
ret.freeze
ret
end
alias :create_v3 :create_md5
# UUID generation using random-number generator. From it's random
# nature, there's no warranty that the created ID is really universaly
# unique.
def create_random
rnd = [
rand(0x100000000),
rand(0x100000000),
rand(0x100000000),
rand(0x100000000),
].pack "N4"
raw = mask 4, rnd
ret = new raw
ret.freeze
ret
end
alias :create_v4 :create_random
def read_state fp # :nodoc:
fp.rewind
Marshal.load fp.read
end
def write_state fp, c, m # :nodoc:
fp.rewind
str = Marshal.dump [c, m]
fp.write str
end
private :read_state, :write_state
STATE_FILE = 'ruby-uuid'
# create the "version 1" UUID with current system clock, current UTC
# timestamp, and the IEEE 802 address (so-called MAC address).
#
# Speed notice: it's slow. It writes some data into hard drive on every
# invokation. If you want to speed this up, try remounting tmpdir with a
# memory based filesystem (such as tmpfs). STILL slow? then no way but
# rewrite it with c :)
def create clock=nil, time=nil, mac_addr=nil
c = t = m = nil
Dir.chdir Dir.tmpdir do
unless FileTest.exist? STATE_FILE then
# Generate a pseudo MAC address because we have no pure-ruby way
# to know the MAC address of the NIC this system uses. Note
# that cheating with pseudo arresses here is completely legal:
# see Section 4.5 of RFC4122 for details.
sha1 = Digest::SHA1.new
256.times do
r = [rand(0x100000000)].pack "N"
sha1.update r
end
str = sha1.digest
r = rand 14 # 20-6
node = str[r, 6] || str
node[0] |= 0x01 # multicast bit
k = rand 0x40000
open STATE_FILE, 'w' do |fp|
fp.flock IO::LOCK_EX
write_state fp, k, node
fp.chmod 0o777 # must be world writable
end
end
open STATE_FILE, 'r+' do |fp|
fp.flock IO::LOCK_EX
c, m = read_state fp
c = clock % 0x4000 if clock
m = mac_addr if mac_addr
t = time
if t.nil? then
# UUID epoch is 1582/Oct/15
tt = Time.now
t = tt.to_i*10000000 + tt.tv_usec*10 + 0x01B21DD213814000
end
c = c.succ # important; increment here
write_state fp, c, m
end
end
tl = t & 0xFFFF_FFFF
tm = t >> 32
tm = tm & 0xFFFF
th = t >> 48
th = th & 0x0FFF
th = th | 0x1000
cl = c & 0xFF
ch = c & 0x3F00
ch = ch >> 8
ch = ch | 0x80
pack tl, tm, th, cl, ch, m
end
alias :create_v1 :create
# A simple GUID parser: just ignores unknown characters and convert
# hexadecimal dump into 16-octet object.
def parse obj
str = obj.to_s.sub %r/\Aurn:uuid:/, ''
str.gsub! %r/[^0-9A-Fa-f]/, ''
raw = str[0..31]
raw = (raw.respond_to?(:lines) ? raw.lines : raw).to_a.pack 'H*'
ret = new raw
ret.freeze
ret
end
# The 'primitive constructor' of this class
# Note UUID.pack(uuid.unpack) == uuid
def pack tl, tm, th, ch, cl, n
raw = [tl, tm, th, ch, cl, n].pack "NnnCCa6"
ret = new raw
ret.freeze
ret
end
end
# The 'primitive deconstructor', or the dual to pack.
# Note UUID.pack(uuid.unpack) == uuid
def unpack
raw_bytes.unpack "NnnCCa6"
end
# Generate the string representation (a.k.a GUID) of this UUID
def to_s
a = unpack
tmp = a[-1].unpack 'C*'
a[-1] = sprintf '%02x%02x%02x%02x%02x%02x', *tmp
"%08x-%04x-%04x-%02x%02x-%s" % a
end
alias guid to_s
# Convert into a RFC4122-comforming URN representation
def to_uri
"urn:uuid:" + self.to_s
end
alias urn to_uri
# Convert into 128-bit unsigned integer
# Typically a Bignum instance, but can be a Fixnum.
def to_int
tmp = self.raw_bytes.unpack "C*"
tmp.inject do |r, i|
r * 256 | i
end
end
alias to_i to_int
# Gets the version of this UUID
# returns nil if bad version
def version
a = unpack
v = (a[2] & 0xF000).to_s(16)[0].chr.to_i
return v if (1..5).include? v
return nil
end
# Two UUIDs are said to be equal if and only if their (byte-order
# canonicalized) integer representations are equivallent. Refer RFC4122 for
# details.
def == other
to_i == other.to_i
end
include Comparable
# UUIDs are comparable (don't know what benefits are there, though).
def <=> other
to_s <=> other.to_s
end
# Pre-defined UUID Namespaces described in RFC4122 Appendix C.
NameSpace_DNS = parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
NameSpace_URL = parse "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
NameSpace_OID = parse "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
NameSpace_X500 = parse "6ba7b814-9dad-11d1-80b4-00c04fd430c8"
# The Nil UUID in RFC4122 Section 4.1.7
Nil = parse "00000000-0000-0000-0000-000000000000"
end
__END__
if __FILE__ == $0 then
require 'test/unit'
class TC_UUID < Test::Unit::TestCase
def test_v1
u1 = UUID.create
u2 = UUID.create
assert_not_equal u1, u2
end
def test_v1_repeatability
u1 = UUID.create 1, 2, "345678"
u2 = UUID.create 1, 2, "345678"
assert_equal u1, u2
end
def test_v3
u1 = UUID.create_md5 "foo", UUID::NameSpace_DNS
u2 = UUID.create_md5 "foo", UUID::NameSpace_DNS
u3 = UUID.create_md5 "foo", UUID::NameSpace_URL
assert_equal u1, u2
assert_not_equal u1, u3
end
def test_v5
u1 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
u2 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
u3 = UUID.create_sha1 "foo", UUID::NameSpace_URL
assert_equal u1, u2
assert_not_equal u1, u3
end
def test_v4
# This test is not perfect, because the random nature of version 4
# UUID it is not always true that the three objects below really
# differ. But in real life it's enough to say we're OK when this
# passes.
u1 = UUID.create_random
u2 = UUID.create_random
u3 = UUID.create_random
assert_not_equal u1.raw_bytes, u2.raw_bytes
assert_not_equal u1.raw_bytes, u3.raw_bytes
assert_not_equal u2.raw_bytes, u3.raw_bytes
end
def test_pack
u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
"\000\300O\3240\310"
assert_equal UUID::NameSpace_DNS, u1
end
def test_unpack
tl, tm, th, cl, ch, m = UUID::NameSpace_DNS.unpack
assert_equal 0x6ba7b810, tl
assert_equal 0x9dad, tm
assert_equal 0x11d1, th
assert_equal 0x80, cl
assert_equal 0xb4, ch
assert_equal "\000\300O\3240\310", m
end
def test_parse
u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
"\000\300O\3240\310"
u2 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
u3 = UUID.parse "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
assert_equal u1, u2
assert_equal u1, u3
end
def test_to_s
u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
assert_equal "6ba7b810-9dad-11d1-80b4-00c04fd430c8", u1.to_s
end
def test_to_i
u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
assert_equal 0x6ba7b8109dad11d180b400c04fd430c8, u1.to_i
end
def test_version
u1 = UUID.create_v1
assert_equal 1, u1.version
u3 = UUID.create_v3 "foo", UUID::NameSpace_DNS
assert_equal 3, u3.version
u4 = UUID.create_v4
assert_equal 4, u4.version
u5 = UUID.create_v5 "foo", UUID::NameSpace_DNS
assert_equal 5, u5.version
end
end
end
# Local Variables:
# mode: ruby
# code: utf-8
# indent-tabs-mode: t
# tab-width: 3
# ruby-indent-level: 3
# fill-column: 79
# default-justification: full
# End:
# vi: ts=3 sw=3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment