noid 0.7.1 → 0.7.2
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.
- checksums.yaml +5 -13
- data/.rspec +1 -0
- data/.travis.yml +4 -3
- data/Gemfile +6 -3
- data/Rakefile +6 -7
- data/VERSION +1 -1
- data/lib/noid.rb +8 -5
- data/lib/noid/minter.rb +24 -51
- data/lib/noid/template.rb +97 -82
- data/lib/noid/version.rb +1 -1
- data/noid.gemspec +15 -15
- data/spec/lib/minter_spec.rb +153 -124
- data/spec/lib/template_spec.rb +16 -0
- data/spec/spec_helper.rb +2 -3
- metadata +18 -18
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZWY4NGM2YjcwMGViNmQxMTk1MmU1OTYzNGFiMDQ1ODk1YTI1ZjE3Mw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 50e4fa485ed73bb6f4d313b5fda35a6ecfeb08e7
|
4
|
+
data.tar.gz: 763d9e2e9f279a0afa7461b9f2ddee88d00e1318
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
MDcyZDFhMmE5ZWQxZDVhOWIwYzRmNjdjOGViZjA4NDQ4ZjcwOTViOWU3ZDcz
|
11
|
-
NDE5ZjhhNWIwMDk3Y2IzZGUzZDI1Zjk5ZDE3NzBlYTE1MDVjNGE=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
MDdlODMyZDMxYWExNzM5MTY4ZGY5ZjkzNzJhMDc0MDUwODg0OTA2OGEwNDJh
|
14
|
-
MzRmNDdjNTk0NWE4NzkzZTYwMDJlNTBmZGY0MjgwZmMwZWNlNjgwMWMzZTYw
|
15
|
-
NjYyNWM3OGMxYzMxNTVmOGZkNTU1Y2RkMDQ5MTc2Y2I4YzczY2M=
|
6
|
+
metadata.gz: b5d19a31918d22133f8c282e3deb6baab51e27cbe7fb65797a503bffff474b34964bef5d9e81d657f591653d807e0be33b5ab2bab644ac633a9904561fecf66c
|
7
|
+
data.tar.gz: 48e622908ddedc36c1296ecdbb9818694f7f3a680f5d610fc9115254d630bcb6489519bf00903ad1f6d751254fa5816ab4c989a93014b73439b1d99c8dae73d9
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
source
|
1
|
+
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in test.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
|
7
|
-
gem 'simplecov
|
6
|
+
group :development, :test do
|
7
|
+
gem 'simplecov'
|
8
|
+
gem 'simplecov-rcov'
|
9
|
+
gem 'byebug', require: false
|
10
|
+
end
|
data/Rakefile
CHANGED
@@ -15,10 +15,10 @@ require 'rspec'
|
|
15
15
|
require 'rspec/core/rake_task'
|
16
16
|
|
17
17
|
desc 'Default: run specs.'
|
18
|
-
task :
|
18
|
+
task default: :spec
|
19
19
|
|
20
20
|
RSpec::Core::RakeTask.new do |t|
|
21
|
-
if ENV['COVERAGE']
|
21
|
+
if ENV['COVERAGE'] && RUBY_VERSION =~ /^1.8/
|
22
22
|
t.rcov = true
|
23
23
|
t.rcov_opts = ['--exclude', 'spec', '--exclude', 'gems']
|
24
24
|
end
|
@@ -32,14 +32,13 @@ begin
|
|
32
32
|
doc_destination = File.join(project_root, 'doc')
|
33
33
|
|
34
34
|
YARD::Rake::YardocTask.new(:doc) do |yt|
|
35
|
-
yt.files = Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
|
36
|
-
|
35
|
+
yt.files = Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
|
36
|
+
[File.join(project_root, 'README.md')]
|
37
37
|
yt.options = ['--output-dir', doc_destination, '--readme', 'README.md']
|
38
38
|
end
|
39
39
|
rescue LoadError
|
40
|
-
desc
|
40
|
+
desc 'Generate YARD Documentation'
|
41
41
|
task :doc do
|
42
|
-
abort
|
42
|
+
abort 'Please install the YARD gem to generate rdoc.'
|
43
43
|
end
|
44
44
|
end
|
45
|
-
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.2
|
data/lib/noid.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require 'noid/version'
|
2
|
+
require 'noid/minter'
|
3
|
+
require 'noid/template'
|
4
4
|
|
5
5
|
module Noid
|
6
|
-
|
7
|
-
|
6
|
+
XDIGIT = %w(0 1 2 3 4 5 6 7 8 9 b c d f g h j k m n p q r s t v w x z)
|
7
|
+
MAX_COUNTERS = 293
|
8
|
+
|
9
|
+
class TemplateError < StandardError
|
10
|
+
end
|
8
11
|
end
|
data/lib/noid/minter.rb
CHANGED
@@ -1,74 +1,43 @@
|
|
1
1
|
module Noid
|
2
2
|
class Minter
|
3
|
-
attr_reader :seed, :seq
|
3
|
+
attr_reader :seed, :seq, :template
|
4
4
|
attr_writer :counters
|
5
5
|
|
6
|
-
def initialize
|
7
|
-
|
6
|
+
def initialize(options = {})
|
8
7
|
if options[:state]
|
9
|
-
|
8
|
+
# Only set the sequence ivar if this is a stateful minter
|
10
9
|
@seq = options[:seq]
|
11
|
-
@template_string = options[:template]
|
12
|
-
@counters = options[:counters]
|
13
10
|
else
|
14
|
-
seed(options[:seed], options[:seq])
|
15
|
-
@template_string = options[:template]
|
16
11
|
@max_counters = options[:max_counters]
|
17
|
-
@counters = options[:counters]
|
18
|
-
|
19
12
|
@after_mint = options[:after_mint]
|
20
13
|
end
|
21
|
-
@
|
22
|
-
|
14
|
+
@counters = options[:counters]
|
15
|
+
@template = Template.new(options[:template])
|
16
|
+
seed(options[:seed], options[:seq])
|
17
|
+
end
|
23
18
|
|
24
19
|
##
|
25
20
|
# Mint a new identifier
|
26
21
|
def mint
|
27
22
|
n = next_in_sequence
|
28
23
|
id = template.mint(n)
|
29
|
-
if @after_mint
|
30
|
-
@after_mint.call(self, id)
|
31
|
-
end
|
24
|
+
@after_mint.call(self, id) if @after_mint
|
32
25
|
id
|
33
26
|
end
|
34
27
|
|
35
|
-
##
|
36
|
-
# Noid identifier template
|
37
|
-
#
|
38
|
-
# @return Noid::Template
|
39
|
-
def template
|
40
|
-
@template
|
41
|
-
end
|
42
|
-
|
43
28
|
##
|
44
29
|
# Is the identifier valid under the template string and checksum?
|
45
30
|
# @param [String] id
|
46
31
|
# @return bool
|
47
|
-
def valid?
|
48
|
-
|
49
|
-
ch = @template.prefix.empty? ? id.split('') : id[@template.prefix.length..-1].split('')
|
50
|
-
check = ch.pop if @template.checkdigit?
|
51
|
-
return false unless prefix == @template.prefix
|
52
|
-
|
53
|
-
return false unless @template.characters.length == ch.length
|
54
|
-
@template.characters.split('').each_with_index do |c, i|
|
55
|
-
return false unless Noid::XDIGIT.include? ch[i]
|
56
|
-
return false if c == 'd' and ch[i] =~ /[^\d]/
|
57
|
-
end
|
58
|
-
|
59
|
-
return false unless check.nil? or check == @template.checkdigit(id[0..-2])
|
60
|
-
|
61
|
-
true
|
32
|
+
def valid?(id)
|
33
|
+
template.valid?(id)
|
62
34
|
end
|
63
35
|
|
64
36
|
##
|
65
37
|
# Seed the random number generator with a seed and sequence offset
|
66
|
-
# @param [Integer] seed
|
67
|
-
# @param [Integer] seq
|
68
38
|
# @return [Random]
|
69
|
-
def seed
|
70
|
-
@rand =
|
71
|
-
@rand ||= ::Random.new
|
39
|
+
def seed(seed_number = nil, seq = 0)
|
40
|
+
@rand = seed_number ? Random.new(seed_number) : Random.new
|
72
41
|
@seed = @rand.seed
|
73
42
|
@seq = seq || 0
|
74
43
|
|
@@ -80,27 +49,31 @@ module Noid
|
|
80
49
|
def next_in_sequence
|
81
50
|
n = @seq
|
82
51
|
@seq += 1
|
83
|
-
|
84
|
-
|
85
|
-
|
52
|
+
if template.generator == 'r'
|
53
|
+
next_random
|
54
|
+
else
|
55
|
+
n
|
86
56
|
end
|
87
|
-
n
|
88
57
|
end
|
89
58
|
|
90
59
|
def next_random
|
91
|
-
raise
|
92
|
-
i =
|
60
|
+
raise 'Exhausted noid sequence pool' if counters.size == 0
|
61
|
+
i = random_bucket
|
93
62
|
n = counters[i][:value]
|
94
63
|
counters[i][:value] += 1
|
95
64
|
counters.delete_at(i) if counters[i][:value] == counters[i][:max]
|
96
65
|
n
|
97
66
|
end
|
98
67
|
|
68
|
+
def random_bucket
|
69
|
+
@rand.rand(counters.size)
|
70
|
+
end
|
71
|
+
|
99
72
|
##
|
100
73
|
# Counters to use for quasi-random NOID sequences
|
101
74
|
def counters
|
102
75
|
return @counters if @counters
|
103
|
-
return [] unless template.generator ==
|
76
|
+
return [] unless template.generator == 'r'
|
104
77
|
|
105
78
|
percounter = template.max / (@max_counters || Noid::MAX_COUNTERS) + 1
|
106
79
|
t = 0
|
@@ -120,7 +93,7 @@ module Noid
|
|
120
93
|
end
|
121
94
|
|
122
95
|
def dump
|
123
|
-
{ :
|
96
|
+
{ state: true, seq: @seq, seed: @seed, template: template.template, counters: Marshal.load(Marshal.dump(counters)) }
|
124
97
|
end
|
125
98
|
end
|
126
99
|
end
|
data/lib/noid/template.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
module Noid
|
2
2
|
class Template
|
3
|
-
attr_reader :template
|
3
|
+
attr_reader :template, :prefix, :generator, :characters
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
VALID_PATTERN = /\A(.*)\.([rsz])([ed]+)(k?)\Z/
|
6
|
+
|
7
|
+
# @param [String] template A Template is a coded string of the form Prefix.Mask that governs how identifiers will be minted.
|
8
|
+
def initialize(template)
|
9
|
+
@template = template
|
10
|
+
parse!
|
8
11
|
end
|
9
12
|
|
10
|
-
def mint
|
13
|
+
def mint(n)
|
11
14
|
str = prefix
|
12
15
|
str += n2xdig(n)
|
13
16
|
str += checkdigit(str) if checkdigit?
|
@@ -15,103 +18,116 @@ module Noid
|
|
15
18
|
str
|
16
19
|
end
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
characters.split('').each_with_index do |c, i|
|
28
|
-
case c
|
29
|
-
when 'e'
|
30
|
-
return false unless Noid::XDIGIT.include? str[prefix.length + i]
|
31
|
-
when 'd'
|
32
|
-
return false unless str[prefix.length + i] =~ /\d/
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
return false unless checkdigit(str[0..-2]) == str.split('').last if checkdigit?
|
37
|
-
|
21
|
+
##
|
22
|
+
# Is the string valid against this template string and checksum?
|
23
|
+
# @param [String] str
|
24
|
+
# @return bool
|
25
|
+
def valid?(str)
|
26
|
+
match = validation_regex.match(str)
|
27
|
+
return false if match.nil?
|
28
|
+
return checkdigit(match[1]) == match[3] if checkdigit?
|
38
29
|
true
|
39
30
|
end
|
40
31
|
|
41
32
|
##
|
42
|
-
#
|
43
|
-
|
44
|
-
|
33
|
+
# calculate a checkdigit for the str
|
34
|
+
# @param [String] str
|
35
|
+
# @return [String] checkdigit
|
36
|
+
def checkdigit(str)
|
37
|
+
Noid::XDIGIT[str.split('').map { |x| Noid::XDIGIT.index(x).to_i }.each_with_index.map { |n, idx| n * (idx + 1) }.inject { |sum, n| sum + n } % Noid::XDIGIT.length]
|
45
38
|
end
|
46
39
|
|
47
40
|
##
|
48
|
-
#
|
49
|
-
def
|
50
|
-
@
|
41
|
+
# minimum sequence value
|
42
|
+
def min
|
43
|
+
@min ||= 0
|
51
44
|
end
|
52
45
|
|
53
46
|
##
|
54
|
-
#
|
55
|
-
def
|
56
|
-
@
|
47
|
+
# maximum sequence value for the template
|
48
|
+
def max
|
49
|
+
@max ||= if generator == 'z'
|
50
|
+
nil
|
51
|
+
else
|
52
|
+
size_list.inject(1) { |total, x| total * x }
|
53
|
+
end
|
57
54
|
end
|
58
55
|
|
56
|
+
protected
|
57
|
+
|
59
58
|
##
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
# A noid has the structure (prefix)(code)(checkdigit)
|
60
|
+
# the regexp has the following captures
|
61
|
+
# 1 - the prefix and the code
|
62
|
+
# 2 - the changing id characters (not the prefix and not the checkdigit)
|
63
|
+
# 3 - the checkdigit, if there is one. This field is missing if there is no checkdigit
|
64
|
+
def validation_regex
|
65
|
+
@validation_regex ||= begin
|
66
|
+
character_pattern = ''
|
67
|
+
# the first character in the mask after the type character is the most significant
|
68
|
+
# acc. to the Noid spec (p.9):
|
69
|
+
# https://wiki.ucop.edu/display/Curation/NOID?preview=/16744482/16973835/noid.pdf
|
70
|
+
character_pattern += character_to_pattern(character_list.first) + "*" if generator == 'z'
|
71
|
+
character_pattern += character_list.map { |c| character_to_pattern(c) }.join
|
72
|
+
|
73
|
+
%r{\A(#{Regexp.escape(prefix)}(#{character_pattern}))(#{character_to_pattern('k') if checkdigit?})\Z}
|
74
|
+
end
|
69
75
|
end
|
70
76
|
|
71
77
|
##
|
72
|
-
#
|
73
|
-
|
74
|
-
|
78
|
+
# parse template and put the results into instance variables
|
79
|
+
# raise an exception if there is a parse error
|
80
|
+
def parse!
|
81
|
+
match = VALID_PATTERN.match(template)
|
82
|
+
raise Noid::TemplateError, "Malformed noid template '#{template}'" unless match
|
83
|
+
@prefix = match[1]
|
84
|
+
@generator = match[2]
|
85
|
+
@characters = match[3]
|
86
|
+
@checkdigit = (match[4] == 'k')
|
75
87
|
end
|
76
88
|
|
77
|
-
|
78
|
-
|
79
|
-
# @param [String] str
|
80
|
-
# @return [String] checkdigit
|
81
|
-
def checkdigit str
|
82
|
-
Noid::XDIGIT[str.split('').map { |x| Noid::XDIGIT.index(x).to_i }.each_with_index.map { |n, idx| n*(idx+1) }.inject { |sum, n| sum += n } % Noid::XDIGIT.length ]
|
89
|
+
def xdigit_pattern
|
90
|
+
@xdigit_pattern ||= '[' + Noid::XDIGIT.join('') + ']'
|
83
91
|
end
|
84
92
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
93
|
+
def character_to_pattern(c)
|
94
|
+
case c
|
95
|
+
when 'e', 'k'
|
96
|
+
xdigit_pattern
|
97
|
+
when 'd'
|
98
|
+
'\d'
|
99
|
+
else
|
100
|
+
''
|
101
|
+
end
|
89
102
|
end
|
90
103
|
|
91
104
|
##
|
92
|
-
#
|
93
|
-
def
|
94
|
-
@
|
95
|
-
case generator
|
96
|
-
when 'z'
|
97
|
-
nil
|
98
|
-
else
|
99
|
-
characters.split('').map { |x| character_space(x) }.compact.inject(1) { |total, x| total *= x }
|
100
|
-
end
|
101
|
-
end
|
105
|
+
# Return a list giving the number of possible characters at each position
|
106
|
+
def size_list
|
107
|
+
@size_list ||= character_list.map { |c| character_space(c) }
|
102
108
|
end
|
103
109
|
|
110
|
+
def character_list
|
111
|
+
characters.split('')
|
112
|
+
end
|
113
|
+
|
114
|
+
def mask
|
115
|
+
generator + characters
|
116
|
+
end
|
117
|
+
|
118
|
+
def checkdigit?
|
119
|
+
@checkdigit
|
120
|
+
end
|
104
121
|
|
105
|
-
protected
|
106
122
|
##
|
107
123
|
# total size of a given template character value
|
108
124
|
# @param [String] c
|
109
|
-
def character_space
|
125
|
+
def character_space(c)
|
110
126
|
case c
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
127
|
+
when 'e'
|
128
|
+
Noid::XDIGIT.length
|
129
|
+
when 'd'
|
130
|
+
10
|
115
131
|
end
|
116
132
|
end
|
117
133
|
|
@@ -119,26 +135,25 @@ module Noid
|
|
119
135
|
# convert a minter position to a noid string under this template
|
120
136
|
# @param [Integer] n
|
121
137
|
# @return [String]
|
122
|
-
def n2xdig
|
123
|
-
xdig =
|
124
|
-
value = n %
|
125
|
-
n
|
138
|
+
def n2xdig(n)
|
139
|
+
xdig = size_list.reverse.map { |size|
|
140
|
+
value = n % size
|
141
|
+
n /= size
|
126
142
|
Noid::XDIGIT[value]
|
127
|
-
|
143
|
+
}.compact.join('')
|
128
144
|
|
129
145
|
if generator == 'z'
|
130
|
-
|
146
|
+
size = size_list.last
|
131
147
|
while n > 0
|
132
|
-
value = n %
|
133
|
-
n
|
148
|
+
value = n % size
|
149
|
+
n /= size
|
134
150
|
xdig += Noid::XDIGIT[value]
|
135
151
|
end
|
136
152
|
end
|
137
153
|
|
138
|
-
raise
|
154
|
+
raise 'Exhausted noid sequence pool' if n > 0
|
139
155
|
|
140
156
|
xdig.reverse
|
141
157
|
end
|
142
|
-
|
143
158
|
end
|
144
159
|
end
|
data/lib/noid/version.rb
CHANGED
data/noid.gemspec
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
3
|
-
require
|
2
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'noid/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.name =
|
6
|
+
s.name = 'noid'
|
7
7
|
s.version = Noid::VERSION
|
8
|
-
s.authors = [
|
9
|
-
s.email = [
|
10
|
-
s.homepage =
|
11
|
-
s.summary =
|
12
|
-
s.description =
|
13
|
-
s.licenses = [
|
8
|
+
s.authors = ['Chris Beer']
|
9
|
+
s.email = ['chris@cbeer.info']
|
10
|
+
s.homepage = 'http://github.com/microservices/noid'
|
11
|
+
s.summary = 'Nice Opaque Identifier'
|
12
|
+
s.description = 'Nice Opaque Identifier'
|
13
|
+
s.licenses = ['MIT']
|
14
14
|
|
15
|
-
s.rubyforge_project =
|
15
|
+
s.rubyforge_project = 'noid'
|
16
16
|
|
17
17
|
s.files = `git ls-files`.split("\n")
|
18
18
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
-
s.require_paths = [
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
20
|
+
s.require_paths = ['lib']
|
21
21
|
|
22
22
|
s.required_ruby_version = '>= 1.9.3'
|
23
23
|
|
24
|
-
s.add_development_dependency
|
25
|
-
s.add_development_dependency
|
26
|
-
s.add_development_dependency
|
24
|
+
s.add_development_dependency 'bundler'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'rspec', '>= 3.0'
|
27
27
|
end
|
data/spec/lib/minter_spec.rb
CHANGED
@@ -1,152 +1,158 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Noid::Minter do
|
4
|
-
it
|
5
|
-
minter =
|
6
|
-
minter.mint.
|
4
|
+
it 'mints a few random 3-digit numbers' do
|
5
|
+
minter = described_class.new(template: '.rddd')
|
6
|
+
expect(minter.mint).to match(/\d\d\d/)
|
7
7
|
end
|
8
8
|
|
9
|
-
it
|
10
|
-
minter =
|
11
|
-
1000.times { minter.mint.
|
12
|
-
expect { minter.mint }.to raise_exception
|
9
|
+
it 'mints random 3-digit numbers, stopping after the 1000th' do
|
10
|
+
minter = described_class.new(template: '.rddd')
|
11
|
+
1000.times { expect(minter.mint).to match(/^\d\d\d$/) }
|
12
|
+
expect { minter.mint }.to raise_exception(RuntimeError, /Exhausted noid sequence pool/)
|
13
13
|
end
|
14
14
|
|
15
|
-
it
|
16
|
-
minter =
|
17
|
-
minter.mint.
|
18
|
-
999.times { minter.mint.
|
19
|
-
minter.mint.
|
15
|
+
it 'mints sequential numbers without limit, adding new digits as needed' do
|
16
|
+
minter = described_class.new(template: '.zd')
|
17
|
+
expect(minter.mint).to eq('0')
|
18
|
+
999.times { expect(minter.mint).to match(/\d/) }
|
19
|
+
expect(minter.mint).to eq('1000')
|
20
20
|
end
|
21
21
|
|
22
|
-
it
|
23
|
-
minter =
|
24
|
-
1000.times { minter.mint.
|
22
|
+
it 'mints random 4-digit numbers with constant prefix bc' do
|
23
|
+
minter = described_class.new(template: 'bc.rdddd')
|
24
|
+
1000.times { expect(minter.mint).to match(/^bc\d\d\d\d$/) }
|
25
25
|
end
|
26
26
|
|
27
|
-
it
|
28
|
-
minter =
|
29
|
-
minter.mint.
|
30
|
-
10.times { minter.mint.
|
31
|
-
minter.mint.
|
32
|
-
88.times { minter.mint.
|
33
|
-
expect { minter.mint }.to raise_exception
|
27
|
+
it 'mints sequential 2-digit numbers with constant prefix 8rf' do
|
28
|
+
minter = described_class.new(template: '8rf.sdd')
|
29
|
+
expect(minter.mint).to eq('8rf00')
|
30
|
+
10.times { expect(minter.mint).to match(/^8rf\d\d$/) }
|
31
|
+
expect(minter.mint).to eq('8rf11')
|
32
|
+
88.times { expect(minter.mint).to match(/^8rf\d\d$/) }
|
33
|
+
expect { minter.mint }.to raise_exception(RuntimeError, /Exhausted noid sequence pool/)
|
34
34
|
end
|
35
35
|
|
36
|
-
it
|
37
|
-
minter =
|
38
|
-
29.times.map { minter.mint }.join('').
|
36
|
+
it 'mints sequential extended-digits' do
|
37
|
+
minter = described_class.new(template: '.se')
|
38
|
+
expect(29.times.map { minter.mint }.join('')).to eq('0123456789bcdfghjkmnpqrstvwxz')
|
39
39
|
end
|
40
|
-
|
41
|
-
it "should mint random 3-extended-digit numbers with constant prefix h9" do
|
42
|
-
minter = Noid::Minter.new(:template => 'h9.reee')
|
43
40
|
|
44
|
-
|
45
|
-
|
41
|
+
it 'mints random 3-extended-digit numbers with constant prefix h9' do
|
42
|
+
minter = described_class.new(template: 'h9.reee')
|
43
|
+
|
44
|
+
(minter.template.max).times { expect(minter.mint).to match(/^h9\w\w\w$/) }
|
45
|
+
expect { minter.mint }.to raise_exception(RuntimeError, /Exhausted noid sequence pool/)
|
46
46
|
end
|
47
47
|
|
48
|
-
it
|
49
|
-
minter =
|
50
|
-
(29*29*29).times { minter.mint.
|
51
|
-
minter.mint.
|
48
|
+
it 'mints unlimited sequential numbers with at least 3 extended digits' do
|
49
|
+
minter = described_class.new(template: '.zeee')
|
50
|
+
(29 * 29 * 29).times { expect(minter.mint).to match(/^\w\w\w/) }
|
51
|
+
expect(minter.mint).to match(/^\w\w\w\w/)
|
52
52
|
end
|
53
53
|
|
54
|
-
it
|
55
|
-
minter =
|
56
|
-
1000.times { minter.mint.
|
54
|
+
it 'mints random 7-char numbers, with extended digits at chars 2,4,and 5' do
|
55
|
+
minter = described_class.new(template: '.rdedeedd')
|
56
|
+
1000.times { expect(minter.mint).to match(/^\d\w\d\w\w\d\d$/) }
|
57
57
|
end
|
58
58
|
|
59
|
-
it
|
60
|
-
minter =
|
61
|
-
minter.mint.
|
59
|
+
it 'mints unlimited mixed digits, adding new extended digits as needed' do
|
60
|
+
minter = described_class.new(template: '.zedededed')
|
61
|
+
expect(minter.mint).to eq('00000000')
|
62
62
|
end
|
63
63
|
|
64
|
-
it
|
65
|
-
minter =
|
66
|
-
minter.mint.
|
67
|
-
1000.times { minter.mint.
|
68
|
-
minter.mint.
|
64
|
+
it 'mints sequential 4-mixed-digit with constant prefix sdd' do
|
65
|
+
minter = described_class.new(template: 'sdd.sdede')
|
66
|
+
expect(minter.mint).to eq('sdd0000')
|
67
|
+
1000.times { expect(minter.mint).to match(/^sdd\d\w\d\w$/) }
|
68
|
+
expect(minter.mint).to eq('sdd034h')
|
69
69
|
end
|
70
70
|
|
71
|
-
it
|
72
|
-
minter =
|
73
|
-
1000.times { minter.mint.
|
71
|
+
it 'mints random 3 mixed digits plus final (4th) computed check character' do
|
72
|
+
minter = described_class.new(template: '.rdedk')
|
73
|
+
1000.times { expect(minter.mint).to match(/^\d\w\d\w$/) }
|
74
74
|
end
|
75
75
|
|
76
|
-
it
|
77
|
-
minter =
|
78
|
-
minter.mint.
|
79
|
-
minter.mint.
|
80
|
-
minter.mint.
|
81
|
-
1000.times { minter.mint.
|
82
|
-
minter.mint.
|
76
|
+
it 'mints 5 sequential mixed digits plus final extended digit check char' do
|
77
|
+
minter = described_class.new(template: '.sdeeedk')
|
78
|
+
expect(minter.mint).to eq('000000')
|
79
|
+
expect(minter.mint).to eq('000015')
|
80
|
+
expect(minter.mint).to eq('00002b')
|
81
|
+
1000.times { expect(minter.mint).to match(/^\d\w\w\w\d\w$/) }
|
82
|
+
expect(minter.mint).to eq('003f3m')
|
83
83
|
end
|
84
84
|
|
85
|
-
it
|
86
|
-
minter =
|
87
|
-
minter.mint.
|
88
|
-
minter.mint.
|
89
|
-
(10*29*29-2).times { minter.mint.
|
90
|
-
minter.mint.
|
85
|
+
it 'mints sequential digits plus check char, with new digits added as needed' do
|
86
|
+
minter = described_class.new(template: '.zdeek')
|
87
|
+
expect(minter.mint).to eq('0000')
|
88
|
+
expect(minter.mint).to eq('0013')
|
89
|
+
(10 * 29 * 29 - 2).times { expect(minter.mint).to match(/^\d\w\w\w$/) }
|
90
|
+
expect(minter.mint).to eq('10001')
|
91
91
|
end
|
92
92
|
|
93
|
-
it
|
94
|
-
minter =
|
95
|
-
minter.mint.
|
93
|
+
it 'mints prefix plus random 3 mixed digits plus a check char' do
|
94
|
+
minter = described_class.new(template: '63q.redek')
|
95
|
+
expect(minter.mint).to match(/63q\w\d\w\w/)
|
96
96
|
end
|
97
97
|
|
98
|
-
describe
|
99
|
-
it
|
100
|
-
minter =
|
98
|
+
describe 'validate' do
|
99
|
+
it 'validates a prefixed identifier' do
|
100
|
+
minter = described_class.new(template: 'foobar.redek')
|
101
|
+
id = minter.mint
|
102
|
+
expect(minter.valid?(id)).to eq(true)
|
103
|
+
end
|
104
|
+
it 'validates a prefixless identifier' do
|
105
|
+
minter = described_class.new(template: '.redek')
|
101
106
|
id = minter.mint
|
102
|
-
minter.valid?(id).
|
107
|
+
expect(minter.valid?(id)).to eq(true)
|
103
108
|
end
|
104
|
-
it
|
105
|
-
minter =
|
109
|
+
it 'validates with a new minter' do
|
110
|
+
minter = described_class.new(template: '.redek')
|
106
111
|
id = minter.mint
|
107
|
-
|
112
|
+
minter2 = described_class.new(template: '.redek')
|
113
|
+
expect(minter2.valid?(id)).to eq(true)
|
108
114
|
end
|
109
|
-
it
|
110
|
-
minter =
|
115
|
+
it 'validates an unlimited sequence with mixed digits' do
|
116
|
+
minter = described_class.new(template: '.zed')
|
117
|
+
1000.times { minter.mint }
|
111
118
|
id = minter.mint
|
112
|
-
|
113
|
-
minter2.valid?(id).should be_true
|
119
|
+
expect(minter.valid?(id)).to eq(true)
|
114
120
|
end
|
115
121
|
end
|
116
122
|
|
117
|
-
describe
|
118
|
-
it
|
119
|
-
minter =
|
123
|
+
describe 'seed' do
|
124
|
+
it 'given a specific seed, identifiers should be replicable' do
|
125
|
+
minter = described_class.new(template: '63q.redek')
|
120
126
|
minter.seed(1)
|
121
|
-
minter.mint.
|
122
|
-
|
123
|
-
minter =
|
127
|
+
expect(minter.mint).to eq('63q3706')
|
128
|
+
|
129
|
+
minter = described_class.new(template: '63q.redek')
|
124
130
|
minter.seed(1)
|
125
|
-
minter.mint.
|
131
|
+
expect(minter.mint).to eq('63q3706')
|
126
132
|
end
|
127
133
|
|
128
|
-
it
|
129
|
-
minter =
|
130
|
-
minter.seed(
|
134
|
+
it 'given a specific seed and sequence, identifiers should be replicable' do
|
135
|
+
minter = described_class.new(template: '63q.redek')
|
136
|
+
minter.seed(23_456_789, 567)
|
131
137
|
mint1 = minter.mint
|
132
138
|
dump1 = minter.dump
|
133
139
|
|
134
|
-
minter =
|
135
|
-
minter.seed(
|
140
|
+
minter = described_class.new(template: '63q.redek')
|
141
|
+
minter.seed(23_456_789, 567)
|
136
142
|
mint2 = minter.mint
|
137
143
|
dump2 = minter.dump
|
138
144
|
expect(dump1).to eql(dump2)
|
139
145
|
expect(mint1).to eql(mint2)
|
140
|
-
mint1.
|
146
|
+
expect(mint1).to eq('63qb41v') # "63qh305" was the value from a slightly buggy impl
|
141
147
|
end
|
142
148
|
end
|
143
149
|
|
144
|
-
describe
|
145
|
-
it
|
146
|
-
minter =
|
150
|
+
describe 'dump and reload' do
|
151
|
+
it 'dumps the minter state' do
|
152
|
+
minter = described_class.new(template: '.sddd')
|
147
153
|
d = minter.dump
|
148
|
-
d[:template].
|
149
|
-
d[:seq].
|
154
|
+
expect(d[:template]).to eq('.sddd')
|
155
|
+
expect(d[:seq]).to eq(0)
|
150
156
|
|
151
157
|
minter.mint
|
152
158
|
minter.mint
|
@@ -154,51 +160,63 @@ describe Noid::Minter do
|
|
154
160
|
d[:seq] == 2
|
155
161
|
end
|
156
162
|
|
157
|
-
it
|
158
|
-
minter =
|
163
|
+
it 'dumps the seed, sequence, and counters for the RNG' do
|
164
|
+
minter = described_class.new(template: '.rddd')
|
159
165
|
d = minter.dump
|
160
|
-
d[:seq]
|
161
|
-
d[:seed].
|
166
|
+
expect(d[:seq]).to eq 0
|
167
|
+
expect(d[:seed]).to eq(minter.instance_variable_get('@seed'))
|
162
168
|
end
|
163
169
|
|
164
|
-
it "
|
165
|
-
minter =
|
170
|
+
it "allows a random identifier minter to be 'replayed' accurately" do
|
171
|
+
minter = described_class.new(template: '.rd')
|
166
172
|
d = minter.dump
|
167
173
|
arr = 10.times.map { minter.mint }
|
168
174
|
|
169
|
-
minter =
|
175
|
+
minter = described_class.new(d)
|
170
176
|
|
171
177
|
arr2 = 10.times.map { minter.mint }
|
172
178
|
|
173
|
-
arr.
|
174
|
-
|
179
|
+
expect(arr).to eq(arr2)
|
175
180
|
end
|
176
|
-
|
177
181
|
end
|
178
182
|
|
179
|
-
describe
|
180
|
-
it
|
181
|
-
minter =
|
182
|
-
minter.seed(
|
183
|
-
first_values = (1..1000).collect {|
|
183
|
+
describe 'with large seeds' do
|
184
|
+
it 'does not reproduce noids with constructed sequences' do
|
185
|
+
minter = described_class.new(template: 'ldpd:.reeeeeeee')
|
186
|
+
minter.seed(192_548_637_498_850_379_850_405_658_298_152_906_991)
|
187
|
+
first_values = (1..1000).collect { |_c| minter.mint }
|
184
188
|
|
185
189
|
values = []
|
186
190
|
(0..999).each do |i|
|
187
|
-
minter =
|
188
|
-
minter.seed(
|
191
|
+
minter = described_class.new(template: 'ldpd:.reeeeeeee')
|
192
|
+
minter.seed(192_548_637_498_850_379_850_405_658_298_152_906_991, i)
|
189
193
|
values << minter.mint
|
190
|
-
expect(values[i]).to eql first_values[i]
|
194
|
+
expect(values[i]).to eql first_values[i]
|
191
195
|
end
|
192
196
|
values.uniq!
|
193
197
|
expect(values.length).to eql 1000
|
194
198
|
end
|
195
199
|
end
|
196
200
|
|
197
|
-
describe
|
201
|
+
describe 'multithreading-safe example' do
|
202
|
+
def stateful_minter
|
203
|
+
File.open('minter-state', File::RDWR | File::CREAT, 0644) do |f|
|
204
|
+
f.flock(File::LOCK_EX)
|
205
|
+
yaml = YAML.load(f.read)
|
206
|
+
minter = described_class.new(yaml)
|
207
|
+
yield minter
|
208
|
+
f.rewind
|
209
|
+
yaml = YAML.dump(minter.dump)
|
210
|
+
f.write yaml
|
211
|
+
f.flush
|
212
|
+
f.truncate(f.pos)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
198
216
|
before do
|
199
217
|
require 'yaml'
|
200
|
-
minter =
|
201
|
-
yaml = YAML
|
218
|
+
minter = described_class.new(template: '.reek')
|
219
|
+
yaml = YAML.dump(minter.dump)
|
202
220
|
File.open('minter-state', 'w') { |f| f.write yaml }
|
203
221
|
end
|
204
222
|
|
@@ -206,23 +224,34 @@ describe Noid::Minter do
|
|
206
224
|
File.delete('minter-state')
|
207
225
|
end
|
208
226
|
|
209
|
-
it
|
227
|
+
it 'hops buckets between runs' do
|
228
|
+
bucket_list = []
|
229
|
+
10.times do
|
230
|
+
stateful_minter do |minter|
|
231
|
+
bucket = minter.random_bucket
|
232
|
+
bucket_list << bucket
|
233
|
+
allow(minter).to receive(:random_bucket) { bucket }
|
234
|
+
minter.mint
|
235
|
+
end
|
236
|
+
end
|
237
|
+
expect(bucket_list.uniq.count).to be > 1
|
238
|
+
end
|
210
239
|
|
211
|
-
|
240
|
+
it 'persists state to the filesystem' do
|
241
|
+
# TODO: This is not testing any expectations. Clarify intent and fix.
|
242
|
+
skip
|
243
|
+
File.open('minter-state', File::RDWR | File::CREAT, 0644) do|f|
|
212
244
|
f.flock(File::LOCK_EX)
|
213
|
-
yaml = YAML
|
214
|
-
|
215
|
-
minter =
|
245
|
+
yaml = YAML.load(f.read)
|
246
|
+
|
247
|
+
minter = described_class.new(yaml)
|
216
248
|
|
217
249
|
f.rewind
|
218
|
-
yaml = YAML
|
250
|
+
yaml = YAML.dump(minter.dump)
|
219
251
|
f.write yaml
|
220
252
|
f.flush
|
221
253
|
f.truncate(f.pos)
|
222
|
-
|
223
|
-
|
254
|
+
end
|
224
255
|
end
|
225
256
|
end
|
226
|
-
|
227
|
-
|
228
257
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Noid::Template do
|
4
|
+
context 'with a valid template' do
|
5
|
+
let(:template) { '.redek' }
|
6
|
+
it 'initializes without raising' do
|
7
|
+
expect { described_class.new(template) }.not_to raise_error
|
8
|
+
end
|
9
|
+
end
|
10
|
+
context 'with a bogus template' do
|
11
|
+
let(:template) { 'foobar' }
|
12
|
+
it 'raises Noid::TemplateError' do
|
13
|
+
expect { described_class.new(template) }.to raise_error(Noid::TemplateError)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
|
4
|
-
if ENV['COVERAGE']
|
4
|
+
if ENV['COVERAGE'] && RUBY_VERSION =~ /^1.9/
|
5
5
|
require 'simplecov'
|
6
6
|
|
7
7
|
SimpleCov.start
|
@@ -9,6 +9,5 @@ end
|
|
9
9
|
|
10
10
|
require 'noid'
|
11
11
|
|
12
|
-
RSpec.configure do |
|
13
|
-
|
12
|
+
RSpec.configure do |_config|
|
14
13
|
end
|
metadata
CHANGED
@@ -1,57 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: noid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Beer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '3.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '3.0'
|
55
55
|
description: Nice Opaque Identifier
|
56
56
|
email:
|
57
57
|
- chris@cbeer.info
|
@@ -59,8 +59,9 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
-
- .gitignore
|
63
|
-
- .
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
64
65
|
- Gemfile
|
65
66
|
- LICENSE.txt
|
66
67
|
- README.md
|
@@ -72,6 +73,7 @@ files:
|
|
72
73
|
- lib/noid/version.rb
|
73
74
|
- noid.gemspec
|
74
75
|
- spec/lib/minter_spec.rb
|
76
|
+
- spec/lib/template_spec.rb
|
75
77
|
- spec/spec_helper.rb
|
76
78
|
homepage: http://github.com/microservices/noid
|
77
79
|
licenses:
|
@@ -83,20 +85,18 @@ require_paths:
|
|
83
85
|
- lib
|
84
86
|
required_ruby_version: !ruby/object:Gem::Requirement
|
85
87
|
requirements:
|
86
|
-
- -
|
88
|
+
- - ">="
|
87
89
|
- !ruby/object:Gem::Version
|
88
90
|
version: 1.9.3
|
89
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
92
|
requirements:
|
91
|
-
- -
|
93
|
+
- - ">="
|
92
94
|
- !ruby/object:Gem::Version
|
93
95
|
version: '0'
|
94
96
|
requirements: []
|
95
97
|
rubyforge_project: noid
|
96
|
-
rubygems_version: 2.
|
98
|
+
rubygems_version: 2.4.6
|
97
99
|
signing_key:
|
98
100
|
specification_version: 4
|
99
101
|
summary: Nice Opaque Identifier
|
100
|
-
test_files:
|
101
|
-
- spec/lib/minter_spec.rb
|
102
|
-
- spec/spec_helper.rb
|
102
|
+
test_files: []
|