File Coverage

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


line stmt bran cond sub pod time code
1 3     3   81733 use 5.12.0;
  3         12  
  3         107  
2 3     3   14 use warnings;
  3         6  
  3         127  
3              
4             package Data::TagHive;
5             {
6             $Data::TagHive::VERSION = '0.003';
7             }
8             # ABSTRACT: hierarchical tags with values
9              
10 3     3   14 use Carp;
  3         6  
  3         2802  
11              
12              
13             sub new {
14 25     25 0 15167 my ($class) = @_;
15              
16 25         116 return bless { state => {} } => $class;
17             }
18              
19             my $tagname_re = qr{ [a-z] [-a-z0-9_]* }x;
20             my $tagvalue_re = qr{ [-a-z0-9_]+ }x;
21             my $tagpair_re = qr{ $tagname_re (?::$tagvalue_re)? }x;
22             my $tagstr_re = qr{ \A $tagpair_re (?:\.$tagpair_re)* \z }x;
23              
24             sub _assert_tagstr {
25 86     86   100 my ($self, $tagstr) = @_;
26 86 100       849 croak "invalid tagstr <$tagstr>" unless $tagstr =~ $tagstr_re;
27             }
28              
29             sub _tag_pairs {
30 61     61   79 my ($self, $tagstr) = @_;
31              
32 61         100 $self->_assert_tagstr($tagstr);
33              
34 60         144 my @tags = map { my @pair = split /:/, $_; $#pair = 1; \@pair }
  106         215  
  106         5363  
  106         205  
35             split /\./, $tagstr;
36              
37 60         148 return @tags;
38             }
39              
40             sub __differ {
41 43     43   48 my ($x, $y) = @_;
42              
43 43 100 100     201 return 1 if defined $x xor defined $y;
44 31 100       82 return unless defined $x;
45              
46 16         47 return $x ne $y;
47             }
48              
49              
50             sub add_tag {
51 61     61 1 4834 my ($self, $tagstr) = @_;
52              
53 61         94 my $state = $self->{state};
54              
55 61         126 my @tags = $self->all_tags;
56 61         128 my @pairs = $self->_tag_pairs($tagstr);
57              
58 60         85 my $stem = '';
59              
60 60         139 while (my $pair = shift @pairs) {
61 99 100       184 $stem .= '.' if length $stem;
62              
63 99         156 my $key = $stem . $pair->[0];
64 99 100       175 my $value = length($pair->[1]) ? $pair->[1] : undef;
65              
66             CONFLICT: {
67 99 100       90 if (exists $state->{ $key }) {
  99         198  
68 43         57 my $existing = $state->{ $key };
69              
70             # Easiest cases: if they're both undef, or are eq, no conflict.
71 43 100       74 last CONFLICT unless __differ($value, $existing);
72              
73             # Easist conflict case: we want to set tag:value1 but tag:value2 is
74             # already set. No matter whether there are descendants on either side,
75             # this is a
76             # conflict.
77 18 100 100     659 croak "can't add <$tagstr> to taghive; conflict at $key"
      66        
78             if defined $value and defined $existing and $value ne $existing;
79              
80 12   100     35 my $more_to_set = defined($value) || @pairs;
81 12   100     37 my $more_exists = defined($state->{$key}) || grep { /\A\Q$key./ } @tags;
82              
83 12 100 100     721 croak "can't add <$tagstr> to taghive; conflict at $key"
84             if $more_to_set and $more_exists;
85             }
86             }
87              
88              
89 86         144 $state->{ $key } = $value;
90              
91 86 100       151 $stem = defined $value ? "$key:$value" : $key;
92              
93 86         399 $state->{$stem} = undef;
94             }
95             }
96              
97              
98             sub has_tag {
99 22     22 1 5666 my ($self, $tagstr) = @_;
100              
101 22         44 my $state = $self->{state};
102              
103 22         38 $self->_assert_tagstr($tagstr);
104 21 100       89 return 1 if exists $state->{$tagstr};
105 7         33 return;
106             }
107              
108              
109             sub delete_tag {
110 3     3 1 16 my ($self, $tagstr) = @_;
111              
112 3         5 $self->_assert_tagstr($tagstr);
113              
114 3         5 my $state = $self->{state};
115 3         8 my @keys = grep { /\A$tagstr(?:$|[.:])/ } keys %$state;
  21         134  
116 3         8 delete @$state{ @keys };
117              
118 3 100       33 if ($tagstr =~ s/:($tagvalue_re)\z//) {
119 2 100 66     20 delete $state->{ $tagstr } if $state->{$tagstr} // '' eq $1;
120             }
121             }
122              
123              
124             sub all_tags {
125 62     62 1 285 my ($self) = @_;
126 62         63 return keys %{ $self->{state} };
  62         196  
127             }
128              
129             1;
130              
131             __END__