File Coverage

lib/Data/TagDB/Tag.pm
Criterion Covered Total %
statement 20 297 6.7
branch 0 188 0.0
condition 0 81 0.0
subroutine 7 27 25.9
pod 12 13 92.3
total 39 606 6.4


line stmt bran cond sub pod time code
1             # Copyright (c) 2024-2025 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: Work with Tag databases
6              
7             package Data::TagDB::Tag;
8              
9 1     1   9 use v5.16;
  1         3  
10 1     1   3 use strict;
  1         1  
  1         17  
11 1     1   2 use warnings;
  1         1  
  1         37  
12              
13 1     1   3 use Carp;
  1         2  
  1         60  
14 1     1   604 use URI;
  1         4460  
  1         39  
15              
16             our $VERSION = v0.12;
17              
18 1     1   6 use parent 'Data::Identifier::Interface::Simple';
  1         1  
  1         8  
19              
20 1     1   162087 use Data::Identifier v0.28;
  1         25  
  1         20  
21              
22             my %_key_to_data_identifier = (
23             'small-identifier' => 'sid',
24             (map {$_ => $_} qw(uuid oid uri)),
25             );
26              
27              
28              
29             sub db {
30 0     0 1   my ($self) = @_;
31 0           return $self->{db};
32             }
33              
34              
35             sub dbid {
36 0     0 1   my ($self) = @_;
37 0 0         return 0 unless defined $self;
38 0           return $self->{dbid};
39             }
40              
41              
42             sub _get_id {
43 0     0     my ($self, %opts) = @_;
44 0           my $key = $opts{_key};
45 0   0       my $value = $self->{$key} //= eval { $self->_get_data(_tag_simple_identifier => $key => $self->dbid) };
  0            
46 0           my $curtype;
47              
48 0 0 0       if (!defined($value) && !$opts{no_defaults}) {
49 0 0         if (defined $self->{$key.'_defaults'}) {
50 0           $value = $self->{$key.'_defaults'};
51             } else {
52 0           my $id;
53             my $backup_key;
54 0           foreach my $backup_key_try (qw(uuid oid uri small-identifier)) {
55 0           $id = eval { $self->_get_data(_tag_simple_identifier => $backup_key_try => $self->dbid) };
  0            
56 0 0         if (defined $id) {
57 0           $backup_key = $backup_key_try;
58 0           last;
59             }
60             }
61 0 0 0       if (defined($id) && defined($backup_key)) {
62 0 0 0       if (defined(my $type = $_key_to_data_identifier{$backup_key})) {
    0 0        
    0 0        
    0          
63 0           my $did = Data::Identifier->new($type => $id);
64 0   0       my $func = $did->can($_key_to_data_identifier{$key} // '');
65 0 0         if (defined $func) {
66 0           $value = eval {$did->$func()};
  0            
67 0           $curtype = $key;
68             }
69             } elsif ($backup_key eq 'uuid' && $key eq 'uri') {
70 0           $value = sprintf('urn:uuid:%s', $id);
71             } elsif ($backup_key eq 'oid' && $key eq 'uri') {
72 0           $value = sprintf('urn:oid:%s', $id);
73             } elsif ($backup_key eq 'small-identifier' && $key eq 'uri') {
74 0           my $u = URI->new("https://uriid.org/");
75 0           $u->path_segments('', 'sid', $id);
76 0           $value = $u->as_string;
77             }
78             }
79              
80 0 0         $self->{$key.'_defaults'} = $value if defined $value;
81             }
82             }
83              
84 0 0         if (defined $value) {
85 0   0       my $as = $opts{as} // $key;
86 0   0       $curtype //= $key;
87              
88 0 0 0       if ($as eq $key || $as eq 'raw') {
    0 0        
    0 0        
89 0           return $value;
90             } elsif ($as eq 'URI' && $curtype eq 'uri') {
91 0   0       return $self->{$key.'_URI'} //= URI->new($value); # convert and cache.
92             } elsif ($as eq 'Data::Identifier' && defined(my $type = $_key_to_data_identifier{$curtype})) {
93 0     0     return Data::Identifier->new($type => $value, displayname => sub { $self->displayname(default => undef) });
  0            
94             } else {
95 0           croak 'Unsupported as option: '.$as;
96             }
97             }
98              
99 0 0         return $opts{default} if exists $opts{default};
100 0           croak 'No identifier of requested type';
101             }
102              
103             sub uuid {
104 0     0 1   my ($self, %opts) = @_;
105 0           return $self->_get_id(%opts, _key => 'uuid');
106             }
107             sub oid {
108 0     0 1   my ($self, %opts) = @_;
109 0           return $self->_get_id(%opts, _key => 'oid');
110             }
111             sub uri {
112 0     0 1   my ($self, %opts) = @_;
113 0   0       return $self->_get_id(%opts, _key => 'uri', as => $opts{as} // 'URI');
114             }
115             sub sid {
116 0     0 1   my ($self, %opts) = @_;
117 0           return $self->_get_id(%opts, _key => 'small-identifier');
118             }
119              
120              
121             sub ise {
122 0     0 1   my ($self, %opts) = @_;
123 0           my $has_default = exists $opts{default};
124 0           my $val_default = delete $opts{default};
125 0           my $val_no_defaults = delete $opts{no_defaults};
126 0           my @keys = qw(uuid oid uri);
127 0           my $value;
128              
129 0           $opts{default} = undef;
130 0           $opts{no_defaults} = 1;
131              
132 0           foreach my $key (@keys) {
133 0           $value = $self->_get_id(%opts, _key => $key);
134 0 0         last if defined $value;
135             }
136 0 0         return $value if defined $value;
137              
138 0 0         unless ($val_no_defaults) {
139             # retry with defaults
140 0           delete $opts{default};
141 0           delete $opts{no_defaults};
142 0           foreach my $key (@keys) {
143 0           my $func = $self->can($key);
144 0           $value = eval {$self->$func(%opts)};
  0            
145 0 0         last if defined $value;
146             }
147 0 0         return $value if defined $value;
148             }
149              
150 0 0         return $val_default if $has_default;
151              
152 0           croak 'No ISE found or unsupported as-option';
153             }
154              
155              
156             sub displayname {
157 0     0 1   my ($self, %opts) = @_;
158              
159 0 0         unless (defined $self->{displayname}) {
160 0           my $policies = [qw(british dash nospace lower noupper long)];
161 0           my $db = $self->db;
162 0           my $wk = $db->wk;
163 0           my $asi = $wk->also_shares_identifier;
164 0           my $name;
165 0           my @identifier_types = (
166             $wk->tagname,
167             );
168              
169 0           foreach my $relation (
170             [qw(also_has_title)],
171             [qw(tagpool_title gamebook_has_title)],
172             ) {
173 0           $relation = [grep {defined} map {eval{$wk->_call($_)}} @{$relation}];
  0            
  0            
  0            
  0            
174 0 0         next unless scalar @{$relation};
  0            
175 0           $name = eval {
176 0           _select_string(
177             $policies,
178             $db->metadata(tag => $self, relation => $relation)->collect('data', skip_died => 1),
179             );
180             };
181 0 0 0       return $self->{displayname} = $name if defined($name) && length($name);
182             }
183              
184 0           foreach my $type (@identifier_types) {
185 0           $name = eval {
186 0           _select_string(
187             $policies,
188             $db->metadata(tag => $self, relation => $asi, type => $type)->collect('data', skip_died => 1),
189             );
190             };
191 0 0 0       return $self->{displayname} = $name if defined($name) && length($name);
192             }
193              
194 0 0 0       unless ($opts{no_defaults} || defined($self->{displayname_defaults})) {
195 0           $name = eval {
196 0           _select_string(
197             $policies,
198             $db->metadata(tag => $self, relation => $asi, no_type => [
199             @identifier_types,
200             $wk->uuid, $wk->oid, $wk->uri,
201             ])->collect('data', skip_died => 1),
202             );
203             };
204 0 0 0       return $self->{displayname_defaults} = $name if defined($name) && length($name);
205              
206 0           $name = eval {$self->ise};
  0            
207 0 0 0       return $self->{displayname_defaults} = $name if defined($name) && length($name);
208             }
209             }
210              
211 0 0         return $self->{displayname} if defined $self->{displayname};
212 0 0         return $opts{default} if exists $opts{default};
213 0 0         unless ($opts{no_defaults}) {
214 0 0         return $self->{displayname_defaults} if defined $self->{displayname_defaults};
215 0           return 'no name';
216             }
217              
218 0           croak 'No displayname found';
219             }
220              
221              
222             sub displaycolour {
223 0     0 1   my ($self, %opts) = @_;
224 0 0         if (exists $self->{displaycolour}) {
225 0 0         return $self->{displaycolour} if defined $self->{displaycolour};
226 0 0         return $opts{default} if exists $opts{default};
227 0           croak 'No value found';
228             } else {
229 0           my $db = $self->db;
230 0           my $wk = $db->wk;
231              
232 0           $self->{displaycolour} = undef; # set to undef early so we can safely recurse.
233              
234 0           foreach my $relation (
235             [qw(displaycolour)],
236             [qw(has_colour_value)],
237             [qw(wd_sRGB_colour_hex_triplet)]
238             ) {
239 0           $relation = [grep {defined} map {eval{$wk->_call($_)}} @{$relation}];
  0            
  0            
  0            
  0            
240 0 0         next unless scalar @{$relation};
  0            
241 0           my $colour = eval {
242 0           ($db->metadata(tag => $self, relation => $relation)->collect('data', skip_died => 1))[0],
243             };
244 0 0         return $self->{displaycolour} = $colour if defined($colour);
245             }
246              
247 0           foreach my $relation (
248             [qw(displaycolour)],
249             [qw(primary_colour)],
250             [qw(also_shares_colour)],
251             ) {
252 0           $relation = [grep {defined} map {eval{$wk->_call($_)}} @{$relation}];
  0            
  0            
  0            
  0            
253 0 0         next unless scalar @{$relation};
  0            
254 0           my $colour = eval {
255 0     0     ($db->relation(tag => $self, relation => $relation)->collect(sub {$_[0]->related->displaycolour}, skip_died => 1))[0],
  0            
256             };
257 0 0         return $self->{displaycolour} = $colour if defined($colour);
258             }
259              
260 0 0         return $self->{displaycolour} if defined $self->{displaycolour};
261 0 0         return $opts{default} if exists $opts{default};
262 0           croak 'No value found';
263             }
264             }
265              
266              
267             sub icontext {
268 0     0 1   my ($self, %opts) = @_;
269 0 0         if (exists $self->{icontext}) {
270 0 0         return $self->{icontext} if defined $self->{icontext};
271 0 0         return $opts{default} if exists $opts{default};
272 0           croak 'No value found';
273             } else {
274 0           my $db = $self->db;
275 0           my $wk = $db->wk;
276              
277 0           $self->{icontext} = undef; # set to undef as a default.
278              
279 0           foreach my $relation (
280             [qw(tagpool_tag_icontext)],
281             [qw(wd_unicode_character)],
282             ) {
283 0           $relation = [grep {defined} map {eval{$wk->_call($_)}} @{$relation}];
  0            
  0            
  0            
  0            
284 0 0         next unless scalar @{$relation};
  0            
285 0           my $icontext = eval {
286 0           ($db->metadata(tag => $self, relation => $relation)->collect('data', skip_died => 1))[0],
287             };
288 0 0 0       return $self->{icontext} = $icontext if defined($icontext) && length($icontext);
289             }
290              
291             # TODO: This should be extended to all roles:
292             {
293 0 0         if ((defined(my $tagpool_type_icontext = eval {$wk->tagpool_type_icontext}))) {
  0            
  0            
294 0           my $relation = [grep {defined} map {eval{$wk->_call($_)}} qw(has_type)];
  0            
  0            
  0            
295 0 0         if (scalar @{$relation}) {
  0            
296 0           my $icontext = ($db->metadata(
297             tag => $db->relation(tag => $self, relation => $relation)->collect('related', return_ref => 1),
298             relation => $tagpool_type_icontext
299             )->collect('data', skip_died => 1))[0];
300 0 0 0       return $self->{icontext} = $icontext if defined($icontext) && length($icontext);
301             }
302             }
303             }
304              
305 0 0         return $self->{icontext} if defined $self->{icontext};
306 0 0         return $opts{default} if exists $opts{default};
307 0           croak 'No value found';
308             }
309             }
310              
311              
312             sub description {
313 0     0 1   my ($self, %opts) = @_;
314 0 0         if (exists $self->{description}) {
315 0 0         return $self->{description} if defined $self->{description};
316 0 0         return $opts{default} if exists $opts{default};
317 0           croak 'No value found';
318             } else {
319 0           my $db = $self->db;
320 0           my $wk = $db->wk;
321              
322 0           $self->{description} = undef; # set to undef as a default.
323              
324 0           foreach my $relation (
325             [qw(also_has_description)],
326             [qw(tagpool_description)],
327             ) {
328 0           $relation = [grep {defined} map {eval{$wk->_call($_)}} @{$relation}];
  0            
  0            
  0            
  0            
329 0           ($self->{description}) = $db->metadata(tag => $self, relation => $relation)->collect('data', skip_died => 1);
330 0 0         last if defined $self->{description};
331             }
332              
333 0 0         return $self->{description} if defined $self->{description};
334 0 0         return $opts{default} if exists $opts{default};
335 0           croak 'No value found';
336             }
337             }
338              
339              
340             sub cloudlet {
341 0     0 1   my ($self, $which) = @_;
342 0           my $wk = $self->db->wk;
343 0           my %opts = (
344             tag => $self,
345             indirect => [
346             $wk->specialises,
347             ],
348             );
349              
350 0 0         if ($which eq 'roles') {
    0          
351             $opts{direct} = [
352 0           $wk->has_type,
353             $wk->also_has_role,
354             ];
355             } elsif ($which eq 'flags') {
356             $opts{direct} = [
357 0           $wk->flagged_as,
358             ];
359             } else {
360 0           croak 'Unknown cloudlet';
361             }
362              
363 0           return $self->db->_load_cloudlet(%opts);
364             }
365              
366             # ---- Private helpers ----
367              
368             sub _new {
369 0     0     my ($pkg, %opts) = @_;
370              
371 0           return bless \%opts, $pkg;
372             }
373              
374             sub _query {
375 0     0     my ($self, $name) = @_;
376 0           return $self->db->_query($name);
377             }
378              
379             sub _get_data {
380 0     0     my ($self, $name, @args) = @_;
381 0           return $self->db->_get_data($name, @args);
382             }
383              
384             sub _select_string {
385 0     0     my ($policies, @strings) = @_;
386 0           my %res = map {$_ => 0} @strings;
  0            
387              
388 0 0         return undef unless scalar @strings;
389              
390 0           foreach my $policy (@{$policies}) {
  0            
391 0           foreach my $first (@strings) {
392 0 0 0       if ($policy eq 'upper') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
393 0 0         $res{$first} += $first =~ /\p{upper}/ ? 1 : -1;
394             } elsif ($policy eq 'lower') {
395 0 0         $res{$first} += $first !~ /\p{upper}/ ? 1 : -1;
396             } elsif ($policy eq 'space') {
397 0 0         if ($first =~ /\s/) {
    0          
398 0           $res{$first}++;
399             } elsif ($first =~ /[-_]/) {
400 0           $res{$first}--;
401             }
402             } elsif ($policy eq 'dash') {
403 0 0         if ($first =~ /[-_]/) {
    0          
404 0           $res{$first}++;
405             } elsif ($first =~ /\s/) {
406 0           $res{$first}--;
407             }
408             } elsif ($policy eq 'nospace') {
409 0 0         if ($first =~ /\s/) {
410 0           $res{$first} -= 64;
411             }
412             } elsif ($policy eq 'noupper') {
413 0 0         if ($first =~ /\p{upper}/) {
414 0           $res{$first} -= 64;
415             }
416             } elsif ($policy eq 'long' || $policy eq 'short') {
417 0           foreach my $second (@strings) {
418 0 0         next if $first eq $second;
419 0           my $lf = length($first);
420 0           my $ls = length($second);
421 0 0         ($lf, $ls) = ($ls, $lf) if $policy eq 'short'; # swap.
422 0 0         if ($lf > $ls) {
423 0           $res{$first}++;
424 0           $res{$second}--;
425             }
426             }
427             } elsif ($policy eq 'british') {
428 0           foreach my $second (@strings) {
429 0 0         next if $first eq $second;
430 0 0         if (fc($first =~ tr/z/s/r) eq fc($second =~ tr/z/s/r)) {
431 0   0       my $fx = scalar($first =~ /(z)/g) || 0;
432 0   0       my $sx = scalar($second =~ /(z)/g) || 0;
433 0 0         if ($fx < $sx) {
434 0           $res{$first}++;
435 0           $res{$second}--;
436             }
437             }
438 0 0         if (fc(($first.' ') =~ s/er\b/re/gr) eq fc(($second.' ') =~ s/er\b/re/gr)) {
439 0   0       my $fx = scalar(($first.' ') =~ /(er)\b/g) || 0;
440 0   0       my $sx = scalar(($second.' ') =~ /(er)\b/g) || 0;
441 0 0         if ($fx < $sx) {
442 0           $res{$first}++;
443 0           $res{$second}--;
444             }
445             }
446 0 0         if (fc($first =~ s/ou/o/gr) eq fc($second =~ s/ou/o/gr)) {
447 0   0       my $fx = scalar($first =~ /(ou)/g) || 0;
448 0   0       my $sx = scalar($second =~ /(ou)/g) || 0;
449 0 0         if ($fx > $sx) {
    0          
450 0           $res{$first}++;
451 0           $res{$second}--;
452             } elsif ($fx < $sx) { # this one is not symetric, so we need it the other way around as well
453 0           $res{$first}--;
454 0           $res{$second}++;
455             }
456             }
457 0 0         if (fc($first =~ s/gray/grey/gir) eq fc($second)) {
458 0           $res{$first}--;
459 0           $res{$second}++;
460             }
461             }
462             } else {
463 0           die 'Bad policy';
464             }
465             }
466             }
467              
468             #say '#++ Dump: [', join(' ', @{$policies}), ']', map {sprintf(' "%s" => %d', $_, $res{$_})} sort {$res{$b} <=> $res{$a} || $a cmp $b} @strings;
469 0 0         return (sort {$res{$b} <=> $res{$a} || $a cmp $b} @strings)[0];
  0            
470             }
471              
472             sub attribute {
473 0     0 0   my ($self, $attribute, %opts) = @_;
474              
475 0 0 0       if ($attribute =~ /^display/ || $attribute eq 'icontext') {
476 0           my $func = $self->can($attribute);
477              
478 0 0         return $self->$func(%opts) if defined $func;
479             }
480              
481 0   0       $self->{attribute} //= {};
482              
483 0 0         if (exists $opts{set}) {
484 0           $self->{attribute}{$attribute} = $opts{set};
485             }
486              
487 0 0         unless (exists $self->{attribute}{$attribute}) {
488             # TODO: try to calculate here.
489             }
490              
491 0           return $self->{attribute}{$attribute};
492             }
493              
494             1;
495              
496             __END__
497              
498             =pod
499              
500             =encoding UTF-8
501              
502             =head1 NAME
503              
504             Data::TagDB::Tag - Work with Tag databases
505              
506             =head1 VERSION
507              
508             version v0.12
509              
510             =head1 SYNOPSIS
511              
512             use Data::TagDB;
513              
514             my $db = Data::TagDB->new(...);
515              
516             my Data::TagDB::Tag $tag = $db->tag_by_...(...);
517              
518             This package represents a single tag in the database.
519              
520             This package inherits from L<Data::Identifier::Interface::Simple> (since v0.11).
521              
522             =head1 UNIVERSAL OPTIONS
523              
524             The following universe options are supported by many methods of this module. Each method lists which universal options it supports.
525              
526             =head2 default
527              
528             The default value to be returned if no value could be found.
529             Can be C<undef> to switch the method from C<die>ing to returning C<undef> in case no value is found.
530              
531             =head2 no_defaults
532              
533             Prevents the calculation of any fallback values.
534              
535             =head1 METHODS
536              
537             =head2 db
538              
539             my Data::TagDB $db = $tag->db;
540              
541             Returns the current L<Data::TagDB> object.
542              
543             =head2 dbid
544              
545             my $dbid = $db->dbid;
546              
547             Returns the current tag's database internal identifier. This call should be avoided as those identifiers are not stable nor portable.
548             It is however the best option when directly interacting with the backend database.
549              
550             =head2 uuid, oid, uri, sid
551              
552             my $uuid = $tag->uuid( [ %opts ] );
553             my $oid = $tag->oid( [ %opts ] );
554             my URI $uri = $tag->uri( [ %opts ] );
555             my $sid = $tag->sid( [ %opts ] );
556              
557             Returns the tags UUID, OID, URI, or SID (small-identifier).
558             Identifiers may also be unavailable due to being not part of the database.
559              
560             The following universal options are supported: L</default>, L</no_defaults>.
561              
562             =head2 ise
563              
564             my $ise = $tag->ise( [ %opts ] );
565              
566             Returns an identifier (C<uuid>, C<oid>, or C<uri>) for the tag as string.
567              
568             Supports the same options as supported by L</uuid>, L</oid>, and L</uri>.
569              
570             =head2 displayname
571              
572             my $displayname = $tag->displayname( [ %opts ] );
573              
574             Returns a name that can be used to display to the user or C<die>s.
575             This function always returns a plain string (even if no usable name is found) unless L</no_defaults> is given.
576              
577             The following universal options are supported: L</default>, L</no_defaults>.
578              
579             =head2 displaycolour
580              
581             my $displaycolour = $tag->displaycolour( [ %opts ] );
582              
583             Returns a colour that can be used to display the tag.
584             This will return a decoded object, most likely (but not necessarily) an instance of L<Data::URIID::Colour>.
585             Later versions of this module may allow to force a specific type.
586              
587             This method C<die>s if no value can be found and no L</default> is given (since v0.11).
588              
589             The following universal options are supported: L</default>.
590             The following universal options are ignored (without warning or error): L</no_defaults>.
591              
592             =head2 icontext
593              
594             my $icontext = $tag->icontext( [ %opts ] );
595              
596             Returns a string that is a single unicode character that represents the tag.
597             This can be used as a visual aid for the user.
598             It is not well defined what single character means in this case. A single character may map
599             to multiple unicode code points (such as a base and modifiers). If the application requies a
600             specific definition of single character it must validate the value.
601              
602             This method C<die>s if no value can be found and no L</default> is given (since v0.11).
603              
604             The following universal options are supported: L</default>.
605             The following universal options are ignored (without warning or error): L</no_defaults>.
606              
607             =head2 description
608              
609             my $description = $tag->description( [ %opts ] );
610              
611             Returns a description that can be used to display to the user.
612              
613             This method C<die>s if no value can be found and no L</default> is given (since v0.11).
614              
615             The following universal options are supported: L</default>.
616             The following universal options are ignored (without warning or error): L</no_defaults>.
617              
618             =head2 cloudlet
619              
620             my Data::TagDB::Cloudlet $cl = $tag->cloudlet($which);
621              
622             B<Experimental:>
623             Gets the given cloudlet.
624              
625             B<Note:>
626             This method is experimental. It may change prototype, and behaviour or may be removed in future versions without warning.
627              
628             =head1 AUTHOR
629              
630             Philipp Schafft <lion@cpan.org>
631              
632             =head1 COPYRIGHT AND LICENSE
633              
634             This software is Copyright (c) 2024-2025 by Philipp Schafft <lion@cpan.org>.
635              
636             This is free software, licensed under:
637              
638             The Artistic License 2.0 (GPL Compatible)
639              
640             =cut