Skip to content

Commit 1b2c9ef

Browse files
committed
Delegate template parsing to Template.
Fixes for template validation (see the added spec, which failed before): with template '.zed' the noid '111' should be valid, yet the code did not validate it. Takes the work from #3 (thanks to @dbrower) and rebases it the latest commit with changes to match current styles.
1 parent 1866f53 commit 1b2c9ef

File tree

6 files changed

+116
-85
lines changed

6 files changed

+116
-85
lines changed

Gemfile

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ source 'https://rubygems.org'
33
# Specify your gem's dependencies in test.gemspec
44
gemspec
55

6-
gem 'simplecov'
7-
gem 'simplecov-rcov'
6+
group :development, :test do
7+
gem 'simplecov'
8+
gem 'simplecov-rcov'
9+
gem 'byebug', require: false
10+
end

lib/noid.rb

+3
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
module Noid
66
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)
77
MAX_COUNTERS = 293
8+
9+
class TemplateError < StandardError
10+
end
811
end

lib/noid/minter.rb

+1-14
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,7 @@ def mint
3434
# @param [String] id
3535
# @return bool
3636
def valid?(id)
37-
prefix = @template.prefix.empty? ? '' : id[0..@template.prefix.length - 1]
38-
ch = @template.prefix.empty? ? id.split('') : id[@template.prefix.length..-1].split('')
39-
check = ch.pop if @template.checkdigit?
40-
return false unless prefix == @template.prefix
41-
42-
return false unless @template.characters.length == ch.length
43-
@template.characters.split('').each_with_index do |c, i|
44-
return false unless Noid::XDIGIT.include? ch[i]
45-
return false if c == 'd' && ch[i] =~ /[^\d]/
46-
end
47-
48-
return false unless check.nil? || check == @template.checkdigit(id[0..-2])
49-
50-
true
37+
template.valid?(id)
5138
end
5239

5340
##

lib/noid/template.rb

+85-69
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
module Noid
22
class Template
3-
attr_reader :template
3+
attr_reader :template, :prefix, :generator, :characters
4+
5+
VALID_PATTERN = /\A(.*)\.([rsz])([ed]+)(k?)\Z/
46

57
# @param [String] template A Template is a coded string of the form Prefix.Mask that governs how identifiers will be minted.
68
def initialize(template)
79
@template = template
10+
parse!
811
end
912

1013
def mint(n)
@@ -15,93 +18,106 @@ def mint(n)
1518
str
1619
end
1720

21+
##
22+
# Is the string valid against this template string and checksum?
23+
# @param [String] str
24+
# @return bool
1825
def valid?(str)
19-
return false unless str[0..prefix.length] == prefix
20-
21-
if generator == 'z'
22-
str[prefix.length..-1].length > 2
23-
else
24-
str[prefix.length..-1].length == characters.length
25-
end
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-
26+
match = validation_regex.match(str)
27+
return false if match.nil?
28+
return checkdigit(match[1]) == match[3] if checkdigit?
3829
true
3930
end
4031

4132
##
42-
# identifier prefix string
43-
def prefix
44-
@prefix ||= @template.split('.').first
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]
4538
end
4639

4740
##
48-
# identifier mask string
49-
def mask
50-
@mask ||= @template.split('.').last
41+
# minimum sequence value
42+
def min
43+
@min ||= 0
5144
end
5245

5346
##
54-
# generator type to use: r, s, z
55-
def generator
56-
@generator ||= mask[0..0]
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
5754
end
5855

56+
protected
57+
5958
##
60-
# sequence pattern: e (extended), d (digit)
61-
def characters
62-
@characters ||= begin
63-
if checkdigit?
64-
mask[1..-2]
65-
else
66-
mask[1..-1]
67-
end
68-
end
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
6975
end
7076

7177
##
72-
# should generated identifiers have a checkdigit?
73-
def checkdigit?
74-
mask.split('').last == 'k'
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')
7587
end
7688

77-
##
78-
# calculate a checkdigit for the str
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('') + ']'
8391
end
8492

85-
##
86-
# minimum sequence value
87-
def min
88-
@min ||= 0
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
89102
end
90103

91104
##
92-
# maximum sequence value for the template
93-
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
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) }
102108
end
103109

104-
protected
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
105121

106122
##
107123
# total size of a given template character value
@@ -120,17 +136,17 @@ def character_space(c)
120136
# @param [Integer] n
121137
# @return [String]
122138
def n2xdig(n)
123-
xdig = characters.reverse.split('').map do |c|
124-
value = n % character_space(c)
125-
n /= character_space(c)
139+
xdig = size_list.reverse.map { |size|
140+
value = n % size
141+
n /= size
126142
Noid::XDIGIT[value]
127-
end.compact.join('')
143+
}.compact.join('')
128144

129145
if generator == 'z'
130-
c = characters.split('').last
146+
size = size_list.last
131147
while n > 0
132-
value = n % character_space(c)
133-
n /= character_space(c)
148+
value = n % size
149+
n /= size
134150
xdig += Noid::XDIGIT[value]
135151
end
136152
end

spec/lib/minter_spec.rb

+6
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@
112112
minter2 = described_class.new(template: '.redek')
113113
expect(minter2.valid?(id)).to eq(true)
114114
end
115+
it 'validates an unlimited sequence with mixed digits' do
116+
minter = described_class.new(template: '.zed')
117+
1000.times { minter.mint }
118+
id = minter.mint
119+
expect(minter.valid?(id)).to eq(true)
120+
end
115121
end
116122

117123
describe 'seed' do

spec/lib/template_spec.rb

+16
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)