cuid 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.
- data/.gitignore +6 -0
- data/.rbenv-version +1 -0
- data/.travis.yml +6 -0
- data/.yardopts +5 -0
- data/Gemfile +5 -0
- data/README.md +71 -0
- data/Rakefile +8 -0
- data/cuid.gemspec +17 -0
- data/doc/index.html +1164 -0
- data/lib/cuid.rb +175 -0
- data/lib/cuid/version.rb +3 -0
- data/test/test_cuid.rb +58 -0
- metadata +58 -0
data/lib/cuid.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require "socket"
|
2
|
+
require 'cuid/version'
|
3
|
+
begin
|
4
|
+
require "securerandom"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
# Cuid is a library for generating unique collision-resistant IDs optimized for horizontal scaling and performance
|
10
|
+
#
|
11
|
+
# @example Generate a hash
|
12
|
+
# hash = Cuid::generate #=> "ch8qypsnz0000a4welw8anyr"
|
13
|
+
#
|
14
|
+
# @example Generate 2 hashes
|
15
|
+
# hashes = Cuid::generate(2) #=> ["ch8qyq35f0002a4wekjcwmh30", "ch8qyq35f0003a4weewy22izq"]
|
16
|
+
#
|
17
|
+
# @see http://github.com/dilvie/cuid Original author's detailed explanation of the cuid spec
|
18
|
+
|
19
|
+
module Cuid
|
20
|
+
##
|
21
|
+
# @private
|
22
|
+
@count = 0
|
23
|
+
|
24
|
+
##
|
25
|
+
# length of each segment of the hash
|
26
|
+
BLOCK_SIZE = 4
|
27
|
+
|
28
|
+
##
|
29
|
+
# size of the alphabet (e.g. base36 is [a-z0-9])
|
30
|
+
BASE = 36
|
31
|
+
|
32
|
+
##
|
33
|
+
# @private
|
34
|
+
# maximum number that can be stored in a block of the specified size using the specified alphabet
|
35
|
+
DISCRETE_VALUES = (BASE ** BLOCK_SIZE) - 1
|
36
|
+
|
37
|
+
##
|
38
|
+
# size of the random segment of the block
|
39
|
+
RAND_SIZE = BLOCK_SIZE * 2
|
40
|
+
|
41
|
+
##
|
42
|
+
# @private
|
43
|
+
# maximum number that can be stored in the random block
|
44
|
+
RAND_MAX = (BASE ** RAND_SIZE) - 1
|
45
|
+
|
46
|
+
##
|
47
|
+
# @private
|
48
|
+
# minimum number that can be stored in the random block (otherwise it will be too short)
|
49
|
+
RAND_MIN = BASE ** (RAND_SIZE - 1)
|
50
|
+
|
51
|
+
##
|
52
|
+
# @private
|
53
|
+
# first letter of the hash
|
54
|
+
LETTER = "c"
|
55
|
+
|
56
|
+
class << self
|
57
|
+
|
58
|
+
##
|
59
|
+
# Returns one or more hashes based on the parameter supplied
|
60
|
+
#
|
61
|
+
# @overload generate()
|
62
|
+
# Returns one hash when called with no parameters or a parameter of 1
|
63
|
+
#
|
64
|
+
# @param [optional, Integer] quantity determines number of hashes returned (must be nil, 0 or 1)
|
65
|
+
# @return [String]
|
66
|
+
#
|
67
|
+
# @overload generate(quantity)
|
68
|
+
# Returns an array of hashes when called with a parameter greater than 1
|
69
|
+
#
|
70
|
+
# @param [Integer] quantity determines number of hashes returned (must be greater than 1)
|
71
|
+
# @return [Array<String>]
|
72
|
+
#
|
73
|
+
# @overload generate(quantity,secure_random)
|
74
|
+
# Returns an array of hashes when called with a parameter greater than 1
|
75
|
+
#
|
76
|
+
# @param [Integer] quantity determines number of hashes returned (must be greater than 1)
|
77
|
+
# @param [Boolean] secure_random attempts to use SecureRandom if set to True (Ruby 1.9.2 and up; reverts to Kernel#rand if SecureRandom is not supported)
|
78
|
+
def generate(quantity=1,secure_random=false)
|
79
|
+
@use_secure_random = secure_random
|
80
|
+
@fingerprint = get_fingerprint # only need to get the fingerprint once because it is constant per-run
|
81
|
+
return api unless quantity > 1
|
82
|
+
|
83
|
+
values = Array(1.upto(quantity)) # create an array of the correct size
|
84
|
+
return values.collect { api } # fill array with hashes
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Validates (minimally) the supplied string is in the correct format to be a hash
|
89
|
+
#
|
90
|
+
# Validation checks that the first letter is correct and that the rest of the
|
91
|
+
# string is the correct length and consists of lower case letters and numbers.
|
92
|
+
#
|
93
|
+
# @param [String] str string to check
|
94
|
+
# @return [Boolean] returns true if the format is correct
|
95
|
+
def validate(str)
|
96
|
+
blen = BLOCK_SIZE * 6
|
97
|
+
!!str.match(/#{LETTER}[a-z0-9]{#{blen}}/)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
##
|
103
|
+
# Collects and asssembles the pieces into the actual hash string.
|
104
|
+
#
|
105
|
+
# @private
|
106
|
+
def api
|
107
|
+
timestamp = (Time.now.to_f * 1000).truncate.to_s(BASE)
|
108
|
+
|
109
|
+
random = get_random_block
|
110
|
+
|
111
|
+
@count = @count % DISCRETE_VALUES
|
112
|
+
counter = pad(@count.to_s(BASE))
|
113
|
+
|
114
|
+
@count += 1
|
115
|
+
|
116
|
+
return (LETTER + timestamp + counter + @fingerprint + random)
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Returns a string which has been converted to the correct size alphabet as defined in
|
121
|
+
# the BASE constant (e.g. base36) and then padded or trimmed to the correct length.
|
122
|
+
#
|
123
|
+
# @private
|
124
|
+
def format(text,size=BLOCK_SIZE)
|
125
|
+
base36_text = text.to_s(BASE)
|
126
|
+
return (base36_text.length > size) ? trim(base36_text,size) : pad(base36_text,size)
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Returns a string trimmed to the length supplied or BLOCK_SIZE if no length is supplied.
|
131
|
+
#
|
132
|
+
# @private
|
133
|
+
def trim(text,max=BLOCK_SIZE)
|
134
|
+
original_length = text.length
|
135
|
+
return text[original_length-max,max]
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Returns a string right-justified and padded with zeroes up to the length supplied or
|
140
|
+
# BLOCK_SIZE if no length is supplied.
|
141
|
+
#
|
142
|
+
# @private
|
143
|
+
def pad(text,size=BLOCK_SIZE)
|
144
|
+
return text.rjust(size,"0")
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Generates a random string.
|
149
|
+
#
|
150
|
+
# @private
|
151
|
+
def get_random_block
|
152
|
+
@secure_random = defined?(SecureRandom) if @secure_random.nil?
|
153
|
+
if @secure_random && @use_secure_random then
|
154
|
+
number = SecureRandom.random_number(RAND_MAX - RAND_MIN) + RAND_MIN
|
155
|
+
else
|
156
|
+
number = ((rand * (RAND_MAX - RAND_MIN)) + RAND_MIN)
|
157
|
+
end
|
158
|
+
return number.truncate.to_s(BASE)
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Assembles the host fingerprint based on the hostname and the PID.
|
163
|
+
#
|
164
|
+
# @private
|
165
|
+
def get_fingerprint
|
166
|
+
padding = 2
|
167
|
+
hostname = Socket.gethostname
|
168
|
+
hostid = hostname.split('').inject(hostname.length+BASE) do |a,i|
|
169
|
+
a += (i.respond_to? "ord") ? i.ord : i[0]
|
170
|
+
end
|
171
|
+
return format($$, padding) + format(hostid, padding)
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
data/lib/cuid/version.rb
ADDED
data/test/test_cuid.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'cuid'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class CuidTest < Test::Unit::TestCase
|
5
|
+
def test_generate_string
|
6
|
+
c = Cuid::generate
|
7
|
+
assert c.is_a? String
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_generate_array
|
11
|
+
c = Cuid::generate(3)
|
12
|
+
assert c.is_a? Array
|
13
|
+
assert_equal 3, c.size
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_generate_format
|
17
|
+
c = Cuid::generate
|
18
|
+
assert_equal 25, c.length
|
19
|
+
assert c.start_with? "c"
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_validate_true
|
23
|
+
c = Cuid::generate
|
24
|
+
assert Cuid::validate(c)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_valiate_false
|
28
|
+
c = "d00000000000000000000"
|
29
|
+
assert !Cuid::validate(c)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_collision
|
33
|
+
results = {}
|
34
|
+
collision = false
|
35
|
+
c = Cuid::generate(600000)
|
36
|
+
c.each do |e|
|
37
|
+
collision = true if results[e]
|
38
|
+
results[e] = true
|
39
|
+
end
|
40
|
+
assert !collision
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_secure_random
|
44
|
+
results = {}
|
45
|
+
collision = false
|
46
|
+
c = Cuid::generate(1000,true)
|
47
|
+
c.each do |e|
|
48
|
+
collision = true if results[e]
|
49
|
+
results[e] = true
|
50
|
+
end
|
51
|
+
assert !collision
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_version
|
55
|
+
assert_nothing_raised { Cuid::VERSION }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cuid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ian Shannon
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-15 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Ruby implementation of Eric Elliot's javascript cuid
|
15
|
+
email:
|
16
|
+
- iyshannon@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- .rbenv-version
|
23
|
+
- .travis.yml
|
24
|
+
- .yardopts
|
25
|
+
- Gemfile
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- cuid.gemspec
|
29
|
+
- doc/index.html
|
30
|
+
- lib/cuid.rb
|
31
|
+
- lib/cuid/version.rb
|
32
|
+
- test/test_cuid.rb
|
33
|
+
homepage: http://github.com/iyshannon/cuid
|
34
|
+
licenses: []
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.8.24
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: Collision-resistant ids optimized for horizontal scaling and performance
|
57
|
+
test_files: []
|
58
|
+
has_rdoc:
|