File Coverage

lib/Data/Identifier/Generate.pm
Criterion Covered Total %
statement 112 251 44.6
branch 53 160 33.1
condition 45 152 29.6
subroutine 15 21 71.4
pod 9 9 100.0
total 234 593 39.4


line stmt bran cond sub pod time code
1             # Copyright (c) 2023-2026 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: format independent identifier object
6              
7              
8             package Data::Identifier::Generate;
9              
10 3     3   221636 use v5.20;
  3         7  
11 3     3   10 use strict;
  3         4  
  3         61  
12 3     3   32 use warnings;
  3         3  
  3         125  
13              
14 3     3   12 use Carp;
  3         25  
  3         177  
15 3     3   11 use Encode qw(encode);
  3         4  
  3         216  
16 3     3   1429 use Digest;
  3         1789  
  3         97  
17              
18 3     3   466 use Data::Identifier;
  3         8  
  3         17  
19              
20             use constant {
21 3         9447 NS_GTE => '90355e77-6942-4d82-a0b6-3a6de65bf948',
22              
23             WK_UNSIGNED_INTEGER_GENERATOR => '53863a15-68d4-448d-bd69-a9b19289a191',
24             WK_SIGNED_INTEGER_GENERATOR => 'e8aa9e01-8d37-4b4b-8899-42ca0a2a906f',
25             WK_UNICODE_CHARACTER_GENERATOR => 'd74f8c35-bcb8-465c-9a77-01010e8ed25c',
26             WK_RGB_COLOUR_GENERATOR => '55febcc4-6655-4397-ae3d-2353b5856b34',
27             WK_DATE_GENERATOR => '97b7f241-e1c5-4f02-ae3c-8e31e501e1dc',
28             #WK_LANGUAGE_GENERATOR => '',
29             WK_MULTIPLICITY_GENERATOR => '19659233-0a22-412c-bdf1-8ee9f8fc4086',
30             WK_MINIMUM_MULTIPLICITY_GENERATOR => '5ec197c3-1406-467c-96c7-4b1a6ec2c5c9',
31 3     3   16 };
  3         3  
