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