File Coverage

lib/App/Followme/BaseData.pm
Criterion Covered Total %
statement 194 205 94.6
branch 75 88 85.2
condition 13 14 92.8
subroutine 37 37 100.0
pod 3 28 10.7
total 322 372 86.5


line stmt bran cond sub pod time code
1             package App::Followme::BaseData;
2              
3 21     21   2622 use 5.008005;
  21         73  
4 21     21   115 use strict;
  21         41  
  21         538  
5 21     21   116 use warnings;
  21         44  
  21         595  
6 21     21   670 use integer;
  21         50  
  21         100  
7 21     21   1064 use lib '../..';
  21         698  
  21         111  
8              
9 21     21   146872 use base qw(App::Followme::ConfiguredObject);
  21         46  
  21         6062  
10 21     21   967 use App::Followme::FIO;
  21         46  
  21         54450  
11              
12             #----------------------------------------------------------------------
13             # Default values of parameters
14              
15             sub parameters {
16 372     372 1 652 my ($pkg) = @_;
17              
18             return (
19 372         1122 list_length => 5,
20             target_prefix => 'target',
21             );
22             }
23              
24             #----------------------------------------------------------------------
25             # Build a new variable value given its name and context
26              
27             sub build {
28 493     493 1 30759 my ($self, $variable_name, $item, $loop) = @_;
29              
30             # Extract the sigil from the variable name, if present
31 493         1240 my ($sigil, $name) = $self->split_name($variable_name);
32              
33             # Extract the sort field from the variable name
34 493         907 my ($data_field, $sort_field, $sort_reverse);
35 493         1429 ($data_field, $sort_field) = split('_by_', $name);
36 493 100       1123 if (defined $sort_field) {
37 56 100       1005 if ($sort_field =~ s/_reversed$//) {
38 20         44 $sort_reverse = 1;
39             } else {
40 36         53 $sort_reverse = 0;
41             }
42             }
43              
44 493         821 my %cache = ();
45 493 100       1046 if ($sigil eq '$') {
46 265 100 100     1832 if (defined $item &&
      100        
47             (! $self->{cache}{item} || $self->{cache}{item} ne $item)) {
48             # Clear cache when argument to build changes
49 66         186 %cache = (item => $item);
50             } else {
51 199         322 %cache = %{$self->{cache}};
  199         984  
52             }
53             }
54              
55             # Build the value associated with a name if it is not in the cache
56 493 100       1199 unless (exists $cache{$data_field}) {
57 411         1290 my %data = $self->fetch_data($data_field, $item, $loop);
58              
59 411         825 my $sorted_order = 0;
60 411         1074 my $sorted_data = $self->sort(\%data, $sort_field, $sort_reverse);
61 411         1028 $sorted_data = $self->format($sorted_order, $sorted_data);
62              
63 411         2067 %cache = (%cache, %$sorted_data);
64             }
65              
66             # Check the value for agreement with the sigil and return reference
67 493         1382 my $ref_value = $self->ref_value($cache{$data_field}, $sigil, $data_field);
68 493 100       1441 $self->{cache} = \%cache if $sigil eq '$';
69 493         8318 return $ref_value;
70             }
71              
72             #----------------------------------------------------------------------
73             # Coerce the data to a hash
74              
75             sub coerce_data {
76 707     707 0 3248 my ($self, $name, @data) = @_;
77              
78 707         1018 my %data;
79 707 100       1728 if (@data == 0) {
    100          
    50          
80 157         254 %data = ();
81              
82             } elsif (@data == 1) {
83 549         1451 %data = ($name => $data[0]);
84              
85             } elsif (@data % 2 == 0) {
86 1         4 %data = @data;
87              
88             } else {
89 0         0 my $pkg = ref $self;
90 0         0 die "$name does not return a hash\n";
91             }
92              
93 707         2284 return %data;
94             }
95              
96             #----------------------------------------------------------------------
97             # Fetch the data for building a variable's value
98              
99             sub fetch_data {
100 29     29 0 63 my ($self, $name, $item, $loop) = @_;
101              
102 29         62 my %data = $self->gather_data('get', $name, $item, $loop);
103 29         66 return %data;
104             }
105              
106             #----------------------------------------------------------------------
107             # Choose the file comparison routine that matches the configuration
108              
109             sub file_comparer {
110 159     159 0 308 my ($self, $sort_reverse) = @_;
111              
112 159         216 my $comparer;
113 159 100       347 if ($sort_reverse) {
114 54     56   220 $comparer = sub ($$) {$_[1]->[0] cmp $_[0]->[0]};
  56         123  
115             } else {
116 105     103   489 $comparer = sub ($$) {$_[0]->[0] cmp $_[1]->[0]};
  103         223  
117             }
118              
119 159         342 return $comparer;
120             }
121              
122             #----------------------------------------------------------------------
123             # If there is omly a single field containing data, return its name
124              
125             sub find_data_field {
126 412     412 0 754 my ($self, $data) = @_;
127              
128 412         931 my @keys = keys %$data;
129              
130 412         602 my $field;
131 412 100       1048 if (@keys == 1 ) {
132 302         491 my $key = $keys[0];
133 302 100       811 if (ref $data->{$key} eq 'ARRAY') {
134 125         260 $field = $key;
135             }
136             }
137              
138 412         892 return $field;
139             }
140              
141             #----------------------------------------------------------------------
142             # Find the values to sort by and format them so they are in sort order
143              
144             sub find_sort_column {
145 142     142 0 301 my ($self, $data_column, $sort_field) = @_;
146            
147 142         342 my $formatter = "format_$sort_field";
148 142 100       701 $formatter = "format_nothing" unless $self->can($formatter);
149              
150 142         246 my @sort_column;
151 142         240 my $sorted_order = 1;
152              
153 142         292 for my $data_item (@$data_column) {
154 218         623 my %data = $self->fetch_data($sort_field, $data_item, $data_column);
155              
156 218 50       522 if (exists $data{$sort_field}) {
157             push(@sort_column, $self->$formatter($sorted_order,
158 218         724 $data{$sort_field}));
159             } else {
160 0         0 warn "Sort field not found: $sort_field";
161 0         0 push(@sort_column, $data_item);
162             }
163            
164             }
165              
166 142         446 return \@sort_column;
167             }
168              
169             #----------------------------------------------------------------------
170             # Find the target, return the target plus an offset
171              
172             sub find_target {
173 30     30 0 56 my ($self, $offset, $item, $loop) = @_;
174 30 50       70 die "Can't use \$target_* outside of for\n" unless $loop;
175              
176 30         44 my $match = -999;
177 30         79 foreach my $i (0 .. @$loop) {
178 60 100       125 if ($loop->[$i] eq $item) {
179 30         46 $match = $i;
180 30         58 last;
181             }
182             }
183              
184 30         69 my $index = $match + $offset + 1;
185 30 100 100     156 $index = 0 if $index < 1 || $index > @$loop;
186 30 100       133 return $index ? $self->{target_prefix} . $index : '';
187             }
188              
189             #----------------------------------------------------------------------
190             # Apply an optional format to the data
191              
192             sub format {
193 414     414 0 5478 my ($self, $sorted_order, $sorted_data) = @_;
194              
195 414         1068 foreach my $name (keys %$sorted_data) {
196 829 100       1779 next unless $sorted_data->{$name};
197              
198 786         1635 my $formatter = join('_', 'format', $name);
199 786 100       3770 if ($self->can($formatter)) {
200 200 50       565 if (ref $sorted_data->{$name} eq 'ARRAY') {
    50          
201 0         0 for my $value (@{$sorted_data->{$name}}) {
  0         0  
202 0         0 $value = $self->$formatter($sorted_order,
203             $value);
204             }
205              
206             } elsif (ref $sorted_data->{$name} eq 'HASH') {
207 0         0 die("Illegal data format for build: $name");
208              
209             } else {
210             $sorted_data->{$name} =
211 200         618 $self->$formatter($sorted_order, $sorted_data->{$name});
212             }
213             }
214             }
215              
216 414         862 return $sorted_data;
217             }
218              
219             #----------------------------------------------------------------------
220             # Don't format anything
221              
222             sub format_nothing {
223 7     7 0 13 my ($self, $sorted_order, $value) = @_;
224 7         16 return $value;
225             }
226              
227             #----------------------------------------------------------------------
228             # Gather the data for building a variable's value
229              
230             sub gather_data {
231 704     704 0 1550 my ($self, $method, $name, $item, $loop) = @_;
232              
233 704         1006 my @data;
234 704         1451 $method = join('_', $method, $name);
235              
236 704 100       3054 if ($self->can($method)) {
237 548         1624 @data = $self->$method($item, $loop);
238              
239             } else {
240 156         289 @data = ();
241             }
242              
243 704         1833 my %data = $self->coerce_data($name, @data);
244 704         2216 return %data;
245             }
246              
247             #----------------------------------------------------------------------
248             # Get the count of the item in the list
249              
250             sub get_count {
251 3     3 0 7 my ($self, $item, $loop) = @_;
252 3 50       8 die "Can't use \$count outside of for\n" unless $loop;
253              
254 3         7 foreach my $i (0 .. @$loop) {
255 6 100       14 if ($loop->[$i] eq $item) {
256 3         6 my $count = $i + 1;
257 3         7 return $count;
258             }
259             }
260              
261 0         0 return;
262             }
263              
264             #----------------------------------------------------------------------
265             # Is this the first item in the list?
266              
267             sub get_is_first {
268 3     3 0 5 my ($self, $item, $loop) = @_;
269              
270 3 50       8 die "Can't use \$is_first outside of for\n" unless $loop;
271 3 100       9 return $loop->[0] eq $item ? 1 : 0;
272             }
273              
274             #----------------------------------------------------------------------
275             # Is this the last item in the list?
276              
277             sub get_is_last {
278 3     3 0 6 my ($self, $item, $loop) = @_;
279              
280 3 50       7 die "Can't use \$is_last outside of for\n" unless $loop;
281 3 100       21 return $loop->[-1] eq $item ? 1 : 0;
282             }
283              
284             #----------------------------------------------------------------------
285             # Return the current list of loop items
286              
287             sub get_loop {
288 1     1 0 3 my ($self, $item, $loop) = @_;
289              
290 1 50       2 die "Can't use \@loop outside of for\n" unless $loop;
291 1         3 return $loop;
292             }
293              
294             #----------------------------------------------------------------------
295             # Return the name of the current item in a loop
296              
297             sub get_name {
298 144     144 0 283 my ($self, $item) = @_;
299 144         292 return $item;
300             }
301              
302             #----------------------------------------------------------------------
303             # Get the current target
304              
305             sub get_target {
306 12     12 0 3436 my ($self, $item, $loop) = @_;
307 12         36 return $self->find_target(0, $item, $loop);
308             }
309              
310             #----------------------------------------------------------------------
311             # Get the next target
312              
313             sub get_target_next {
314 9     9 0 2371 my ($self, $item, $loop) = @_;
315 9         30 return $self->find_target(1, $item, $loop);
316             }
317              
318             #----------------------------------------------------------------------
319             # Get the previous target
320              
321             sub get_target_previous {
322 9     9 0 2350 my ($self, $item, $loop) = @_;
323 9         22 return $self->find_target(-1, $item, $loop);
324             }
325              
326              
327             #----------------------------------------------------------------------
328             # Augment the array to be sorted with the column to sort it by
329             sub make_augmented {
330 142     142 0 326 my ($self, $sort_column, $data_column) = @_;
331              
332 142         206 my @augmented_list;
333 142         434 for (my $i = 0; $i < @$sort_column; $i++) {
334 218         682 push(@augmented_list, [$sort_column->[$i], $data_column->[$i]]);
335             }
336              
337 142         470 return @augmented_list;
338             }
339              
340             #----------------------------------------------------------------------
341             # Merge two sorted lists of augmented filenames
342              
343             sub merge_augmented {
344 17     17 0 33 my ($self, $list1, $list2) = @_;
345              
346 17         29 my @merged_list = ();
347 17         33 my $sort_reverse = 1;
348 17         31 my $comparer = $self->file_comparer($sort_reverse);
349              
350 17   66     59 while(@$list1 && @$list2) {
351 3 100       12 last if @merged_list == $self->{list_length};
352 2 50       7 if ($comparer->($list1->[0], $list2->[0]) > 0) {
353 2         8 push(@merged_list, shift @$list2);
354             } else {
355 0         0 push(@merged_list, shift @$list1);
356             }
357             }
358              
359 17         53 while (@$list1) {
360 1 50       3 last if @merged_list == $self->{list_length};
361 0         0 push(@merged_list, shift @$list1);
362             }
363              
364 17         43 while (@$list2) {
365 20 100       48 last if @merged_list == $self->{list_length};
366 17         41 push(@merged_list, shift @$list2);
367             }
368              
369 17         86 return \@merged_list;
370             }
371              
372             #----------------------------------------------------------------------
373             # Get a reference value and check it for agreement with the sigil
374              
375             sub ref_value {
376 496     496 0 3743 my ($self, $value, $sigil, $data_field) = @_;
377              
378 496         808 my ($check, $ref_value);
379 496 100       1016 $value = '' unless defined $value;
380              
381 496 100       1272 if ($sigil eq '$'){
    100          
    50          
382 268 100       511 if (ref $value ne 'SCALAR') {
383             # Convert data structures for inclusion in template
384 267         685 $value = fio_flatten($value);
385 267         480 $ref_value = \$value;
386             } else {
387 1         2 $ref_value = $value;
388             }
389 268         538 $check = ref $ref_value eq 'SCALAR';
390              
391             } elsif ($sigil eq '@') {
392 44         68 $ref_value = $value;
393 44         102 $check = ref $ref_value eq 'ARRAY';
394              
395             } elsif ($sigil eq '') {
396 184 100       435 $ref_value = ref $value ? $value : \$value;
397 184         292 $check = 1;
398             }
399              
400 496 50       951 die "Unknown variable: $sigil$data_field\n" unless $check;
401 496         884 return $ref_value;
402             }
403              
404             #----------------------------------------------------------------------
405             # Set up the cache for data
406              
407             sub setup {
408 93     93 1 197 my ($self) = @_;
409              
410 93         259 $self->{cache} = {};
411             }
412              
413             #----------------------------------------------------------------------
414             # Sort the data if it is in an array
415              
416             sub sort {
417 412     412 0 1479 my ($self, $data, $sort_field, $sort_reverse) = @_;
418              
419 412         575 my $sorted_data;
420 412         962 my $data_field = $self->find_data_field($data);
421              
422 412 100       794 if ($data_field) {
423 125         390 my @augmented_data = $self->sort_with_field($data->{$data_field},
424             $sort_field,
425             $sort_reverse);
426              
427 125         438 my @stripped_data = $self->strip_augmented(@augmented_data);
428 125         407 $sorted_data = {$data_field => \@stripped_data};
429              
430             } else {
431 287         476 $sorted_data = $data;
432             }
433              
434 412         790 return $sorted_data;
435             }
436              
437             #----------------------------------------------------------------------
438             # Sort augmented list by swartzian transform
439              
440             sub sort_augmented {
441 142     142 0 337 my ($self, $sort_reverse, @augmented_data) = @_;
442              
443 142         385 my $comparer = $self->file_comparer($sort_reverse);
444 142         393 @augmented_data = sort $comparer @augmented_data;
445 142         714 return @augmented_data;
446             }
447              
448             #----------------------------------------------------------------------
449             # Sort data retaining the field you sort with
450              
451             sub sort_with_field {
452 142     142 0 321 my ($self, $data_column, $sort_field, $sort_reverse) = @_;
453 142 100       377 $sort_field = 'name' unless defined $sort_field;
454 142 100       317 $sort_reverse = 0 unless defined $sort_reverse;
455              
456 142         789 my $sort_column = $self->find_sort_column($data_column, $sort_field);
457              
458 142         570 return $self->sort_augmented($sort_reverse,
459             $self->make_augmented($sort_column, $data_column));
460             }
461              
462             #----------------------------------------------------------------------
463             # Return the filenames from an augmented set of files
464              
465             sub strip_augmented {
466 134     134 0 285 my $self = shift @_;
467 134         258 return map {$_->[1]} @_;
  209         498  
468             }
469              
470             #----------------------------------------------------------------------
471             # Split the sigil off from the variable name from a template
472              
473             sub split_name {
474 496     496 0 4673 my ($self, $variable_name) = @_;
475              
476 496         800 my $name = $variable_name;
477 496         1553 $name =~ s/^([\$\@])//;
478 496   100     1905 my $sigil = $1 || '';
479              
480 496         1410 return ($sigil, $name);
481             }
482              
483             1;
484              
485             =pod
486              
487             =encoding utf-8
488              
489             =head1 NAME
490              
491             App::Followme::BaseData
492              
493             =head1 SYNOPSIS
494              
495             use App::Followme::BaseData;
496             my $meta = App::Followme::BaseData->new();
497             my %data = $meta->build($name, $filename);
498              
499             =head1 DESCRIPTION
500              
501             This module is the base class for all metadata classes and provides the build
502             method used to interface metadata classes with the App::Followme::Template
503             class.
504              
505             Followme uses templates to construct web pages. These templates contain
506             variables whose values are computed by calling the build method of the metadata
507             object, which is passed as an argument to the template function. The build
508             method returns either a reference to a scalar or list. The names correspond to
509             the variable names in the template. This class contains the build method, which
510             couples the variable name to the metadata object method that computes the value
511             of the variable.
512              
513             =head1 METHODS
514              
515             There is only one public method, build.
516              
517             =over 4
518              
519             =item my %data = $meta->build($name, $filename);
520              
521             Build a variable's value. The first argument is the name of the variable
522             to be built. The second argument is the filename the variable is computed for.
523             If the variable returned is a list of files, this variable should be left
524             undefined.
525              
526             =back
527              
528             =head1 VARIABLES
529              
530             The base metadata class can evaluate the following variables. When passing
531             a name to the build method, the sigil should not be used. All these variables
532             can only be used inside a for block.
533              
534             =over 4
535              
536             =item @loop
537              
538             A list with all the loop items from the immediately enclosing for block.
539              
540             =item $count
541              
542             The count of the current item in the for block.The count starts at one.
543              
544             =item $is_first
545              
546             One if this is the first item in the for block, zero otherwise.
547              
548             =item $is_last
549              
550             One if this is the last item in the for block, zero otherwise
551              
552             =item $name
553              
554             The name of the current item in the for block.
555              
556             =item $target
557              
558             A string that can be used as a target for the location of the current item
559             in the page.
560              
561             =item $target_next
562              
563             A string that can be used as a target for the location of the next item
564             in the page. Empty if there is no next item.
565              
566             =item $target_previous
567              
568             A string that can be used as a target for the location of the previous item
569             in the page. Empty if there is no previous item.
570              
571             =back
572              
573             =head1 CONFIGURATION
574              
575             There are two parameters:
576              
577             =over 4
578              
579             =item list_length
580              
581             This determines the number of filenames in a merged list. The default
582             value of this parameter is 5
583              
584             =item target_prefix
585              
586             The prefix used to build the target names. The default value is 'target'.
587              
588             =back
589              
590             =head1 LICENSE
591              
592             Copyright (C) Bernie Simon.
593              
594             This library is free software; you can redistribute it and/or modify
595             it under the same terms as Perl itself.
596              
597             =head1 AUTHOR
598              
599             Bernie Simon E<lt>bernie.simon@gmail.comE<gt>
600              
601             =cut