File Coverage

blib/lib/Data/Record/Serialize/Role/Base.pm
Criterion Covered Total %
statement 151 152 99.3
branch 61 68 89.7
condition 9 11 81.8
subroutine 35 37 94.5
pod 7 8 87.5
total 263 276 95.2


line stmt bran cond sub pod time code
1             package Data::Record::Serialize::Role::Base;
2              
3             # ABSTRACT: Base Role for Data::Record::Serialize
4              
5 20     20   437537 use v5.12;
  20         94  
6 20     20   527 use Moo::Role;
  20         11703  
  20         155  
7              
8             our $VERSION = '2.02';
9              
10 20     20   13608 use Data::Record::Serialize::Error { errors => [ 'fields', 'types' ] }, -all;
  20         74  
  20         447  
11              
12 20     20   16328 use Data::Record::Serialize::Util -all;
  20         79  
  20         251  
13              
14 20     20   66760 use Types::Standard qw[ ArrayRef CodeRef CycleTuple HashRef Enum Str Bool is_HashRef Maybe ];
  20         2713232  
  20         355  
15 20     20   105489 use Data::Record::Serialize::Types qw( SerializeType );
  20         1319  
  20         263  
16              
17 20     20   25369 use Ref::Util qw( is_coderef is_arrayref );
  20         14482  
  20         2242  
18 20     20   165 use List::Util 1.33 qw( any );
  20         501  
  20         1595  
19              
20 20     20   11105 use POSIX ();
  20         189723  
  20         869  
21              
22 20     20   210 use namespace::clean;
  20         47  
  20         227  
