File Coverage

blib/lib/App/WRT/EntryStore.pm
Criterion Covered Total %
statement 185 187 98.9
branch 34 42 80.9
condition 5 6 83.3
subroutine 39 41 95.1
pod 30 33 90.9
total 293 309 94.8


line stmt bran cond sub pod time code
1             package App::WRT::EntryStore;
2              
3 8     8   51 use strict;
  8         10  
  8         213  
4 8     8   35 use warnings;
  8         12  
  8         163  
5 8     8   74 use 5.10.0;
  8         32  
6              
7 8     8   45 use File::Find;
  8         10  
  8         401  
8 8     8   51 use Carp;
  8         8  
  8         467  
9 8     8   2586 use App::WRT::Sort qw(sort_entries);
  8         18  
  8         425  
10 8     8   2272 use App::WRT::Util qw(file_get_contents);
  8         19  
  8         19905  
11              
12             =pod
13              
14             =head1 NAME
15              
16             App::WRT::EntryStore - model the contents of a wrt repo's entry_dir
17              
18             =head1 SYNOPSIS
19              
20             use App::WRT::EntryStore;
21             my $entries = App::WRT::EntryStore->new('./archives');
22              
23             my @all = $entries->all();
24             my @months = $entries->all_months();
25             my @years = $entries->all_years();
26             my @days = $entries->all_days();
27              
28             # all_* are wrappers for dates_by_depth():
29             my @days = $entries->dates_by_depth(
30             3 # 1 for years, 2 for months, 3 for days
31             );
32              
33             my @recent_days = $entries->recent_days(30);
34             my @recent_months = $entries->recent_months(12);
35             my @recent_years = $entries->recent_years(10);
36              
37             # recent_* are wrappers for recent_by_depth():
38             my @recent_days $entries->recent_by_depth(
39             3, # 1 for years, 2 for months, 3 for days
40             30 # count
41             );
42              
43             =cut
44              
45             # "Constants"
46              
47             my $ENTRYTYPE_FILE = 0;
48             my $ENTRYTYPE_DIR = 1;
49             my $ENTRYTYPE_VIRT = 2;
50              
51             my %SUBENTRY_IGNORE = ('index' => 1);
52             my $SUBENTRY_EXPR = qr{
53             ^
54             [[:lower:][:digit:]_-]+
55             (
56             [.]
57             (tgz|zip|tar[.]gz|gz|txt)
58             )?
59             $
60             }x;
61              
62             # What gets considered a renderable entry path:
63             my $RENDERABLE_EXPR = qr{
64             ^
65             (
66             [[:lower:][:digit:]_\/-]+
67             )
68             $
69             }x;
70              
71             =head1 METHODS
72              
73             =over
74              
75             =item new($class, $entry_dir)
76              
77             Get a new EntryStore, using a given $entry_dir.
78              
79             Finds a list of entries for the given directory, and builds data structures
80             which can be used to index into entries by depth, property, and next/previous
81             entry.
82              
83             =cut
84              
85             sub new {
86 11     11 1 184 my $class = shift;
87 11         31 my ($entry_dir) = @_;
88              
89 11         38 my %params = (
90             entry_dir => $entry_dir
91             );
92              
93 11         24 my $self = \%params;
94              
95 11         24 bless $self, $class;
96              
97 11         59 my @entries;
98             my %source_files;
99 11         0 my %entry_properties;
100 11         0 my %property_entries;
101 11         0 my %children;
102              
103             find(
104             sub {
105 451 100   451   4123 return unless $File::Find::name =~ m{^ \Q$entry_dir\E / (.*) $}x;
106              
107 440         934 my $target = $1;
108              
109             # Build an ordered array of entries:
110 440         732 push @entries, $target;
111              
112             # Build a hash indicating:
113             # a. that a file exists
114             # b. whether it's a flatfile or a directory
115 440 100       6229 if (-f $_) {
    50          
116 275         927 $source_files{$target} = $ENTRYTYPE_FILE;
117             } elsif (-d $_) {
118 165         545 $source_files{$target} = $ENTRYTYPE_DIR;
119             }
120              
121             # Build hashes of all properties of entries, and all entries of properties:
122 440 100       14310 if ($target =~ m{(.*) / (.*) [.]prop $}x) {
123 55         196 my ($entry, $property) = ($1, $2);
124              
125 55   100     254 $entry_properties{$entry} //= [];
126 55         73 push @{ $entry_properties{$entry} }, $property;
  55         131  
127              
128 55   50     235 $property_entries{$property} //= [];
129 55         66 push @{ $property_entries{$property} }, $entry;
  55         722  
130             }
131             },
132 11         1079 $entry_dir
133             );
134              
135             # Ensure that the entry list for every property is sorted:
136 11         128 for (keys %property_entries) {
137 55         75 $property_entries{$_} = [ sort_entries(@{ $property_entries{$_} }) ];
  55         135  
138             }
139              
140             # Create virtual entries based on tags, _if there's not already a file
141             # there_:
142 11         40 foreach my $prop (keys %property_entries) {
143 55 100       151 if ( $prop =~ m/^ tag[.] (.*)$/x ) {
144 33         67 my $tag = $1;
145 33         84 $tag =~ s{[.]}{/}g;
146 33 50       85 unless (defined $source_files{$tag}) {
147 33         57 push @entries, $tag;
148 33         68 $source_files{$tag} = $ENTRYTYPE_VIRT;
149             }
150             }
151             }
152              
153             # Stash refs for future use:
154 11         68 $self->{entries} = \@entries;
155 11         88 $self->{source_files} = \%source_files;
156 11         23 $self->{property_entries} = \%property_entries;
157 11         25 $self->{entry_properties} = \%entry_properties;
158              
159 11         59 $self->generate_date_hashes();
160 11         48 $self->store_children();
161              
162 11         84 return $self;
163             }
164              
165             =item all()
166              
167             Returns a list of all source files for the current entry archive (excepting
168             index files, which are a special case - this part could use some work).
169              
170             This was originally in App::WRT::Renderer, so there may be some pitfalls here.
171              
172             =cut
173              
174             sub all {
175 57     57 1 89 my ($self) = shift;
176 57         62 return @{ $self->{entries} };
  57         1857  
177             }
178              
179             =item all_renderable()
180              
181             Returns a list of all source paths which are considered "renderable".
182              
183             =cut
184              
185             sub all_renderable() {
186 10     10 1 23 my ($self) = shift;
187             return grep {
188 430 100       1753 (index($_, 'index', -5) == -1)
189             &&
190             m/$RENDERABLE_EXPR/
191 10         17 } @{ $self->{entries} }; }
  10         28  
