Skip to content

Commit 498d7d9

Browse files
committed
Improve validator to handle infinite generators with mixed digits
Addresses HYDRA-899.
1 parent 9d00569 commit 498d7d9

File tree

3 files changed

+89
-57
lines changed

3 files changed

+89
-57
lines changed

lib/noid/minter.rb

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,7 @@ def template
3838
# @param [String] id
3939
# @return bool
4040
def valid? id
41-
prefix = @template.prefix.empty? ? '' : id[0..@template.prefix.length-1]
42-
ch = @template.prefix.empty? ? id.split('') : id[@template.prefix.length..-1].split('')
43-
check = ch.pop if @template.checkdigit?
44-
return false unless prefix == @template.prefix
45-
46-
return false unless @template.characters.length == ch.length
47-
@template.characters.split('').each_with_index do |c, i|
48-
return false unless Noid::XDIGIT.include? ch[i]
49-
return false if c == 'd' and ch[i] =~ /[^\d]/
50-
end
51-
52-
return false unless check.nil? or check == @template.checkdigit(id[0..-2])
53-
54-
true
41+
template.valid?(id)
5542
end
5643

5744
##

lib/noid/template.rb

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ class Template
44

55
# @param [String] template A Template is a coded string of the form Prefix.Mask that governs how identifiers will be minted.
66
def initialize template
7-
@template = template
7+
@template = template
8+
parse_template
89
end
910

1011
def mint n
@@ -15,63 +16,69 @@ def mint n
1516
str
1617
end
1718

18-
def valid? str
19-
return false unless str[0..prefix.length] == prefix
20-
19+
# A noid has the structure (prefix)(code)(checkdigit)
20+
# the regexp has the following named captures
21+
# :body - the prefix and the code
22+
# :code - the changing id characters (not the prefix and not the checkdigit)
23+
# :check - the checkdigit, if there is one. This field is missing if there is no checkdigit
24+
def validation_regexp
25+
return @validation_regexp if @validation_regexp
26+
pattern_list = ['\A', '(?<body>', Regexp.escape(prefix), '(?<code>']
2127
if generator == 'z'
22-
str[prefix.length..-1].length > 2
23-
else
24-
str[prefix.length..-1].length == characters.length
28+
pattern_list << character_to_pattern(@character_list.last) << '*'
2529
end
30+
@character_list.each do |c|
31+
pattern_list << character_to_pattern(c)
32+
end
33+
pattern_list << ')' << ')' # close <code> and <body>
34+
if checkdigit?
35+
pattern_list << '(?<check>' << character_to_pattern('e') << ')'
36+
end
37+
pattern_list << '\Z'
2638

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?
39+
@validation_regexp = Regexp.new(pattern_list.join(''))
40+
end
3741

42+
##
43+
# Is the passed in string valid against this template?
44+
# Also validates the check digit, if the template has one
45+
def valid?(str)
46+
match = validation_regexp.match(str)
47+
return false if match.nil?
48+
if checkdigit?
49+
return checkdigit(match[:body]) == match[:check]
50+
end
3851
true
3952
end
4053

4154
##
4255
# identifier prefix string
4356
def prefix
44-
@prefix ||= @template.split('.').first
57+
@prefix
4558
end
4659

4760
##
4861
# identifier mask string
4962
def mask
50-
@mask ||= @template.split('.').last
63+
@mask
5164
end
5265

5366
##
5467
# generator type to use: r, s, z
5568
def generator
56-
@generator ||= mask[0..0]
69+
@generator
5770
end
5871

5972
##
6073
# sequence pattern: e (extended), d (digit)
6174
def characters
62-
@characters ||= begin
63-
if checkdigit?
64-
mask[1..-2]
65-
else
66-
mask[1..-1]
67-
end
68-
end
75+
@characters
6976
end
7077

7178
##
7279
# should generated identifiers have a checkdigit?
7380
def checkdigit?
74-
mask.split('').last == 'k'
81+
@checkdigit
7582
end
7683

7784
##
@@ -91,18 +98,50 @@ def min
9198
##
9299
# maximum sequence value for the template
93100
def max
94-
@max ||= begin
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
101+
@max ||= case generator
102+
when 'z' then nil
103+
else size_list.inject(1) { |total, x| total * x }
104+
end
102105
end
103106

104107

105108
protected
109+
##
110+
# parse @template and put the results into class variables
111+
# raise an exception if there is a parse error
112+
#
113+
def parse_template
114+
match = /\A(?<prefix>.*)\.(?<generator>[rsz])(?<mask>[ed]+)(?<check>k?)\Z/.match(@template)
115+
if match.nil?
116+
raise "Malformed Noid template '#{@template}'"
117+
end
118+
@prefix = match[:prefix]
119+
@generator = match[:generator]
120+
@characters = match[:mask]
121+
@character_list = @characters.split('')
122+
@mask = @generator + @characters
123+
@checkdigit = (match[:check] == 'k')
124+
end
125+
126+
def xdigit_pattern
127+
@xdigit_pattern ||= "[" + Noid::XDIGIT.join('') + "]"
128+
end
129+
130+
def character_to_pattern(c)
131+
case c
132+
when 'e' then xdigit_pattern
133+
when 'd' then '\d'
134+
else ''
135+
end
136+
end
137+
138+
##
139+
# Return a list giving the number of possible characters at each position
140+
#
141+
def size_list
142+
@size_list ||= @character_list.map { |c| character_space(c) }
143+
end
144+
106145
##
107146
# total size of a given template character value
108147
# @param [String] c
@@ -119,18 +158,18 @@ def character_space c
119158
# convert a minter position to a noid string under this template
120159
# @param [Integer] n
121160
# @return [String]
122-
def n2xdig n
123-
xdig = characters.reverse.split('').map do |c|
124-
value = n % character_space(c)
125-
n = n / character_space(c)
161+
def n2xdig(n)
162+
xdig = size_list.reverse.map do |size|
163+
value = n % size
164+
n = n / size
126165
Noid::XDIGIT[value]
127166
end.compact.join('')
128167

129168
if generator == 'z'
130-
c = characters.split('').last
169+
size = size_list.last
131170
while n > 0
132-
value = n % character_space(c)
133-
n = n / character_space(c)
171+
value = n % size
172+
n = n / size
134173
xdig += Noid::XDIGIT[value]
135174
end
136175
end

spec/lib/minter_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@
106106
id = minter.mint
107107
minter.valid?(id).should be_true
108108
end
109+
it "should validate unlimited sequence with mixed digits" do
110+
minter = Noid::Minter.new(:template => ".zed")
111+
1000.times { minter.mint }
112+
id = minter.mint
113+
minter.valid?(id).should be_true
114+
end
109115
end
110116

111117
describe "seed" do

0 commit comments

Comments
 (0)