23              
24              
25              
26              
27              
28              
29              
30              
31              
32              
33              
34              
35              
36              
37              
38              
39             has types => (
40             is => 'rwp',
41             isa => ( HashRef [SerializeType] | CycleTuple [ Str, SerializeType ] )
42             , # need parens for perl <= 5.12.5
43             predicate => 1,
44             trigger => sub {
45             $_[0]->clear_type_index;
46             $_[0]->clear_output_types;
47             },
48             );
49              
50              
51              
52              
53              
54              
55              
56              
57              
58              
59              
60              
61              
62             has default_type => (
63             is => 'ro',
64             isa => SerializeType,
65             predicate => 1,
66             );
67              
68              
69              
70              
71              
72              
73              
74              
75              
76              
77              
78             has fields => (
79             is => 'rwp',
80             isa => ( ArrayRef [Str] | Enum ['all'] ), # need parens for perl <= 5.12.5
81             predicate => 1,
82             clearer => 1,
83             trigger => sub {
84             $_[0]->_clear_fieldh;
85             $_[0]->clear_output_types;
86             $_[0]->clear_output_fields;
87             },
88             );
89              
90              
91             # for quick lookup of field names
92             has _fieldh => (
93             is => 'lazy',
94             init_arg => undef,
95             clearer => 1,
96             builder => sub {
97 56     56   785 my $self = shift;
98 56         118 my %fieldh;
99 56         152 @fieldh{ @{ $self->fields } } = ();
  56         364  
100 56         404 return \%fieldh;
101             },
102             );
103              
104              
105              
106              
107              
108              
109              
110              
111              
112              
113              
114             has output_fields => (
115             is => 'lazy',
116             trigger => 1,
117             clearer => 1,
118             builder => sub {
119 19     19   26871 my $self = shift;
120 19   66     49 [ map { $self->rename_fields->{$_} // $_ } @{ $self->fields } ];
  47         438  
  19         111  
121             },
122             init_arg => undef,
123             );
124              
125             # something for other roles to wrap.
126       0     sub _trigger_output_fields { }
127              
128             has _run_setup => (
129             is => 'rwp',
130             isa => Bool,
131             init_args => undef,
132             default => 1,
133             );
134              
135              
136             # have we initialized types? can't simply use $self->has_types, as
137             # the caller may have provided some.
138             has _have_initialized_types => (
139             is => 'rwp',
140             init_arg => undef,
141             isa => Bool,
142             default => 0,
143             );
144              
145             has _boolify => (
146             is => 'lazy',
147             isa => Bool,
148             init_arg => undef,
149 52 100   52   1962 builder => sub { $_[0]->_can_bool || $_[0]->_convert_boolean_to_int },
150             );
151              
152             has _convert_boolean_to_int => (
153             is => 'rwp',
154             default => 0,
155             );
156              
157             has _can_bool => (
158             is => 'lazy',
159             isa => Bool,
160             init_arg => undef,
161 52     52   3976 builder => sub { !!$_[0]->can( 'to_bool' ) },
162             );
163              
164              
165              
166              
167              
168              
169              
170              
171              
172              
173              
174              
175              
176              
177              
178              
179              
180             sub _build_field_list_with_type {
181 32     32   333 my ( $self, $list_spec, $type, $error_label ) = @_;
182              
183 32         646 my $fieldh = $self->_fieldh;
184 32         225 my $list = do {
185 32 100       151 if ( is_coderef( $list_spec ) ) {
    100          
186 3         22 ( ArrayRef [Str] )->assert_return( $list_spec->( $self ) );
187             }
188             elsif ( is_arrayref( $list_spec ) ) {
189 10         48 [@$list_spec];
190             }
191             else {
192             # want all of the fields. actually just want the ones that will be output,
193             # otherwise the check below will fail.
194 19 100       67 [ grep { exists $fieldh->{$_} } $list_spec ? @{ $self->type_index->[$type] } : () ];
  24         182  
  16         283  
195             }
196             };
197              
198             # this check is to help catch typos by users
199 32         136 my @not_field = grep { !exists $fieldh->{$_} } @{$list};
  35         106  
  32         72  
200 32 100       150 error( 'fields', "unknown $error_label fields: " . join( ', ', @not_field ) )
201             if @not_field;
202              
203 26         516 return $list;
204             }
205              
206              
207              
208              
209              
210              
211              
212              
213              
214              
215              
216              
217              
218              
219              
220              
221              
222              
223              
224              
225              
226              
227              
228              
229              
230              
231              
232              
233              
234              
235              
236              
237              
238              
239              
240             has [ 'nullify', 'numify', 'stringify' ] => (
241             is => 'rw',
242             isa => ( ArrayRef [Str] | CodeRef | Bool ), # need parens for perl <= 5.12.5
243             predicate => 1,
244             trigger => 1,
245             );
246              
247 11     11   40674 sub _trigger_nullify { $_[0]->_clear_nullified }
248 7     7   44358 sub _trigger_numify { $_[0]->_clear_numified }
249 7     7   5325 sub _trigger_stringify { $_[0]->_clear_stringified }
250              
251              
252              
253              
254              
255              
256              
257              
258              
259              
260              
261              
262              
263              
264              
265              
266              
267              
268             sub nullified {
269 10     10 1 4417 my $self = shift;
270 10 100       61 return [ $self->has_fields ? @{ $self->_nullified } : () ];
  8         299  
271             }
272              
273              
274              
275              
276              
277              
278              
279              
280              
281              
282              
283              
284              
285              
286              
287              
288              
289              
290              
291             sub numified {
292 10     10 1 4758 my $self = shift;
293 10 100       58 return [ $self->has_fields ? @{ $self->_numified } : () ];
  8         201  
294             }
295              
296              
297              
298              
299              
300              
301              
302              
303              
304              
305              
306              
307              
308              
309              
310              
311              
312              
313             sub stringified {
314 10     10 1 4321 my $self = shift;
315 10 100       55 return [ $self->has_fields ? @{ $self->_stringified } : () ];
  8         237  
316             }
317              
318              
319             has [ '_nullified', '_numified', '_stringified' ] => (
320             is => 'lazy',
321             isa => ArrayRef [Str],
322             clearer => 1,
323             predicate => 1,
324             init_arg => undef,
325             builder => 1,
326             );
327              
328             sub _build__nullified {
329 58     58   834 my $self = shift;
330 58 100       1470 return $self->has_nullify
331             ? $self->_build_field_list_with_type( $self->nullify, ANY, 'nullify' )
332             : [];
333             }
334              
335             sub _build__numified {
336 57     57   744 my $self = shift;
337 57 100       1311 return $self->has_numify
338             ? $self->_build_field_list_with_type( $self->numify, NUMBER, 'numify' )
339             : [];
340             }
341              
342             sub _build__stringified {
343 56     56   694 my $self = shift;
344 56 100       1360 return $self->has_stringify
345             ? $self->_build_field_list_with_type( $self->stringify, STRING, 'stringify' )
346             : [];
347             }
348              
349              
350              
351              
352              
353              
354              
355              
356              
357              
358 1     1 1 280 sub string_fields { $_[0]->type_index->[STRING] }
359              
360              
361              
362              
363              
364              
365              
366              
367              
368              
369 44     44 1 2004 sub numeric_fields { $_[0]->type_index->[NUMBER] }
370              
371              
372              
373              
374              
375              
376              
377              
378              
379              
380 136     136 1 3343 sub boolean_fields { $_[0]->type_index->[BOOLEAN] }
381              
382              
383              
384              
385              
386              
387              
388              
389              
390              
391              
392              
393              
394              
395              
396              
397              
398              
399              
400              
401              
402              
403              
404              
405              
406              
407              
408              
409              
410              
411              
412              
413              
414              
415              
416             has type_index => (
417             is => 'lazy',
418             init_arg => undef,
419             clearer => 1,
420             builder => sub {
421 60     60   708 my $self = shift;
422 60 50       271 error( 'types', 'no types for fields are available' )
423             unless $self->has_types;
424 60         2382 index_types( $self->types );
425             },
426             );
427              
428              
429              
430              
431              
432              
433              
434              
435              
436              
437              
438             has output_types => (
439             is => 'lazy',
440             init_arg => undef,
441             clearer => 1,
442             trigger => 1,
443             );
444              
445             sub _build_output_types {
446 59     59   7891 my $self = shift;
447 59         240 my %types;
448              
449             return
450 59 50       323 unless $self->has_types;
451              
452 59         143 my @int_fields = grep { defined $self->types->{$_} } @{ $self->fields };
  139         635  
  59         243  
453 59         196 @types{@int_fields} = @{ $self->types }{@int_fields};
  59         442  
454              
455 59 50       301 unless ( $self->_encoder_has_type( BOOLEAN ) ) {
456 59         163 $types{$_} = T_INTEGER for @{ $self->boolean_fields };
  59         319  
457 59         352 $self->_set__convert_boolean_to_int( 1 );
458             }
459              
460 59 100       470 unless ( $self->_encoder_has_type( INTEGER ) ) {
461 42         110 $types{$_} = T_NUMBER for @{ $self->numeric_fields };
  42         154  
462             }
463              
464 59 100       2766 if ( my $map_types = $self->_map_types ) {
465 19         172 for my $field ( keys %types ) {
466 46         86 my $type = $types{$field};
467 46 50       117 next unless exists $map_types->{$type};
468 46         115 $types{$field} = $map_types->{$type};
469             }
470             }
471              
472 59         1960 for my $key ( keys %types ) {
473 140 100       560 my $rename = $self->rename_fields->{$key}
474             or next;
475              
476 2         8 $types{$rename} = delete $types{$key};
477             }
478              
479 59         421 \%types;
480             }
481              
482             # something for other roles to wrap.
483       0     sub _trigger_output_types { }
484              
485              
486             sub _encoder_has_type {
487 118     118   507 my ( $self, $type ) = @_;
488 118   100 96   765 any { is_type( $_, $type ) } keys %{ $self->_map_types // {} };
  96         614  
  118         594  
489             }
490              
491              
492              
493              
494              
495              
496              
497              
498             has format_fields => (
499             is => 'ro',
500             isa => HashRef [ Str | CodeRef ],
501             );
502              
503              
504              
505              
506              
507              
508              
509             has format_types => (
510             is => 'ro',
511             isa => HashRef [ Str | CodeRef ],
512             );
513              
514              
515              
516              
517              
518              
519              
520              
521             has rename_fields => (
522             is => 'ro',
523             isa => HashRef [Str],
524             coerce => sub {
525             return $_[0] unless is_HashRef( $_[0] );
526              
527             # remove renames which do nothing
528             my %rename = %{ $_[0] };
529             delete @rename{ grep { $rename{$_} eq $_ } keys %rename };
530             return \%rename;
531             },
532             default => sub { {} },
533             trigger => sub {
534             $_[0]->clear_output_types;
535             },
536             );
537              
538              
539              
540              
541              
542              
543              
544              
545              
546             has format => (
547             is => 'ro',
548             isa => Bool,
549             default => 1,
550             );
551              
552             has _format => (
553             is => 'rwp',
554             lazy => 1,
555             default => sub {
556             my $self = shift;
557              
558             if ( $self->format ) {
559             my %format;
560              
561             # first consider types; they'll be overridden by per field
562             # formats in the next step.
563             if ( $self->format_types && $self->types ) {
564              
565             for my $field ( @{ $self->fields } ) {
566              
567             my $type = $self->types->{$field}
568             or next;
569              
570             my $format = $self->format_types->{$type}
571             or next;
572              
573             $format{$field} = $format;
574             }
575             }
576              
577             if ( $self->format_fields ) {
578             for my $field ( @{ $self->fields } ) {
579             my $format = $self->format_fields->{$field}
580             or next;
581              
582             $format{$field} = $format;
583             }
584             }
585              
586             return \%format
587             if keys %format;
588             }
589              
590             return;
591             },
592             init_arg => undef,
593             );
594              
595              
596              
597              
598              
599              
600             sub BUILD {
601 63     63 0 1575 my $self = shift;
602              
603             # if types is passed, set fields if it's not set.
604             # convert types to hash if it's an array
605 63 100       493 if ( $self->has_types ) {
606 19         73 my $types = $self->types;
607              
608 19 100       98 if ( 'HASH' eq ref $types ) {
    50          
609 18 100       96 $self->_set_fields( [ keys %{$types} ] )
  9         200  
610             unless $self->has_fields;
611             }
612             elsif ( 'ARRAY' eq ref $types ) {
613 1         2 $self->_set_types( { @{$types} } );
  1         32  
614              
615 1 50       13 if ( !$self->has_fields ) {
616 1         2 my @fields;
617             # pull off "keys"
618 1         12 push @fields, ( shift @$types, shift @$types )[0] while @$types;
619 1         28 $self->_set_fields( \@fields );
620             }
621             }
622             else {
623 0         0 error( '::attribute::value', 'internal error' );
624             }
625             }
626              
627 63 100       394 if ( $self->has_fields ) {
628              
629 30 100       165 if ( ref $self->fields ) {
630             # in this specific case everything can be done before the first
631             # record is read. this is kind of overkill, but at least one
632             # test depended upon being able to determine types prior
633             # to sending the first record, so need to do this here rather
634             # than in Default::setup
635 25 100       131 $self->_set_types_from_default
636             if $self->has_default_type;
637             }
638              
639             # if fields eq 'all', clear out the attribute so that it will get
640             # filled in when the first record is sent.
641             else {
642 5         172 $self->clear_fields;
643             }
644             }
645              
646 63         905 return;
647             }
648              
649             sub _set_types_from_record {
650 50     50   159 my ( $self, $data ) = @_;
651              
652 50 50       313 return if $self->_have_initialized_types;
653              
654 50 100       239 my $types = $self->has_types ? $self->types : {};
655              
656 50         117 for my $field ( grep !defined $types->{$_}, @{ $self->fields } ) {
  50         315  
657 101         234 my $value = $data->{$field};
658 101 100       474 my $def = Scalar::Util::looks_like_number( $value ) ? T_NUMBER : T_STRING;
659              
660 101 100 100     736 $def = T_INTEGER
661             if $def eq T_NUMBER
662             && POSIX::floor( $value ) == POSIX::ceil( $value );
663              
664 101         352 $types->{$field} = $def;
665             }
666              
667 50         1555 $self->_set_types( $types );
668 50         1505 $self->_set__have_initialized_types( 1 );
669             }
670              
671             sub _set_types_from_default {
672 9     9   22 my $self = shift;
673              
674 9 100       42 return if $self->_have_initialized_types;
675              
676 7 100       39 my $types = $self->has_types ? $self->types : {};
677              
678 7         18 $types->{$_} = $self->default_type for grep { !defined $types->{$_} } @{ $self->fields };
  14         64  
  7         24  
679              
680 7         154 $self->_set_types( $types );
681 7         176 $self->_set__have_initialized_types( 1 );
682             }
683              
684              
685              
686              
687              
688              
689              
690              
691              
692              
693              
694              
695              
696              
697              
698             sub setup_from_record {
699 56     56 1 175 my ( $self, $data ) = @_;
700              
701             # if fields has not been set yet, set it to the names in the data
702 56 100       1348 $self->_set_fields( [ keys %$data ] )
703             unless $self->has_fields;
704              
705             # make sure there are no duplicate output fields
706 56         360 my %dups;
707 56   66     131 $dups{$_}++ && error( fields => "duplicate output field: $_" ) for @{ $self->fields };
  56         657  
708              
709 55 100       294 if ( $self->has_default_type ) {
710 5         20 $self->_set_types_from_default;
711             }
712             else {
713 50         252 $self->_set_types_from_record( $data );
714             }
715              
716             # trigger building of output_types, which also remaps types. ick.
717 55         3194 $self->output_types;
718              
719             }
720              
721             1;
722              
723             #
724             # This file is part of Data-Record-Serialize
725             #
726             # This software is Copyright (c) 2017 by Smithsonian Astrophysical Observatory.
727             #
728             # This is free software, licensed under:
729             #
730             # The GNU General Public License, Version 3, June 2007
731             #
732              
733             __END__