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   222097 use 5.12.0;
  3         36  
2 3     3   15 use warnings;
  3         4  
  3         110  
3              
4             package Data::TagHive 0.005;
5             # ABSTRACT: hierarchical tags with values
6              
7 3     3   16 use Carp;
  3         5  
  3         2263  
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 15884 my ($class) = @_;
63              
64 25         115 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   127 my ($self, $tagstr) = @_;
74 86 100       731 croak "invalid tagstr <$tagstr>" unless $tagstr =~ $tagstr_re;
75             }
76              
77             sub _tag_pairs {
78 61     61   102 my ($self, $tagstr) = @_;
79              
80 61         115 $self->_assert_tagstr($tagstr);
81              
82 60         155 my @tags = map { my @pair = split /:/, $_; $#pair = 1; \@pair }
  106         166  
  106         178  
  106         194  
83             split /\./, $tagstr;
84              
85 60         117 return @tags;
86             }
87              
88             sub __differ {
89 43     43   59 my ($x, $y) = @_;
90              
91 43 100 100     151 return 1 if defined $x xor defined $y;
92 31 100       75 return unless defined $x;
93              
94 16         40 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 5384 my ($self, $tagstr) = @_;
113              
114 61         83 my $state = $self->{state};
115              
116 61         103 my @tags = $self->all_tags;
117 61         106 my @pairs = $self->_tag_pairs($tagstr);
118              
119 60         71 my $stem = '';
120              
121 60         115 while (my $pair = shift @pairs) {
122 99 100       164 $stem .= '.' if length $stem;
123              
124 99         187 my $key = $stem . $pair->[0];
125 99 100       176 my $value = length($pair->[1]) ? $pair->[1] : undef;
126              
127             CONFLICT: {
128 99 100       112 if (exists $state->{ $key }) {
  99         164  
129 43         54 my $existing = $state->{ $key };
130              
131             # Easiest cases: if they're both undef, or are eq, no conflict.
132 43 100       67 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     512 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     28 my $more_to_set = defined($value) || @pairs;
142 12   100     28 my $more_exists = defined($state->{$key}) || grep { /\A\Q$key./ } @tags;
143              
144 12 100 100     561 croak "can't add <$tagstr> to taghive; conflict at $key"
145             if $more_to_set and $more_exists;
146             }
147             }
148              
149              
150 86         125 $state->{ $key } = $value;
151              
152 86 100       140 $stem = defined $value ? "$key:$value" : $key;
153              
154 86         295 $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 6100 my ($self, $tagstr) = @_;
168              
169 22         33 my $state = $self->{state};
170              
171 22         45 $self->_assert_tagstr($tagstr);
172 21 100       86 return 1 if exists $state->{$tagstr};
173 7         29 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 17 my ($self, $tagstr) = @_;
189              
190 3         5 $self->_assert_tagstr($tagstr);
191              
192 3         4 my $state = $self->{state};
193 3         7 my @keys = grep { /\A$tagstr(?:$|[.:])/ } keys %$state;
  21         132  
194 3         8 delete @$state{ @keys };
195              
196 3 100       32 if ($tagstr =~ s/:($tagvalue_re)\z//) {
197 2 100 66     15 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 286 my ($self) = @_;
210 62         66 return keys %{ $self->{state} };
  62         169  
211             }
212              
213             1;
214              
215             __END__