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   2287 use 5.008005;
  20         84  
4 20     20   112 use strict;
  20         44  
  20         502  
5 20     20   110 use warnings;
  20         47  
  20         564  
6 20     20   732 use integer;
  20         53  
  20         116  
7 20     20   1139 use lib '../..';
  20         680  
  20         94  
8              
9 20     20   2654 use base qw(App::Followme::ConfiguredObject);
  20         41  
  20         5023  
10 20     20   669 use App::Followme::FIO;
  20         47  
  20         52141  
11              
12             #----------------------------------------------------------------------
13             # Default values of parameters
14              
15             sub parameters {
16 368     368 1 631 my ($pkg) = @_;
17              
18             return (
19 368         1126 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 510     510 1 28375 my ($self, $variable_name, $item, $loop) = @_;
29              
30             # Extract the sigil from the variable name, if present
31 510         1308 my ($sigil, $name) = $self->split_name($variable_name);
32              
33             # Extract the sort field from the variable name
34 510         916 my ($data_field, $sort_field, $sort_reverse);
35 510         1524 ($data_field, $sort_field) = split('_by_', $name);
36 510 100       1237 if (defined $sort_field) {
37 56 100       185 if ($sort_field =~ s/_reversed$//) {
38 20         46 $sort_reverse = 1;
39             } else {
40 36         51 $sort_reverse = 0;
41             }
42             }
43              
44 510         848 my %cache = ();
45 510 100       1150 if ($sigil eq '$') {
46 273 100 100     1514 if (defined $item &&
      100        
47             (! $self->{cache}{item} || $self->{cache}{item} ne $item)) {
48             # Clear cache when argument to build changes
49 68         222 %cache = (item => $item);
50             } else {
51 205         343 %cache = %{$self->{cache}};
  205         992  
52             }
53             }
54              
55             # Build the value associated with a name if it is not in the cache
56 510 100       1338 unless (exists $cache{$data_field}) {
57 423         1318 my %data = $self->fetch_data($data_field, $item, $loop);
58              
59 423         867 my $sorted_order = 0;
60 423         1136 my $sorted_data = $self->sort(\%data, $sort_field, $sort_reverse);
61 423         1152 $sorted_data = $self->format($sorted_order, $sorted_data);
62              
63 423         2135 %cache = (%cache, %$sorted_data);
64             }
65              
66             # Check the value for agreement with the sigil and return reference
67 510         1472 my $ref_value = $self->ref_value($cache{$data_field}, $sigil, $data_field);
68 510 100       1583 $self->{cache} = \%cache if $sigil eq '$';
69 510         9731 return $ref_value;
70             }
71              
72             #----------------------------------------------------------------------
73             # Coerce the data to a hash
74              
75             sub coerce_data {
76 715     715 0 3263 my ($self, $name, @data) = @_;
77              
78 715         1037 my %data;
79 715 100       1833 if (@data == 0) {
    100          
    50          
80 156         271 %data = ();
81              
82             } elsif (@data == 1) {
83 558         1420 %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 715         2312 return %data;
94             }
95              
96             #----------------------------------------------------------------------
97             # Fetch the data for building a variable's value
98              
99             sub fetch_data {
100 29     29 0 60 my ($self, $name, $item, $loop) = @_;
101              
102 29         60 my %data = $self->gather_data('get', $name, $item, $loop);
103 29         112 return %data;
104             }
105              
106             #----------------------------------------------------------------------
107             # Choose the file comparison routine that matches the configuration
108              
109             sub file_comparer {
110 172     172 0 339 my ($self, $sort_reverse) = @_;
111              
112 172         241 my $comparer;
113 172 100       330 if ($sort_reverse) {
114 58     53   235 $comparer = sub ($$) {$_[1]->[0] cmp $_[0]->[0]};
  53         135  
115             } else {
116 114     105   516 $comparer = sub ($$) {$_[0]->[0] cmp $_[1]->[0]};
  105         223  
117             }
118              
119 172         369 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 424     424 0 724 my ($self, $data) = @_;
127              
128 424         965 my @keys = keys %$data;
129              
130 424         595 my $field;
131 424 100       985 if (@keys == 1 ) {
132 315         495 my $key = $keys[0];
133 315 100       865 if (ref $data->{$key} eq 'ARRAY') {
134 134         244 $field = $key;
135             }
136             }
137              
138 424         954 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 153     153 0 307 my ($self, $data_column, $sort_field) = @_;
146            
147 153         416 my $formatter = "format_$sort_field";
148 153 100       721 $formatter = "format_nothing" unless $self->can($formatter);
149              
150 153         266 my @sort_column;
151 153         231 my $sorted_order = 1;
152              
153 153         321 for my $data_item (@$data_column) {
154 220         625 my %data = $self->fetch_data($sort_field, $data_item, $data_column);
155              
156 220 50       513 if (exists $data{$sort_field}) {
157             push(@sort_column, $self->$formatter($sorted_order,
158 220         697 $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 153         450 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 58 my ($self, $offset, $item, $loop) = @_;
174 30 50       67 die "Can't use \$target_* outside of for\n" unless $loop;
175              
176 30         46 my $match = -999;
177 30         79 foreach my $i (0 .. @$loop) {
178 60 100       125 if ($loop->[$i] eq $item) {
179 30         44 $match = $i;
180 30         54 last;
181             }
182             }
183              
184 30         53 my $index = $match + $offset + 1;
185 30 100 100     114 $index = 0 if $index < 1 || $index > @$loop;
186 30 100       122 return $index ? $self->{target_prefix} . $index : '';
187             }
188              
189             #----------------------------------------------------------------------
190             # Apply an optional format to the data
191              
192             sub format {
193 425     425 0 4337 my ($self, $sorted_order, $sorted_data) = @_;
194              
195 425         1160 foreach my $name (keys %$sorted_data) {
196 848 100       1777 next unless $sorted_data->{$name};
197              
198 804         1681 my $formatter = join('_', 'format', $name);
199 804 100       3408 if ($self->can($formatter)) {
200 207 50       552 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 207         672 $self->$formatter($sorted_order, $sorted_data->{$name});
212             }
213             }
214             }
215              
216 425         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 712     712 0 1510 my ($self, $method, $name, $item, $loop) = @_;
232              
233 712         1125 my @data;
234 712         1486 $method = join('_', $method, $name);
235              
236 712 100       3208 if ($self->can($method)) {
237 557         1582 @data = $self->$method($item, $loop);
238              
239             } else {
240 155         283 @data = ();
241             }
242              
243 712         1940 my %data = $self->coerce_data($name, @data);
244 712         2286 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         10 foreach my $i (0 .. @$loop) {
255 6 100       13 if ($loop->[$i] eq $item) {
256 3         7 my $count = $i + 1;
257 3         8 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 7 my ($self, $item, $loop) = @_;
269              
270 3 50       7 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 5 my ($self, $item, $loop) = @_;
279              
280 3 50       8 die "Can't use \$is_last outside of for\n" unless $loop;
281 3 100       8 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       4 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 286 my ($self, $item) = @_;
299 149         285 return $item;
300             }
301              
302             #----------------------------------------------------------------------
303             # Get the current target
304              
305             sub get_target {
306 12     12 0 2379 my ($self, $item, $loop) = @_;
307 12         38 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 2071 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 1929 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 153     153 0 348 my ($self, $sort_column, $data_column) = @_;
331              
332 153         271 my @augmented_list;
333 153         479 for (my $i = 0; $i < @$sort_column; $i++) {
334 220         697 push(@augmented_list, [$sort_column->[$i], $data_column->[$i]]);
335             }
336              
337 153         490 return @augmented_list;
338             }
339              
340             #----------------------------------------------------------------------
341             # Merge two sorted lists of augmented filenames
342              
343             sub merge_augmented {
344 19     19 0 42 my ($self, $list1, $list2) = @_;
345              
346 19         37 my @merged_list = ();
347 19         37 my $sort_reverse = 1;
348 19         37 my $comparer = $self->file_comparer($sort_reverse);
349              
350 19   66     130 while(@$list1 && @$list2) {
351 3 100       11 last if @merged_list == $self->{list_length};
352 2 50       5 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         50 while (@$list1) {
360 1 50       23 last if @merged_list == $self->{list_length};
361 0         0 push(@merged_list, shift @$list1);
362             }
363              
364 19         43 while (@$list2) {
365 20 100       45 last if @merged_list == $self->{list_length};
366 17         43 push(@merged_list, shift @$list2);
367             }
368              
369 19         91 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 513     513 0 3706 my ($self, $value, $sigil, $data_field) = @_;
377              
378 513         772 my ($check, $ref_value);
379 513 100       1063 $value = '' unless defined $value;
380              
381 513 100       1338 if ($sigil eq '$'){
    100          
    50          
382 276 100       509 if (ref $value ne 'SCALAR') {
383             # Convert data structures for inclusion in template
384 275         774 $value = fio_flatten($value);
385 275         474 $ref_value = \$value;
386             } else {
387 1         3 $ref_value = $value;
388             }
389 276         560 $check = ref $ref_value eq 'SCALAR';
390              
391             } elsif ($sigil eq '@') {
392 47         85 $ref_value = $value;
393 47         118 $check = ref $ref_value eq 'ARRAY';
394              
395             } elsif ($sigil eq '') {
396 190 100       398 $ref_value = ref $value ? $value : \$value;
397 190         319 $check = 1;
398             }
399              
400 513 50       1138 die "Unknown variable: $sigil$data_field\n" unless $check;
401 513         919 return $ref_value;
402             }
403              
404             #----------------------------------------------------------------------
405             # Set up the cache for data
406              
407             sub setup {
408 92     92 1 197 my ($self) = @_;
409              
410 92         254 $self->{cache} = {};
411             }
412              
413             #----------------------------------------------------------------------
414             # Sort the data if it is in an array
415              
416             sub sort {
417 424     424 0 1427 my ($self, $data, $sort_field, $sort_reverse) = @_;
418              
419 424         591 my $sorted_data;
420 424         1007 my $data_field = $self->find_data_field($data);
421              
422 424 100       905 if ($data_field) {
423 134         394 my @augmented_data = $self->sort_with_field($data->{$data_field},
424             $sort_field,
425             $sort_reverse);
426              
427 134         372 my @stripped_data = $self->strip_augmented(@augmented_data);
428 134         453 $sorted_data = {$data_field => \@stripped_data};
429              
430             } else {
431 290         437 $sorted_data = $data;
432             }
433              
434 424         814 return $sorted_data;
435             }
436              
437             #----------------------------------------------------------------------
438             # Sort augmented list by swartzian transform
439              
440             sub sort_augmented {
441 153     153 0 355 my ($self, $sort_reverse, @augmented_data) = @_;
442              
443 153         417 my $comparer = $self->file_comparer($sort_reverse);
444 153         401 @augmented_data = sort $comparer @augmented_data;
445 153         741 return @augmented_data;
446             }
447              
448             #----------------------------------------------------------------------
449             # Sort data retaining the field you sort with
450              
451             sub sort_with_field {
452 153     153 0 372 my ($self, $data_column, $sort_field, $sort_reverse) = @_;
453 153 100       374 $sort_field = 'name' unless defined $sort_field;
454 153 100       390 $sort_reverse = 0 unless defined $sort_reverse;
455              
456 153         477 my $sort_column = $self->find_sort_column($data_column, $sort_field);
457              
458 153         539 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 144     144 0 276 my $self = shift @_;
467 144         325 return map {$_->[1]} @_;
  211         542  
468             }
469              
470             #----------------------------------------------------------------------
471             # Split the sigil off from the variable name from a template
472              
473             sub split_name {
474 513     513 0 4652 my ($self, $variable_name) = @_;
475              
476 513         858 my $name = $variable_name;
477 513         1637 $name =~ s/^([\$\@])//;
478 513   100     2885 my $sigil = $1 || '';
479              
480 513         1452 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