192              
193             =item dates_by_depth($depth)
194              
195             Returns a sorted list of all date-like entries which are at a specified depth.
196             Use 1 for years, 2 for months, and 3 for days.
197              
198             Fairly silly, but entertaining in its perverse way. all_years(), all_months(),
199             and all_days() are provided for convenience.
200              
201             =cut
202              
203             sub dates_by_depth {
204 153     153 1 215 my ($self) = shift;
205 153         204 my ($depth) = @_;
206              
207 153 50       275 croak('No $depth given.')
208             unless defined $depth;
209              
210             # Check if we already have a value cached:
211 120         360 return @{ $self->{by_depth}->{$depth} }
212 153 100       357 if defined $self->{by_depth}->{$depth};
213              
214             # Build a pattern for matching the given depth of date-like entries. For
215             # example, a day would be depth 3, and matched by \d+/\d+/\d+
216 33         44 my @particles;
217 33         85 for (my $i = 0; $i < $depth; $i++) {
218 66         119 push @particles, '\d+';
219             }
220 33         82 my $pattern = join '/', @particles;
221              
222 33         71 my @by_depth = sort_entries(
223             grep m{^ $pattern $}x, $self->all()
224             );
225              
226             # Stash arrayref for future use:
227 33         151 $self->{by_depth}->{$depth} = \@by_depth;
228              
229 33         89 return @by_depth;
230             }
231              
232             =item all_years(), all_months(), all_days()
233              
234             Convenience wrappers for dates_by_depth().
235              
236             =cut
237              
238 4     4 1 327 sub all_years { return $_[0]->dates_by_depth(1); }
239 33     33 1 382 sub all_months { return $_[0]->dates_by_depth(2); }
240 74     74 1 137 sub all_days { return $_[0]->dates_by_depth(3); }
241              
242             =item days_for($month), months_for($year)
243              
244             Convenience wrappers for extracting days or months in a given month
245             or year.
246              
247             =cut
248              
249             sub days_for {
250 72     72 1 1033 my ($self, $container) = @_;
251 72         169 return grep { m{^ \Q$container\E / }x } $self->all_days();
  216         1018  
252             }
253              
254             sub months_for {
255 31     31 1 607 my ($self, $year) = @_;
256 31         81 return grep { m{^ \Q$year\E / }x } $self->all_months();
  124         710  
257             }
258              
259             =item recent_by_depth($depth, $entry_count)
260              
261             Returns the $entry_count most recent dated entries at $depth (1 for year, 2 for
262             month, 3 for day). recent_years(), recent_months(), and recent_days() are
263             provided for convenience.
264              
265             =cut
266              
267             sub recent_by_depth {
268 9     9 1 18 my ($self) = shift;
269 9         17 my ($depth, $entry_count) = @_;
270              
271 9         30 my @entries;
272 9         37 for my $entry (reverse $self->dates_by_depth($depth)) {
273 27 50       46 last if scalar(@entries) == $entry_count;
274 27         46 push @entries, $entry;
275             }
276              
277 9         54 return @entries;
278             }
279              
280             =item all_years(), all_months(), all_days()
281              
282             Convenience wrappers for recent_by_depth().
283              
284             =cut
285              
286 0     0 0 0 sub recent_years { return $_[0]->recent_by_depth(1, $_[1]); }
287 0     0 0 0 sub recent_months { return $_[0]->recent_by_depth(2, $_[1]); }
288 9     9 0 30 sub recent_days { return $_[0]->recent_by_depth(3, $_[1]); }
289              
290             =item generate_date_hashes()
291              
292             Store hashes which map dated entries to their previous and next entries at the
293             same depth in the tree. That is, something like:
294              
295             %prev_dates = {
296             '2014' => '2013',
297             '2014/1' => '2013/12'
298             '2014/1/1' => '2013/12/30',
299             ...
300             }
301              
302             %next_dates = {
303             '2013' => '2014',
304             '2013/12' => '2014/1',
305             '2013/12/30' => '2014/1/1',
306             ...
307             }
308              
309             =cut
310              
311             sub generate_date_hashes {
312 11     11 1 24 my $self = shift;
313              
314 11         19 my %prev;
315              
316             # Depth 1 is years, 2 is months, 3 is days. Get lists for all:
317 11         27 for my $depth (1, 2, 3) {
318 33         98 my @dates = $self->dates_by_depth($depth);
319              
320 33         45 my $last_seen;
321 33         64 foreach my $current_date (@dates) {
322 121 100       166 if ($last_seen) {
323 88         133 $prev{$current_date} = $last_seen;
324             }
325 121         171 $last_seen = $current_date;
326             }
327             }
328              
329 11         70 $self->{prev_dates} = { %prev };
330 11         83 $self->{next_dates} = { reverse %prev };
331             }
332              
333             =item store_children
334              
335             Store hashes of arrayrefs which maps parents to their immediate children.
336              
337             =cut
338              
339             sub store_children {
340 11     11 1 23 my $self = shift;
341              
342 11         16 my %child_cache;
343              
344 11         31 for my $entry ($self->all()) {
345 473         686 my $dirname = $self->dirname($entry);
346 473   100     1069 $child_cache{$dirname} //= [ ];
347 473         462 push @{ $child_cache{$dirname} }, $entry;
  473         797  
348             }
349              
350 11         130 $self->{child_cache} = { %child_cache };
351             }
352              
353             =item parent($entry)
354              
355             Return an entry's parent, or undef if it's at the top level.
356              
357             =cut
358              
359             sub parent {
360 3     3 1 623 my $self = shift;
361 3         8 my ($entry) = @_;
362              
363             # Explode unless an entry actually exists in the archives:
364 3 100       7 croak("No such entry: $entry") unless $self->is_extant($entry);
365              
366 2         8 my (@components) = split '/', $entry;
367 2         4 pop @components;
368 2 100       54 if (@components) {
369 1         7 return join '/', @components;
370             }
371 1         3 return undef;
372             }
373              
374             =item children($entry)
375              
376             Return an entry's (immediate) children, if any.
377              
378             =cut
379              
380             sub children {
381 164     164 1 898 my $self = shift;
382 164         240 my ($entry) = @_;
383              
384             # Explode unless an entry actually exists in the archives:
385 164 50       255 croak("No such entry: $entry") unless $self->is_extant($entry);
386              
387 164 100       420 if (defined $self->{child_cache}{$entry}) {
388 133         192 return @{ $self->{child_cache}{$entry} };
  133         423  
389             }
390 31         72 return ();
391             }
392              
393             =item children_basenames($entry)
394              
395             Returns an entry's immediate children, but just basenames - not full paths.
396              
397             =cut
398              
399             sub children_basenames {
400 162     162 1 509 my $self = shift;
401 162         212 my ($entry) = @_;
402              
403 162         317 return map { $self->basename($_) } $self->children($entry);
  297         458  
404             }
405              
406             =item get_sub_entries($entry_loc)
407              
408             Returns "sub entries" based on the C regexp.
409              
410             =cut
411              
412             sub get_sub_entries {
413 161     161 1 587 my ($self, $entry) = @_;
414              
415             # index gets special treatment as the text body of an entry, rather
416             # than as a sub-entry:
417 161         304 my @subs = grep { m/$SUBENTRY_EXPR/ } $self->children_basenames($entry);
  293         1631  
418 161         307 return grep { ! $SUBENTRY_IGNORE{$_} } @subs;
  192         481  
419              
420             # return grep { ! $SUBENTRY_IGNORE{$_} }
421             # grep { m/$SUBENTRY_EXPR/ }
422             # $self->children_basenames($entry);
423             }
424              
425             =item previous($entry)
426              
427             Return the previous entry at the same depth for the given entry.
428              
429             =cut
430              
431             sub previous {
432 60     60 1 141 return $_[0]->{prev_dates}->{ $_[1] };
433             }
434              
435             =item next($entry)
436              
437             Return the next entry at the same depth for the given entry.
438              
439             =cut
440              
441             sub next {
442 60     60 1 656 return $_[0]->{next_dates}->{ $_[1] };
443             }
444              
445             =item by_prop($property)
446              
447             Return an array of any entries for the given property.
448              
449             =cut
450              
451             sub by_prop {
452 432     432 1 634 my ($self, $property) = @_;
453              
454 432         417 my @entries;
455 432 100       688 if (defined $self->{property_entries}{$property}) {
456 31         40 @entries = @{ $self->{property_entries}{$property} };
  31         82  
457             }
458              
459 432         596 return @entries;
460             }
461              
462             =item props_for($entry)
463              
464             Return an array of any properties for the given entry.
465              
466             =cut
467              
468             sub props_for {
469 216     216 1 382 my ($self, $entry) = @_;
470              
471 216         250 my @props;
472 216 100       481 if (defined $self->{entry_properties}{$entry}) {
473 54         68 @props = @{ $self->{entry_properties}{$entry} };
  54         130  
474             }
475              
476 216         461 return @props;
477             }
478              
479             =item has_prop($entry, $prop)
480              
481             Return 1 if the given entry has the given property.
482              
483             =cut
484              
485             sub has_prop {
486 13     13 1 65 my ($self, $entry, $prop) = @_;
487 13         53 my @props = grep { $_ eq $prop } $self->props_for($entry);
  2         7  
488 13         73 return (@props == 1);
489             }
490              
491             =item prop_value($entry, $prop)
492              
493             Return the value of given property, if it exists. Otherwise return undef.
494              
495             =cut
496              
497             sub prop_value {
498 2     2 1 1443 my ($self, $entry, $prop) = @_;
499 2 100       7 if ($self->has_prop($entry, $prop)) {
500             return file_get_contents(
501 1         14 $self->{entry_dir} . '/' . $entry . '/' . $prop . '.prop'
502             );
503             }
504 1         5 return undef;
505             }
506              
507             =item all_props()
508              
509             Return an array of all properties.
510              
511             =cut
512              
513             sub all_props {
514 2     2 1 4 my $self = shift;
515 2         4 return sort keys %{ $self->{property_entries} };
  2         15  
516             }
517              
518             =item is_extant($entry)
519              
520             Check if a given entry exists.
521              
522             =cut
523              
524             sub is_extant {
525 2710     2710 1 4316 my ($self, $entry) = @_;
526 2710         6366 return exists($self->{source_files}{$entry});
527             }
528              
529             =item is_dir($entry)
530              
531             Check if an entry is a directory.
532              
533             =cut
534              
535             sub is_dir {
536 277     277 1 714 my ($self, $entry) = @_;
537 277 50       405 croak("No such entry: $entry") unless $self->is_extant($entry);
538 277         755 return ($self->{source_files}{$entry} == $ENTRYTYPE_DIR);
539             }
540              
541             =item is_file($entry)
542              
543             Check if an entry is a flatfile.
544              
545             =cut
546              
547             sub is_file {
548 656     656 1 954 my ($self, $entry) = @_;
549 656 50       972 croak("No such entry: $entry") unless $self->is_extant($entry);
550 656         1965 return ($self->{source_files}{$entry} == $ENTRYTYPE_FILE);
551             }
552              
553             =item is_renderable($entry)
554              
555             Check if an entry path is, theoretically, renderable.
556              
557             =cut
558              
559             sub is_renderable {
560 72     72 1 106 my ($self, $entry) = @_;
561 72         459 return ($entry =~ $RENDERABLE_EXPR);
562             }
563              
564             =item has_index($entry)
565              
566             Check if an entry contains an index file.
567              
568             TODO: Should this care about the pathological (?) case where index is a
569             directory?
570              
571             =cut
572              
573             sub has_index {
574 222     222 1 320 my ($self, $entry) = @_;
575 222 50       342 croak("No such entry: $entry") unless $self->is_extant($entry);
576 222         485 return $self->is_extant($entry . '/index');
577             }
578              
579             =item basename($entry)
580              
581             Get a base name (i.e., filename without path) for a given entry.
582              
583             =cut
584              
585             sub basename {
586 370     370 1 491 my ($self, $entry) = @_;
587 370         784 my @parts = split '/', $entry;
588 370         911 return pop @parts;
589             }
590              
591             =item dirname($entry)
592              
593             Get a directory name (i.e., directory without filename) for a given entry.
594              
595             =cut
596              
597             sub dirname {
598 473     473 1 593 my ($self, $entry) = @_;
599 473         781 my @parts = split '/', $entry;
600 473         474 pop @parts;
601 473         819 return join '/', @parts;
602             }
603              
604             =back
605              
606             =cut
607              
608             1;