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 20     20   2248 use 5.008005;
  20         80  
4 20     20   114 use strict;
  20         52  
  20         447  
5 20     20   99 use warnings;
  20         48  
  20         583  
6 20     20   716 use integer;
  20         66  
  20         120  
7 20     20   1214 use lib '../..';
  20         718  
  20         107  
8              
9 20     20   2818 use base qw(App::Followme::ConfiguredObject);
  20         40  
  20         5224  
10 20     20   642 use App::Followme::FIO;
  20         41  
  20         52995  
11              
12             #----------------------------------------------------------------------
13             # Default values of parameters
14              
15             sub parameters {
16 368     368 1 621 my ($pkg) = @_;
17              
18             return (
19 368         1081 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 495     495 1 30735 my ($self, $variable_name, $item, $loop) = @_;
29              
30             # Extract the sigil from the variable name, if present
31 495         1322 my ($sigil, $name) = $self->split_name($variable_name);
32              
33             # Extract the sort field from the variable name
34 495         926 my ($data_field, $sort_field, $sort_reverse);
35 495         1526 ($data_field, $sort_field) = split('_by_', $name);
36 495 100       1199 if (defined $sort_field) {
37 54 100       190 if ($sort_field =~ s/_reversed$//) {
38 20         37 $sort_reverse = 1;
39             } else {
40 34         59 $sort_reverse = 0;
41             }
42             }
43              
44 495         840 my %cache = ();
45 495 100       1041 if ($sigil eq '$') {
46 260 100 100     1399 if (defined $item &&
      100        
47             (! $self->{cache}{item} || $self->{cache}{item} ne $item)) {
48             # Clear cache when argument to build changes
49 64         186 %cache = (item => $item);
50             } else {
51 196         322 %cache = %{$self->{cache}};
  196         967  
52             }
53             }
54              
55             # Build the value associated with a name if it is not in the cache
56 495 100       1259 unless (exists $cache{$data_field}) {
57 412         1306 my %data = $self->fetch_data($data_field, $item, $loop);
58              
59 412         837 my $sorted_order = 0;
60 412         1170 my $sorted_data = $self->sort(\%data, $sort_field, $sort_reverse);
61 412         1103 $sorted_data = $self->format($sorted_order, $sorted_data);
62              
63 412         2099 %cache = (%cache, %$sorted_data);
64             }
65              
66             # Check the value for agreement with the sigil and return reference
67 495         1585 my $ref_value = $self->ref_value($cache{$data_field}, $sigil, $data_field);
68 495 100       1549 $self->{cache} = \%cache if $sigil eq '$';
69 495         8386 return $ref_value;
70             }
71              
72             #----------------------------------------------------------------------
73             # Coerce the data to a hash
74              
75             sub coerce_data {
76 698     698 0 3359 my ($self, $name, @data) = @_;
77              
78 698         1006 my %data;
79 698 100       1773 if (@data == 0) {
    100          
    50          
80 146         247 %data = ();
81              
82             } elsif (@data == 1) {
83 551         1409 %data = ($name => $data[0]);
84              
85             } elsif (@data % 2 == 0) {
86 1         3 %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 698         2252 return %data;
94             }
95              
96             #----------------------------------------------------------------------
97             # Fetch the data for building a variable's value
98              
99             sub fetch_data {
100 29     29 0 53 my ($self, $name, $item, $loop) = @_;
101              
102 29         61 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 170     170 0 324 my ($self, $sort_reverse) = @_;
111              
112 170         596 my $comparer;
113 170 100       346 if ($sort_reverse) {
114 58     53   228 $comparer = sub ($$) {$_[1]->[0] cmp $_[0]->[0]};
  53         130  
115             } else {
116 112     99   508 $comparer = sub ($$) {$_[0]->[0] cmp $_[1]->[0]};
  99         215  
117             }
118              
119 170         381 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 413     413 0 735 my ($self, $data) = @_;
127              
128 413         977 my @keys = keys %$data;
129              
130 413         611 my $field;
131 413 100       959 if (@keys == 1 ) {
132 308         507 my $key = $keys[0];
133 308 100       860 if (ref $data->{$key} eq 'ARRAY') {
134 132         280 $field = $key;
135             }
136             }
137              
138 413         908 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 151     151 0 306 my ($self, $data_column, $sort_field) = @_;
146            
147 151         323 my $formatter = "format_$sort_field";
148 151 100       730 $formatter = "format_nothing" unless $self->can($formatter);
149              
150 151         256 my @sort_column;
151 151         238 my $sorted_order = 1;
152              
153 151         322 for my $data_item (@$data_column) {
154 214         640 my %data = $self->fetch_data($sort_field, $data_item, $data_column);
155              
156 214 50       486 if (exists $data{$sort_field}) {
157             push(@sort_column, $self->$formatter($sorted_order,
158 214         646 $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 151         429 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 55 my ($self, $offset, $item, $loop) = @_;
174 30 50       68 die "Can't use \$target_* outside of for\n" unless $loop;
175              
176 30         45 my $match = -999;
177 30         80 foreach my $i (0 .. @$loop) {
178 60 100       125 if ($loop->[$i] eq $item) {
179 30         42 $match = $i;
180 30         51 last;
181             }
182             }
183              
184 30         56 my $index = $match + $offset + 1;
185 30 100 100     119 $index = 0 if $index < 1 || $index > @$loop;
186 30 100       119 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 4274 my ($self, $sorted_order, $sorted_data) = @_;
194              
195 414         1135 foreach my $name (keys %$sorted_data) {
196 813 100       1839 next unless $sorted_data->{$name};
197              
198 773         1595 my $formatter = join('_', 'format', $name);
199 773 100       3560 if ($self->can($formatter)) {
200 198 50       581 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 198         738 $self->$formatter($sorted_order, $sorted_data->{$name});
212             }
213             }
214             }
215              
216 414         904 return $sorted_data;
217             }
218              
219             #----------------------------------------------------------------------
220             # Don't format anything
221              
222             sub format_nothing {
223 7     7 0 12 my ($self, $sorted_order, $value) = @_;
224 7         18 return $value;
225             }
226              
227             #----------------------------------------------------------------------
228             # Gather the data for building a variable's value
229              
230             sub gather_data {
231 695     695 0 1549 my ($self, $method, $name, $item, $loop) = @_;
232              
233 695         1048 my @data;
234 695         1465 $method = join('_', $method, $name);
235              
236 695 100       3301 if ($self->can($method)) {
237 550         1700 @data = $self->$method($item, $loop);
238              
239             } else {
240 145         262 @data = ();
241             }
242              
243 695         1858 my %data = $self->coerce_data($name, @data);
244 695         2225 return %data;
245             }
246              
247             #----------------------------------------------------------------------
248             # Get the count of the item in the list
249              
250             sub get_count {
251 3     3 0 6 my ($self, $item, $loop) = @_;
252 3 50       7 die "Can't use \$count outside of for\n" unless $loop;
253              
254 3         7 foreach my $i (0 .. @$loop) {
255 6 100       15 if ($loop->[$i] eq $item) {
256 3         3 my $count = $i + 1;
257 3         9 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 6 my ($self, $item, $loop) = @_;
269              
270 3 50       8 die "Can't use \$is_first outside of for\n" unless $loop;
271 3 100       10 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       10 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 2 my ($self, $item, $loop) = @_;
289              
290 1 50       3 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 149     149 0 287 my ($self, $item) = @_;
299 149         313 return $item;
300             }
301              
302             #----------------------------------------------------------------------
303             # Get the current target
304              
305             sub get_target {
306 12     12 0 2443 my ($self, $item, $loop) = @_;
307 12         37 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 2173 my ($self, $item, $loop) = @_;
315 9         26 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 1930 my ($self, $item, $loop) = @_;
323 9         27 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 151     151 0 358 my ($self, $sort_column, $data_column) = @_;
331              
332 151         258 my @augmented_list;
333 151         470 for (my $i = 0; $i < @$sort_column; $i++) {
334 214         708 push(@augmented_list, [$sort_column->[$i], $data_column->[$i]]);
335             }
336              
337 151         482 return @augmented_list;
338             }
339              
340             #----------------------------------------------------------------------
341             # Merge two sorted lists of augmented filenames
342              
343             sub merge_augmented {
344 19     19 0 40 my ($self, $list1, $list2) = @_;
345              
346 19         41 my @merged_list = ();
347 19         30 my $sort_reverse = 1;
348 19         37 my $comparer = $self->file_comparer($sort_reverse);
349              
350 19   66     66 while(@$list1 && @$list2) {
351 3 100       10 last if @merged_list == $self->{list_length};
352 2 50       4 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 19         59 while (@$list1) {
360 1 50       8 last if @merged_list == $self->{list_length};
361 0         0 push(@merged_list, shift @$list1);
362             }
363              
364 19         56 while (@$list2) {
365 20 100       60 last if @merged_list == $self->{list_length};
366 17         46 push(@merged_list, shift @$list2);
367             }
368              
369 19         90 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 498     498 0 3646 my ($self, $value, $sigil, $data_field) = @_;
377              
378 498         793 my ($check, $ref_value);
379 498 100       1076 $value = '' unless defined $value;
380              
381 498 100       1458 if ($sigil eq '$'){
    100          
    50          
382 263 100       485 if (ref $value ne 'SCALAR') {
383             # Convert data structures for inclusion in template
384 262         760 $value = fio_flatten($value);
385 262         462 $ref_value = \$value;
386             } else {
387 1         2 $ref_value = $value;
388             }
389 263         529 $check = ref $ref_value eq 'SCALAR';
390              
391             } elsif ($sigil eq '@') {
392 45         80 $ref_value = $value;
393 45         104 $check = ref $ref_value eq 'ARRAY';
394              
395             } elsif ($sigil eq '') {
396 190 100       435 $ref_value = ref $value ? $value : \$value;
397 190         326 $check = 1;
398             }
399              
400 498 50       1018 die "Unknown variable: $sigil$data_field\n" unless $check;
401 498         906 return $ref_value;
402             }
403              
404             #----------------------------------------------------------------------
405             # Set up the cache for data
406              
407             sub setup {
408 92     92 1 190 my ($self) = @_;
409              
410 92         267 $self->{cache} = {};
411             }
412              
413             #----------------------------------------------------------------------
414             # Sort the data if it is in an array
415              
416             sub sort {
417 413     413 0 1421 my ($self, $data, $sort_field, $sort_reverse) = @_;
418              
419 413         599 my $sorted_data;
420 413         936 my $data_field = $self->find_data_field($data);
421              
422 413 100       856 if ($data_field) {
423 132         384 my @augmented_data = $self->sort_with_field($data->{$data_field},
424             $sort_field,
425             $sort_reverse);
426              
427 132         386 my @stripped_data = $self->strip_augmented(@augmented_data);
428 132         439 $sorted_data = {$data_field => \@stripped_data};
429              
430             } else {
431 281         437 $sorted_data = $data;
432             }
433              
434 413         905 return $sorted_data;
435             }
436              
437             #----------------------------------------------------------------------
438             # Sort augmented list by swartzian transform
439              
440             sub sort_augmented {
441 151     151 0 369 my ($self, $sort_reverse, @augmented_data) = @_;
442              
443 151         417 my $comparer = $self->file_comparer($sort_reverse);
444 151         419 @augmented_data = sort $comparer @augmented_data;
445 151         756 return @augmented_data;
446             }
447              
448             #----------------------------------------------------------------------
449             # Sort data retaining the field you sort with
450              
451             sub sort_with_field {
452 151     151 0 313 my ($self, $data_column, $sort_field, $sort_reverse) = @_;
453 151 100       404 $sort_field = 'name' unless defined $sort_field;
454 151 100       312 $sort_reverse = 0 unless defined $sort_reverse;
455              
456 151         495 my $sort_column = $self->find_sort_column($data_column, $sort_field);
457              
458 151         583 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 142     142 0 290 my $self = shift @_;
467 142         276 return map {$_->[1]} @_;
  205         771  
468             }
469              
470             #----------------------------------------------------------------------
471             # Split the sigil off from the variable name from a template
472              
473             sub split_name {
474 498     498 0 4684 my ($self, $variable_name) = @_;
475              
476 498         799 my $name = $variable_name;
477 498         1648 $name =~ s/^([\$\@])//;
478 498   100     1977 my $sigil = $1 || '';
479              
480 498         2579 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