32              
33              
34             our $VERSION = v0.29;
35              
36             my %_multiplicity_prefix = (
37             total => '4.1',
38             minimum => '4.3',
39             );
40              
41             my %_multiplicity_names = (
42             0 => 'noone',
43             1 => 'solo',
44             2 => 'duo',
45             3 => 'trio',
46             );
47              
48             my %_multiplicity_generators = (
49             total => WK_MULTIPLICITY_GENERATOR,
50             minimum => WK_MINIMUM_MULTIPLICITY_GENERATOR,
51             );
52              
53             my %_gte_simple_profiles = (
54             'ab332382-a6f8-4a24-914d-5f823dd866c1' => {
55             namespace => 'a13377a3-88d4-484e-90a8-245afb22a793',
56             order => 'MFHCSmfhcs',
57             case_folding => undef,
58             strip_slash => undef,
59             strip_spaces => undef,
60             },
61             'e537db94-85b9-4125-972a-cc2ea1fdf51d' => {
62             namespace => '52f20647-1dc7-4234-81ad-0639ab6cef60',
63             order => 'FAfa',
64             case_folding => undef,
65             strip_slash => undef,
66             strip_spaces => undef,
67             },
68             '60764502-18cb-41c9-8531-e4c8b43140b9' => {
69             namespace => '1e2edf8d-d459-47cb-9d6e-0690f404fadf',
70             order => 'MFHCSmfhcs',
71             case_folding => undef,
72             strip_slash => undef,
73             strip_spaces => undef,
74             },
75             '5aad9d75-7020-41a4-8ec6-2bf09566f985' => {
76             namespace => 'fc9741b4-2ac7-412b-9d36-2b675fc0482b',
77             order => 'NDTBndtb',
78             case_folding => undef,
79             strip_slash => undef,
80             strip_spaces => undef,
81             },
82             );
83              
84              
85             #@returns Data::Identifier
86             sub integer {
87 48     48 1 2377 my ($pkg, $request, %opts) = @_;
88 48         70 $opts{request} = $request;
89 48         68 $opts{style} = 'integer-based';
90 48         149 $opts{namespace} = Data::Identifier->NS_INT();
91 48   33     149 $opts{displayname}//= $request;
92 48 100       96 $opts{generator} = $request >= 0 ? WK_UNSIGNED_INTEGER_GENERATOR : WK_SIGNED_INTEGER_GENERATOR;
93              
94 48         159 return $pkg->generic(%opts);
95             }
96              
97              
98             sub unicode_character {
99 3     3 1 1753 my ($pkg, $type, $request, %opts) = @_;
100 3         6 my $unicode_cp;
101             my $unicode_cp_str;
102              
103 3 50       6 croak 'No type given' unless defined $type;
104 3 50 33     10 croak 'No/Bad request given' unless defined($request) && length($request);
105              
106 3 50       10 if ($type eq 'unicode') {
    50          
    50          
107 0 0       0 if ($request =~ /^[Uu]\+([0-9a-fA-F]+)$/) {
    0          
108 0         0 $unicode_cp = hex($1);
109             } elsif ($request =~ /^[0-9]+\z/) {
110 0         0 $unicode_cp = int($request);
111             } else {
112 0         0 croak 'Bad request given: '.$request;
113             }
114             } elsif ($type eq 'ascii') {
115 0 0       0 if ($request =~ /^[0-9]+\z/) {
116 0         0 $unicode_cp = int($request);
117             } else {
118 0         0 croak 'Bad request given: '.$request;
119             }
120 0 0 0     0 croak 'US-ASCII character out of range: '.$unicode_cp if $unicode_cp < 0 || $unicode_cp > 0x7F;
121             } elsif ($type eq 'raw') {
122 3 50       5 croak 'Raw value is not exactly one character long' unless length($request) == 1;
123 3         4 $unicode_cp = ord($request);
124             } else {
125 0         0 croak 'Bad type given: '.$type;
126             }
127              
128 3 50 33     7 croak 'Unicode character out of range: '.$unicode_cp if $unicode_cp < 0 || $unicode_cp > 0x10FFFF;
129              
130 3         10 $unicode_cp_str = sprintf('U+%04X', $unicode_cp);
131              
132 3 50 33     18 if ($unicode_cp == 0xFFFC || $unicode_cp == 0xFFFD || $unicode_cp == 0xFEFF || $unicode_cp == 0xFFFE) {
      33        
      33        
133 0 0       0 croak 'Rejected use of special character: '.$unicode_cp_str unless $opts{allow_special};
134             }
135              
136 3   33     9 $opts{displayname} //= $unicode_cp_str;
137              
138 3         9 return Data::Identifier->new(unicodecp => $unicode_cp_str, displayname => $opts{displayname}, generator => WK_UNICODE_CHARACTER_GENERATOR, request => $unicode_cp_str);
139             }
140              
141              
142             #@returns Data::Identifier
143             sub colour {
144 5     5 1 3230 my ($pkg, $colour, %opts) = @_;
145 5         9 $opts{request} = $colour;
146 5         6 $opts{style} = 'colour';
147 5         7 $opts{namespace} = '88d3944f-a13b-4e35-89eb-e3c1fbe53e76';
148 5         5 $opts{generator} = WK_RGB_COLOUR_GENERATOR;
149 5         13 return $pkg->generic(%opts);
150             }
151              
152              
153             #@returns Data::Identifier
154             sub date {
155 9     9 1 5225 my ($pkg, $request, %opts) = @_;
156 9         14 my ($year, $month, $day);
157 9         0 my $precision;
158              
159 9 50       18 if (ref($request)) {
160 0 0       0 if (eval {$request->can('epoch')}) {
  0         0  
161 0         0 $request = $request->epoch;
162             } else {
163 0         0 return $pkg->date(scalar($request->()), %opts);
164             }
165             }
166              
167 9         44 ($year, $month, $day) = $request =~ /^([12][0-9]{3})(?:-([01][0-9])(?:-([0-3][0-9]))?)?Z$/;
168              
169 9 100 100     34 unless (length($year // '') == 4) {
170 3 50 33     20 if ($request eq 'now' || $request eq 'today') {
    50          
171 0         0 $request = time();
172             } elsif ($request =~ /^(?:0|-?[1-9][0-9]*)$/) {
173 3         6 $request = int($request);
174 3 50       7 if ($request > 32503680000) {
175 0         0 croak 'Unlikely far date given. Likely miliseconds are passed as seconds?';
176             }
177             } else {
178 0         0 croak 'Invalid format';
179             }
180              
181 3         12 (undef,undef,undef,$day,$month,$year) = gmtime($request);
182 3         4 $year += 1900;
183 3         3 $month += 1;
184             }
185              
186 9         10 foreach my $entry ($year, $month, $day) {
187 27   100     42 $entry = int($entry // 0);
188             }
189              
190 9 50 33     31 croak 'Invalid year' if $year && ($year < 1583 || $year > 9999);
      33        
191 9 50 33     26 croak 'Invalid month' if $month && ($month < 1 || $month > 12);
      66        
192 9 50 33     23 croak 'Invalid day' if $day && ($day < 1 || $day > 31);
      66        
193              
194 9 50       12 $month = 0 unless $year;
195 9 100       10 $day = 0 unless $month;
196              
197 9 100 66     31 $precision = $opts{precision} // ($day ? 'day' : undef) // ($month ? 'month' : undef) // 'year';
    100 100        
      100        
198 9 100 66     51 if ($precision eq 'day' && $day) {
    100 66        
    50 33        
199 7         25 $request = sprintf('%04u-%02u-%02uZ', $year, $month, $day);
200             } elsif ($precision eq 'month' && $month) {
201 1         4 $request = sprintf('%04u-%02uZ', $year, $month);
202             } elsif ($precision eq 'year' && $year) {
203 1         4 $request = sprintf('%04uZ', $year);
204             } else {
205 0         0 croak 'Bad precision: '.$precision;
206             }
207              
208 9         31 $opts{request} = $request;
209 9   33     26 $opts{input} //= $request; # force raw value!
210 9         9 $opts{style} = undef;
211 9         28 $opts{namespace} = Data::Identifier->NS_DATE();
212 9   33     19 $opts{displayname}//= $request;
213 9         11 $opts{generator} = WK_DATE_GENERATOR;
214              
215 9         30 return $pkg->generic(%opts);
216             }
217              
218              
219             sub language {
220 0     0 1 0 my ($pkg, $req, %opts) = @_;
221 0         0 my $name;
222              
223 0         0 require I18N::LangTags::List;
224              
225 0         0 $opts{request} = $req;
226 0         0 $opts{style} = 'id-based';
227 0         0 $opts{namespace} = '47dd950c-9089-4956-87c1-54c122533219';
228             #$opts{generator} = WK_LANGUAGE_GENERATOR;
229              
230 0 0       0 croak 'Bad language: '.$req unless I18N::LangTags::List::is_decent($req);
231              
232 0         0 $name = I18N::LangTags::List::name($req);
233              
234 0 0 0     0 unless (defined($name) && length($name)) {
235 0         0 croak 'Bad language: '.$req;
236             }
237              
238 0   0     0 $opts{displayname} //= $name;
239 0         0 return $pkg->generic(%opts);
240             }
241              
242              
243             sub multiplicity {
244 0     0 1 0 my ($pkg, $subtype, $request, %opts) = @_;
245 0   0     0 my $prefix = $_multiplicity_prefix{$subtype} // croak 'Invalid subtype: '.$subtype;
246 0         0 my $identifier;
247             my $oid;
248              
249 0 0 0     0 croak 'Invalid value: '.$request unless $request eq '0' || $request =~ /^[1-9][0-9]*$/;
250              
251 0         0 $oid = '1.3.6.1.4.1.46942.16.2.'.$prefix.'.'.$request;
252              
253 0         0 $opts{request} = $request;
254 0         0 $opts{input} = $prefix.'.'.$request;
255 0         0 $opts{namespace} = NS_GTE;
256 0   0     0 $opts{displayname}//= $_multiplicity_names{$request};
257 0   0     0 $opts{displayname}//= $request;
258 0         0 $opts{generator} = $_multiplicity_generators{$subtype};
259 0         0 $identifier = $pkg->generic(%opts);
260              
261 0   0     0 $identifier->{id_cache} //= {};
262 0   0     0 $identifier->{id_cache}->{Data::Identifier->WK_OID} //= $oid;
263              
264 0 0       0 if (defined $_multiplicity_names{$request}) {
265 0         0 $identifier->register;
266             }
267              
268 0         0 return $identifier;
269             }
270              
271              
272             sub gte_simple {
273 0     0 1 0 my ($pkg, $profile, $request, %opts) = @_;
274 0         0 my %order;
275             my $normal;
276              
277 0 0       0 croak 'Called in list context' if wantarray;
278              
279 0 0       0 $profile = $profile->ise if eval {$profile->can('ise')};
  0         0  
280 0   0     0 $profile = $_gte_simple_profiles{$profile} // $profile;
281              
282             {
283 0         0 my $i = 0;
  0         0  
284 0         0 %order = map {$_ => $i++} split(//, $profile->{order});
  0         0  
285             }
286              
287 0 0       0 if (defined(my $folding = $profile->{case_folding})) {
288 0 0       0 if ($folding eq 'none') {
    0          
    0          
289             # no-op
290             } elsif ($folding eq 'upper') {
291 0         0 $request = uc($request);
292             } elsif ($folding eq 'lower') {
293 0         0 $request = lc($request);
294             } else {
295 0         0 croak 'Unsupported/invalid folding rule: '.$folding;
296             }
297             }
298              
299 0 0       0 if ($profile->{strip_slash}) {
300 0         0 $request =~ s#/+##g;
301             }
302              
303 0 0       0 if ($profile->{strip_spaces}) {
304 0         0 $request =~ s#\s+##g;
305             }
306              
307             $normal = join('',
308 0         0 sort {$order{$a} <=> $order{$b}}
309 0 0       0 map {croak 'Invalid input element: '.$_ unless defined $order{$_}; $_}
  0         0  
  0         0  
310             split //, $request);
311              
312 0 0       0 if (defined(my $info = delete $opts{info})) {
313 0         0 $info->{count} = length($normal);
314 0         0 $info->{request} = $normal;
315             }
316              
317 0         0 $opts{input} = $normal;
318 0         0 $opts{request} = $normal;
319 0   0     0 $opts{namespace} //= $profile->{namespace};
320 0   0     0 $opts{displayname}//= $normal;
321 0         0 return $pkg->generic(%opts);
322             }
323              
324              
325             sub unit {
326 0     0 1 0 my ($pkg, $request, %opts) = @_;
327              
328 0         0 require Data::Identifier::Util;
329              
330 0         0 return Data::Identifier::Util->render_unit_request('Data::Identifier' => $request, %opts);
331             }
332              
333              
334             #@returns Data::Identifier
335             sub generic {
336 76     76 1 289 my ($pkg, %opts) = @_;
337              
338 76 50       148 if (defined(my $type)) {
339 0   0     0 $opts{namespace} //= $type->namespace;
340             }
341              
342 76 100 66     218 if (defined(my $style = $opts{style}) && defined(my $request = $opts{request})) {
343 67 100       134 if ($style eq 'integer-based') {
    50          
    100          
    50          
    50          
    50          
344 48 50       226 croak 'Invalid request' unless $request =~ /^(?:0|-?[1-9][0-9]*)$/;
345 48   66     157 $opts{input} //= $request;
346             } elsif ($style eq 'id-based') {
347 0         0 my ($input, $name);
348              
349 0 0       0 if (($input, $name) = $request =~ /^(#?[a-zA-Z0-9\-\.\+]+) (.+)$/) {
    0          
350             # noop
351             } elsif (($input) = $request =~ /^(#?[a-zA-Z0-9\-\.\+]+)$/) {
352 0         0 $name = undef;
353             } else {
354 0         0 croak 'Invalid format, expected: "id name", or "id", got: '.$request;
355             }
356              
357 0   0     0 $opts{input} //= lc($input);
358 0   0     0 $opts{displayname} //= $name;
359              
360             } elsif ($style eq 'name-based') {
361 14   33     101 $opts{input} //= encode('UTF-8', $request);
362 14   33     433 $opts{displayname} //= $request;
363             } elsif ($style eq 'tag-based') {
364 0 0       0 if (ref($request)) {
    0          
365 0         0 my $identifier = Data::Identifier->new(from => $request);
366 0   0     0 $opts{input} //= $identifier->uuid;
367 0   0     0 $opts{displayname} //= $identifier->{displayname}; # steal the raw value here.
368             } elsif ($request =~ Data::Identifier->RE_UUID) {
369 0   0     0 $opts{input} //= $request;
370             } else {
371 0         0 croak 'Invalid format for tag-based generator: '.$request;
372             }
373             } elsif ($style eq 'tagcombiner') {
374 0 0       0 if (ref($request) eq 'ARRAY') {
375 0         0 my %uuids;
376              
377 0         0 foreach my $entry (@{$request}) {
  0         0  
378 0 0       0 if (ref($entry)) {
    0          
379 0         0 $uuids{Data::Identifier->new(from => $entry)->uuid} = undef;
380             } elsif ($entry =~ Data::Identifier->RE_UUID) {
381 0         0 $uuids{$entry} = undef;
382             } else {
383 0         0 croak 'Invalid format for tag-based generator: '.$entry;
384             }
385             }
386              
387 0 0       0 croak 'Less than two tags being combined' unless scalar(keys %uuids) > 1;
388              
389 0   0     0 $opts{input} //= join(',', sort keys %uuids);
390             } else {
391 0         0 croak 'Invalid request for tagcombiner generator: '.$request;
392             }
393             } elsif ($style eq 'colour') {
394 5 50       8 if (defined $request) {
395 5         7 my $req = lc($request);
396              
397 5 50       37 $req = sprintf('#%s%s%s', $1 x 6, $2 x 6, $3 x 6) if $req =~ /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/;
398              
399 5 50       13 if ($req =~ /^#[a-f0-9]{36}$/) {
400 5   33     15 $opts{input} //= $req;
401             } else {
402 0         0 croak 'Bad format for colour';
403             }
404             }
405             } else {
406 0         0 croak 'Style not supported: '.$style;
407             }
408             }
409              
410 76 50       118 croak 'No valid style/request or input is provided' unless defined $opts{input};
411              
412             {
413 76 100       71 my $ns = ref($opts{namespace}) ? $opts{namespace}->uuid(no_defaults => 1) : $opts{namespace};
  76         139  
414 76         168 my $uuid = $pkg->_uuid_v5($ns, $opts{input});
415 76         257 my $tag = Data::Identifier->new(uuid => $uuid, displayname => $opts{displayname});
416 76   66     360 $tag->{$_} //= $opts{$_} foreach qw(generator request);
417 76 100 66     277 $tag->{generator} = Data::Identifier->new(from => $tag->{generator}) if defined($tag->{generator}) && !ref($tag->{generator});
418 76         354 return $tag;
419             }
420             }
421              
422             # ---- Private helpers ----
423              
424             sub _available_module {
425 0     0   0 my (@modules) = @_;
426 0         0 state $tried = {};
427              
428 0         0 foreach my $module (@modules) {
429 0 0       0 if (exists $tried->{$module}) {
430 0 0       0 next unless $tried->{$module};
431 0         0 return $module;
432             } else {
433 0         0 my $res = eval {
434 0         0 my $modname = $module =~ s#::#/#gr;
435 0         0 require $modname.'.pm';
436 0         0 1;
437             };
438 0         0 $tried->{$module} = $res;
439 0 0       0 return $module if $res;
440             }
441             }
442              
443 0         0 croak 'Found none of modules ['.join(', ', @modules).']';
444             }
445              
446             sub _finish {
447 79     79   210 my ($raw, $version) = @_;
448 79         256 substr($raw, 6, 1, chr((ord(substr($raw, 6, 1)) & 0x0F) | ($version << 4)));
449 79         126 substr($raw, 8, 1, chr((ord(substr($raw, 8, 1)) & 0x3F) | 0x80));
450 79         611 return join('-', unpack('H8H4H4H4H12', $raw));
451             }
452              
453             sub _random {
454 0     0   0 my ($pkg, %opts) = @_;
455 0   0     0 my $sources = $opts{sources} // (state $default_sources = [qw(Crypt::URandom UUID4::Tiny Math::Random::Secure UUID::URandom UUID::Tiny::Patch::UseMRS)]);
456 0         0 my $source = _available_module(@{$sources});
  0         0  
457 0         0 my $raw;
458              
459             # Secure:
460 0 0       0 if ($source eq 'Crypt::URandom') {
    0          
    0          
    0          
    0          
    0          
    0          
461 0         0 $raw = Crypt::URandom::urandom(16);
462             } elsif ($source eq 'UUID4::Tiny') {
463 0         0 return UUID4::Tiny::create_uuid_string();
464             } elsif ($source eq 'Math::Random::Secure') {
465 0         0 $raw = join('', map {chr Math::Random::Secure::irand(256)} 0..15);
  0         0  
466             } elsif ($source eq 'UUID::URandom') {
467 0         0 return UUID::URandom::create_uuid_string();
468             } elsif ($source eq 'UUID::Tiny::Patch::UseMRS') {
469 0         0 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_RANDOM());
470              
471             # Insecure:
472             } elsif ($source eq 'UUID::Tiny') {
473 0         0 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_RANDOM());
474             } elsif ($source eq 'Data::UUID') {
475 0         0 require Data::UUID;
476 0         0 return Data::UUID->create_str;
477             #} elsif ($source eq '') {
478             } else {
479 0         0 croak 'Invalid/unsupported source';
480             }
481              
482 0 0 0     0 if (defined($raw) && length($raw) == 16) {
483 0         0 return _finish($raw, 4);
484             }
485              
486 0         0 croak 'Bug!';
487             }
488              
489             sub _uuid_v5 {
490 79     79   140 my ($pkg, $ns, $data) = @_;
491 79         237 my $digest = Digest->new('SHA-1');
492              
493 79 50       10932 $ns = $ns->uuid(no_defaults => 1) if ref $ns;
494              
495 79         301 $ns = pack('H*', $ns =~ tr/-//dr);
496              
497 79 50       146 croak 'Invalid namespace given' unless length($ns) == 16;
498              
499 79         209 $digest->add($ns);
500 79         142 $digest->add($data);
501              
502 79         378 return _finish(substr($digest->digest, 0, 16), 5);
503             }
504              
505             1;
506              
507             __END__