File Coverage

blib/lib/Articulate/LocationSpecification.pm
Criterion Covered Total %
statement 52 65 80.0
branch 23 42 54.7
condition 2 6 33.3
subroutine 13 15 86.6
pod 6 6 100.0
total 96 134 71.6


line stmt bran cond sub pod time code
1             package Articulate::LocationSpecification;
2 10     10   773 use strict;
  10         14  
  10         358  
3 10     10   46 use warnings;
  10         22  
  10         248  
4              
5 10     10   42 use Moo;
  10         13  
  10         56  
6 10     10   2852 use Scalar::Util qw(blessed);
  10         17  
  10         785  
7 10     10   54 use overload '""' => \&to_file_path, '@{}' => sub{ shift->path };
  10     880   11  
  10         106  
  880         2146  
8 10     10   695 use Articulate::Location;
  10         12  
  10         77  
9              
10 10     10   2758 use Exporter::Declare;
  10         18  
  10         47  
11             default_exports qw(locspec);
12              
13             =head1 NAME
14              
15             Articulate::LocationSpecification - represent a specification
16              
17             =cut
18              
19             =head1 DESCRIPTION
20              
21             locspec ['zone', '*', 'article', 'hello-world']
22             locspec 'zone/*/article/hello-world' # same thing
23              
24             An object class which represents a specification - like a 'pattern' or 'glob', and provides methods so that it can be compared with locations. It is similar to C, and stringifies to the 'file path' representation.
25              
26             The main use of this is to determine whether a user has access to a resource based on rules.
27              
28             =cut
29              
30             =head1 FUNCTIONS
31              
32             =head3 locspec
33              
34             C is a constructor. It takes either a string (in the form of a path) or an arrayref. Either will be stored as an arrayref in the C attribute.
35              
36             =cut
37              
38             sub locspec {
39 23 50   23 1 1129 if ( 1 == scalar @_ ) {
40 23 50 33     310 if ( blessed $_[0] and $_[0]->isa('Articulate::LocationSpecification') ) {
    50 33        
    100          
    50          
    50          
    0          
41 0         0 return $_[0];
42             }
43             elsif ( blessed $_[0] and $_[0]->isa('Articulate::Location') ) {
44 0         0 my $path = $_[0]->path; # should this logic be in the coerce?
45 0 0       0 if (@$path) {
46 0         0 for my $i (1..$#$path) {
47 0 0       0 if (0 == ($i % 2) ) {
48 0         0 $path->[$i] = '*';
49             }
50             }
51             }
52 0         0 return __PACKAGE__->new({ path => $path });
53             }
54             elsif ( ref $_[0] eq 'ARRAY' ) {
55 3         66 return __PACKAGE__->new({ path => $_[0] });
56             }
57             elsif ( !defined $_[0] ) {
58 0         0 return __PACKAGE__->new;
59             }
60             elsif ( !ref $_[0] ) {
61 20         94 return __PACKAGE__->new({ path => [ grep { $_ ne '' } split /\//, $_[0] ] });
  62         509  
62             }
63             elsif ( ref $_[0] eq 'HASH' ) {
64 0         0 return __PACKAGE__->new($_[0]);
65             }
66             }
67             };
68              
69             =head1 METHODS
70              
71             =head3 path
72              
73             An arrayref representing the path to the location specification. This is used for overloaded array dereferencing.
74              
75             =cut
76              
77             has path => (
78             is => 'rw',
79             default => sub { []; },
80             );
81              
82             =head3 location
83              
84             $locspec->location->location # same as $locspec
85              
86             This method always returns the object itself.
87              
88             =cut
89              
90              
91             sub location {
92 0     0 1 0 return shift;
93             }
94              
95             =head3 to_file_path
96              
97             Joins the contents of C on C and returns the result. This is used for overloaded stringification.
98              
99             =cut
100              
101             sub to_file_path {
102 0     0 1 0 return join '/', @{ $_[0]->path }
  0         0  
103             };
104              
105             sub _step_matches {
106 438     438   513 my ( $left, $right ) = @_;
107 438 100       1052 return 1 if ( $left eq '*' );
108 228 50       383 return 1 if ( $right eq '*' );
109 228 50       705 return 1 if ( $left eq $right );
110 0         0 return 0;
111              
112             }
113              
114             =head3 matches
115              
116             locspec('/zone/*')->matches(loc('/zone/public')) # true
117             locspec('/zone/*')->matches(loc('/')) # false
118             locspec('/zone/*')->matches(loc('/zone/public/article/hello-world')) # false
119              
120             Determines if the location given as the first argument matches the locspec.
121              
122             =cut
123              
124              
125             sub matches {
126 202     202 1 2093 my $self = shift;
127 202         414 my $location = loc shift;
128 202 100       1256 return 0 unless $#$self == $#$location;
129 108 50       140 return 1 if $#$self == -1; # go no further if both are empty
130 108         149 for my $i (0..$#$self) {
131 406 50       416 return 0 unless _step_matches( $self->[$i], $location->[$i] );
132             }
133 108         316 return 1;
134             }
135              
136             =head3 matches_ancestor_of
137              
138             locspec('/zone/*')->matches_ancestor_of(loc('/zone/public')) # true
139             locspec('/zone/*')->matches_ancestor_of(loc('/')) # false
140             locspec('/zone/*')->matches_ancestor_of(loc('/zone/public/article/hello-world')) # true
141              
142             Determines if the location given as the first argument - or any ancestor thereof - matches the locspec.
143              
144             =cut
145              
146             sub matches_ancestor_of {
147 7     7 1 714 my $self = shift;
148 7         16 my $location = loc shift;
149 7 100       15 return 0 unless $#$self <= $#$location;
150 5 50       5 return 1 if $#$self == -1; # go no further if self is empty
151 5         6 for my $i (0..$#$self) {
152 16 50       15 return 0 unless _step_matches( $self->[$i], $location->[$i] );
153             }
154 5         12 return 1;
155             }
156              
157             =head3 matches_descendant_of
158              
159             locspec('/zone/*')->matches_descendant_of(loc('/zone/public')) # true
160             locspec('/zone/*')->matches_descendant_of(loc('/')) # true
161             locspec('/zone/*')->matches_descendant_of(loc('/zone/public/article/hello-world')) # false
162              
163             Determines if the location given as the first argument - or any descendant thereof - matches the locspec.
164              
165             =cut
166              
167             sub matches_descendant_of {
168 7     7 1 1500 my $self = shift;
169 7         20 my $location = loc shift;
170 7 100       14 return 0 unless $#$self >= $#$location;
171 5 50       7 return 1 if $#$location == -1; # go no further if self is empty
172 5         7 for my $i (0..$#$location) {
173 16 50       15 return 0 unless _step_matches( $self->[$i], $location->[$i] );
174             }
175 5         13 return 1;
176             }
177              
178             1;