File Coverage

blib/lib/Data/TagHive.pm
Criterion Covered Total %
statement 59 59 100.0
branch 26 26 100.0
condition 20 22 90.9
subroutine 11 11 100.0
pod 4 5 80.0
total 120 123 97.5


line stmt bran cond sub pod time code
1 3     3   196272 use 5.12.0;
  3         53  
2 3     3   15 use warnings;
  3         4  
  3         111  
3              
4             package Data::TagHive 0.004;
5             # ABSTRACT: hierarchical tags with values
6              
7 3     3   16 use Carp;
  3         4  
  3         2364  
8              
9             #pod =head1 SYNOPSIS
10             #pod
11             #pod use Data::TagHive;
12             #pod
13             #pod my $taghive = Data::TagHive->new;
14             #pod
15             #pod $taghive->add_tag('book.topic:programming');
16             #pod
17             #pod $taghive->has_tag('book'); # TRUE
18             #pod
19             #pod =head1 OVERVIEW
20             #pod
21             #pod Data::TagHive is the bizarre, corrupted union of L and
22             #pod L. It combines the "simple list of strings" of the former with the
23             #pod "hierarchical key-value/value pairs" of the latter, using a different interface
24             #pod from either.
25             #pod
26             #pod It's probably better than that sounds, though.
27             #pod
28             #pod A Data::TagHive object represents a set of tags. Each tag is a string that
29             #pod represents a structure of nested key-value pairs. For example, a library book
30             #pod might be tagged:
31             #pod
32             #pod book.pages.size:letter
33             #pod book.pages.count:180
34             #pod book.type:hardcover
35             #pod book.topic:programming.perl.cpan
36             #pod
37             #pod Each tag is a set of key-value pairs. Later pairs are qualified by earlier
38             #pod pairs. Values are optional. Keys and values are separated by colons.
39             #pod Key-value pairs are separated by dots.
40             #pod
41             #pod A tag is considered present if it was set explicitly or if any more-specific
42             #pod subtag of it was set. For example, if we had explicitly added all the tags
43             #pod shown above, a tag hive would then report true if asked whether each of the
44             #pod following tags were set:
45             #pod
46             #pod book
47             #pod book.pages
48             #pod book.pages.size
49             #pod book.pages.size:letter
50             #pod book.pages.count
51             #pod book.pages.count:180
52             #pod book.type
53             #pod book.type:hardcover
54             #pod book.topic
55             #pod book.topic:programming
56             #pod book.topic:programming.perl
57             #pod book.topic:programming.perl.cpan
58             #pod
59             #pod =cut
60              
61             sub new {
62 25     25 0 15151 my ($class) = @_;
63              
64 25         125 return bless { state => {} } => $class;
65             }
66              
67             my $tagname_re = qr{ [a-z] [-a-z0-9_]* }x;
68             my $tagvalue_re = qr{ [-a-z0-9_]+ }x;
69             my $tagpair_re = qr{ $tagname_re (?::$tagvalue_re)? }x;
70             my $tagstr_re = qr{ \A $tagpair_re (?:\.$tagpair_re)* \z }x;
71              
72             sub _assert_tagstr {
73 86     86   115 my ($self, $tagstr) = @_;
74 86 100       792 croak "invalid tagstr <$tagstr>" unless $tagstr =~ $tagstr_re;
75             }
76              
77             sub _tag_pairs {
78 61     61   93 my ($self, $tagstr) = @_;
79              
80 61         111 $self->_assert_tagstr($tagstr);
81              
82 60         158 my @tags = map { my @pair = split /:/, $_; $#pair = 1; \@pair }
  106         166  
  106         181  
  106         201  
83             split /\./, $tagstr;
84              
85 60         108 return @tags;
86             }
87              
88             sub __differ {
89 43     43   63 my ($x, $y) = @_;
90              
91 43 100 100     159 return 1 if defined $x xor defined $y;
92 31 100       71 return unless defined $x;
93              
94 16         41 return $x ne $y;
95             }
96              
97             #pod =method add_tag
98             #pod
99             #pod $taghive->add_tag( $tagstr );
100             #pod
101             #pod This method adds the given tag (given as a string) to the hive. It will fail
102             #pod if there are conflicts. For example, if "foo:bar" is already set, "foo:xyz"
103             #pod cannot be set. Each tag can only have one value.
104             #pod
105             #pod Tags without values may be given values through C, but only if they
106             #pod have no tags beneath them. For example, given a tag hive with "foo.bar"
107             #pod tagged, "foo.bar:baz" could be added, but not "foo:baz"
108             #pod
109             #pod =cut
110              
111             sub add_tag {
112 61     61 1 5431 my ($self, $tagstr) = @_;
113              
114 61         89 my $state = $self->{state};
115              
116 61         101 my @tags = $self->all_tags;
117 61         106 my @pairs = $self->_tag_pairs($tagstr);
118              
119 60         84 my $stem = '';
120              
121 60         118 while (my $pair = shift @pairs) {
122 99 100       160 $stem .= '.' if length $stem;
123              
124 99         179 my $key = $stem . $pair->[0];
125 99 100       198 my $value = length($pair->[1]) ? $pair->[1] : undef;
126              
127             CONFLICT: {
128 99 100       113 if (exists $state->{ $key }) {
  99         153  
129 43         61 my $existing = $state->{ $key };
130              
131             # Easiest cases: if they're both undef, or are eq, no conflict.
132 43 100       63 last CONFLICT unless __differ($value, $existing);
133              
134             # Easist conflict case: we want to set tag:value1 but tag:value2 is
135             # already set. No matter whether there are descendants on either side,
136             # this is a
137             # conflict.
138 18 100 100     502 croak "can't add <$tagstr> to taghive; conflict at $key"
      66        
139             if defined $value and defined $existing and $value ne $existing;
140              
141 12   100     25 my $more_to_set = defined($value) || @pairs;
142 12   100     29 my $more_exists = defined($state->{$key}) || grep { /\A\Q$key./ } @tags;
143              
144 12 100 100     569 croak "can't add <$tagstr> to taghive; conflict at $key"
145             if $more_to_set and $more_exists;
146             }
147             }
148              
149              
150 86         136 $state->{ $key } = $value;
151              
152 86 100       154 $stem = defined $value ? "$key:$value" : $key;
153              
154 86         271 $state->{$stem} = undef;
155             }
156             }
157              
158             #pod =method has_tag
159             #pod
160             #pod if ($taghive->has_tag( $tagstr )) { ... }
161             #pod
162             #pod This method returns true if the tag hive has the tag.
163             #pod
164             #pod =cut
165              
166             sub has_tag {
167 22     22 1 5798 my ($self, $tagstr) = @_;
168              
169 22         30 my $state = $self->{state};
170              
171 22         54 $self->_assert_tagstr($tagstr);
172 21 100       90 return 1 if exists $state->{$tagstr};
173 7         32 return;
174             }
175              
176             #pod =method delete_tag
177             #pod
178             #pod $taghive->delete_tag( $tagstr );
179             #pod
180             #pod This method deletes the tag from the hive, along with any tags below it.
181             #pod
182             #pod If your hive has "foo.bar:xyz.abc" and you C "foo.bar" it will be
183             #pod left with nothing but the tag "foo"
184             #pod
185             #pod =cut
186              
187             sub delete_tag {
188 3     3 1 19 my ($self, $tagstr) = @_;
189              
190 3         7 $self->_assert_tagstr($tagstr);
191              
192 3         6 my $state = $self->{state};
193 3         6 my @keys = grep { /\A$tagstr(?:$|[.:])/ } keys %$state;
  21         156  
194 3         9 delete @$state{ @keys };
195              
196 3 100       35 if ($tagstr =~ s/:($tagvalue_re)\z//) {
197 2 100 66     16 delete $state->{ $tagstr } if $state->{$tagstr} // '' eq $1;
198             }
199             }
200              
201             #pod =method all_tags
202             #pod
203             #pod This method returns, as a list of strings, all the tags set on the hive either
204             #pod explicitly or implicitly.
205             #pod
206             #pod =cut
207              
208             sub all_tags {
209 62     62 1 305 my ($self) = @_;
210 62         66 return keys %{ $self->{state} };
  62         165  
211             }
212              
213             1;
214              
215             __END__