File Coverage

lib/Types/Standard/Dict.pm
Criterion Covered Total %
statement 251 261 96.5
branch 131 160 81.8
condition 59 80 73.7
subroutine 28 28 100.0
pod 1 2 50.0
total 470 531 88.7


line stmt bran cond sub pod time code
1             # INTERNAL MODULE: guts for Dict type from Types::Standard.
2              
3             package Types::Standard::Dict;
4              
5 22     22   1429 use 5.008001;
  22         91  
6 22     22   145 use strict;
  22         38  
  22         637  
7 22     22   107 use warnings;
  22         39  
  22         2161  
8              
9             BEGIN {
10 22     22   94 $Types::Standard::Dict::AUTHORITY = 'cpan:TOBYINK';
11 22         1096 $Types::Standard::Dict::VERSION = '2.010001';
12             }
13              
14             $Types::Standard::Dict::VERSION =~ tr/_//d;
15              
16 22     22   133 use Types::Standard ();
  22         189  
  22         456  
17 22     22   103 use Types::TypeTiny ();
  22         41  
  22         3510  
18              
19             sub _croak ($;@) {
20 3     3   19 require Carp;
21 3         1000 goto \&Carp::confess;
22 0         0 require Error::TypeTiny;
23 0         0 goto \&Error::TypeTiny::croak;
24             }
25              
26             my $_Slurpy = Types::Standard::Slurpy;
27             my $_optional = Types::Standard::Optional;
28             my $_hash = Types::Standard::HashRef;
29             my $_map = Types::Standard::Map;
30             my $_any = Types::Standard::Any;
31              
32 22     22   164 use Exporter::Tiny 1.004001 ();
  22         453  
  22         11999  
33             our @ISA = qw( Exporter::Tiny );
34             our @EXPORT_OK = qw( combine );
35              
36             sub _exporter_fail {
37 3     3   644 my ( $class, $type_name, $values, $globals ) = @_;
38 3         9 my $caller = $globals->{into};
39            
40 3         7 my @final;
41             {
42 3         7 my $to_type = sub {
43 9 50   9   375 return $_[0] if Types::TypeTiny::is_TypeTiny($_[0]);
44 0         0 require Type::Utils;
45 0         0 Type::Utils::dwim_type( $_[0], for => 'caller' );
46 3         15 };
47 3         8 my $of = $values->{of};
48 3 50       46 Types::TypeTiny::is_ArrayLike($of)
49             or _croak( qq{Expected arrayref option "of" for type "$type_name"} );
50 3         17 my @of_copy = @$of;
51 3 50       15 my $slurpy = @of_copy % 2 ? pop( @of_copy ) : undef;
52 3         32 my $iter = pair_iterator( @of_copy );
53 3         9 while ( my ( $name, $type ) = $iter->() ) {
54 9         18 push @final, $name, $to_type->( $type );
55             }
56 3 50       31 push @final, $to_type->( $slurpy ) if defined $slurpy;
57             }
58            
59 3         17 my $type = Types::Standard::Dict->of( @final );
60             $type = $type->create_child_type(
61             name => $type_name,
62             $type->has_coercion ? ( coercion => 1 ) : (),
63 3 50       15 exists( $values->{where} ) ? ( constraint => $values->{where} ) : (),
    50          
64             );
65            
66             $INC{'Type/Registry.pm'}
67             ? 'Type::Registry'->for_class( $caller )->add_type( $type, $type_name )
68             : ( $Type::Registry::DELAYED{$caller}{$type_name} = $type )
69 3 50 33     43 unless( ref($caller) or $caller eq '-lexical' or $globals->{'lexical'} );
    50 33        
70 3         6 return map +( $_->{name} => $_->{code} ), @{ $type->exportables };
  3         14  
71             }
72              
73 22     22   311 no warnings;
  22         53  
  22         90173  
74              
75             sub pair_iterator {
76 142 100   142 0 575 _croak( "Expected even-sized list" ) if @_ % 2;
77 141         517 my @array = @_;
78             sub {
79 372 100   372   1149 return unless @array;
80 236         1008 splice( @array, 0, 2 );
81 141         868 };
82             }
83              
84             sub __constraint_generator {
85 67 100 100 67   2000 my $slurpy =
86             @_
87             && Types::TypeTiny::is_TypeTiny( $_[-1] )
88             && $_[-1]->is_strictly_a_type_of( $_Slurpy )
89             ? pop->my_unslurpy
90             : undef;
91 67         320 my $iterator = pair_iterator @_;
92 66         335 my %constraints;
93             my %is_optional;
94 66         0 my @keys;
95            
96 66         188 while ( my ( $k, $v ) = $iterator->() ) {
97 107         332 $constraints{$k} = $v;
98 107 100       3231 Types::TypeTiny::is_TypeTiny( $v )
99             or _croak(
100             "Parameter for Dict[...] with key '$k' expected to be a type constraint; got $v"
101             );
102 106 100       478 Types::TypeTiny::is_StringLike( $k )
103             or _croak( "Key for Dict[...] expected to be string; got $k" );
104 105         303 push @keys, $k;
105 105         2246 $is_optional{$k} = !!$constraints{$k}->is_strictly_a_type_of( $_optional );
106             } #/ while ( my ( $k, $v ) = $iterator...)
107            
108             return sub {
109 124     124   346 my $value = $_[0];
110 124 100       575 if ( $slurpy ) {
111 86 100       674 my %tmp = map +( exists( $constraints{$_} ) ? () : ( $_ => $value->{$_} ) ),
112             keys %$value;
113 86 100       387 return unless $slurpy->check( \%tmp );
114             }
115             else {
116 38   100     483 exists( $constraints{$_} ) || return for sort keys %$value;
117             }
118 94         934 for my $k ( @keys ) {
119 153 100       774 exists( $value->{$k} ) or ( $is_optional{$k} ? next : return );
    100          
120 126 100       409 $constraints{$k}->check( $value->{$k} ) or return;
121             }
122 38         235 return !!1;
123 64         913 };
124             } #/ sub __constraint_generator
125              
126             sub __inline_generator {
127              
128             # We can only inline a parameterized Dict if all the
129             # constraints inside can be inlined.
130            
131 64 100 100 64   2192 my $slurpy =
132             @_
133             && Types::TypeTiny::is_TypeTiny( $_[-1] )
134             && $_[-1]->is_strictly_a_type_of( $_Slurpy )
135             ? pop->my_unslurpy
136             : undef;
137 64 50 66     348 return if $slurpy && !$slurpy->can_be_inlined;
138            
139             # Is slurpy a very loose type constraint?
140             # i.e. Any, Item, Defined, Ref, or HashRef
141 64   100     252 my $slurpy_is_any = $slurpy && $_hash->is_a_type_of( $slurpy );
142            
143             # Is slurpy a parameterized Map, or expressible as a parameterized Map?
144 64   66     329 my $slurpy_is_map =
145             $slurpy
146             && $slurpy->is_parameterized
147             && (
148             ( $slurpy->parent->strictly_equals( $_map ) && $slurpy->parameters )
149             || ( $slurpy->parent->strictly_equals( $_hash )
150             && [ $_any, $slurpy->parameters->[0] ] )
151             );
152            
153 64         251 my $iterator = pair_iterator @_;
154 64         191 my %constraints;
155             my @keys;
156            
157 64         252 while ( my ( $k, $c ) = $iterator->() ) {
158 103 100       405 return unless $c->can_be_inlined;
159 100         340 $constraints{$k} = $c;
160 100         515 push @keys, $k;
161             }
162            
163 61         384 my $regexp = join "|", map quotemeta, @keys;
164             return sub {
165 518     518   4064 require B;
166 518         1166 my $h = $_[1];
167             join " and ",
168             Types::Standard::HashRef->inline_check( $h ),
169             (
170             $slurpy_is_any
171             ? ()
172             : $slurpy_is_map ? do {
173 171         846 '(not grep {' . "my \$v = ($h)->{\$_};" . sprintf(
174             'not((/\\A(?:%s)\\z/) or ((%s) and (%s)))',
175             $regexp,
176             $slurpy_is_map->[0]->inline_check( '$_' ),
177             $slurpy_is_map->[1]->inline_check( '$v' ),
178             ) . "} keys \%{$h})";
179             }
180             : $slurpy ? do {
181 30         145 'do {'
182             . "my \$slurpy_tmp = +{ map /\\A(?:$regexp)\\z/ ? () : (\$_ => ($h)->{\$_}), keys \%{$h} };"
183             . $slurpy->inline_check( '$slurpy_tmp' ) . '}';
184             }
185             : "not(grep !/\\A(?:$regexp)\\z/, keys \%{$h})"
186             ),
187             (
188             map {
189 518 100       2576 my $k = B::perlstring( $_ );
  858 100       3683  
    100          
190             $constraints{$_}->is_strictly_a_type_of( $_optional )
191             ? sprintf(
192             '(!exists %s->{%s} or %s)', $h, $k,
193             $constraints{$_}->inline_check( "$h\->{$k}" )
194             )
195             : (
196             "exists($h\->{$k})",
197 858 100       3633 $constraints{$_}->inline_check( "$h\->{$k}" )
198             )
199             } @keys
200             ),
201             ;
202             }
203 61         862 } #/ sub __inline_generator
204              
205             sub __deep_explanation {
206 8     8   49 require B;
207 8         26 my ( $type, $value, $varname ) = @_;
208 8         18 my @params = @{ $type->parameters };
  8         27  
209            
210 8 50 33     320 my $slurpy =
211             @params
212             && Types::TypeTiny::is_TypeTiny( $params[-1] )
213             && $params[-1]->is_strictly_a_type_of( $_Slurpy )
214             ? pop( @params )->my_unslurpy
215             : undef;
216 8         38 my $iterator = pair_iterator @params;
217 8         17 my %constraints;
218             my @keys;
219            
220 8         22 while ( my ( $k, $c ) = $iterator->() ) {
221 17         37 push @keys, $k;
222 17         56 $constraints{$k} = $c;
223             }
224            
225 8         23 for my $k ( @keys ) {
226             next
227             if $constraints{$k}->has_parent
228             && ( $constraints{$k}->parent == Types::Standard::Optional )
229 16 100 100     53 && ( !exists $value->{$k} );
      100        
230 14 100       110 next if $constraints{$k}->check( $value->{$k} );
231            
232             return [
233             sprintf( '"%s" requires key %s to appear in hash', $type, B::perlstring( $k ) )
234             ]
235 5 100       73 unless exists $value->{$k};
236            
237             return [
238             sprintf(
239             '"%s" constrains value at key %s of hash with "%s"',
240             $type,
241             B::perlstring( $k ),
242             $constraints{$k},
243             ),
244             @{
245 2         28 $constraints{$k}->validate_explain(
246 2         15 $value->{$k},
247             sprintf( '%s->{%s}', $varname, B::perlstring( $k ) ),
248             )
249             },
250             ];
251             } #/ for my $k ( @keys )
252            
253 3 50       18 if ( $slurpy ) {
254 0 0       0 my %tmp = map { exists( $constraints{$_} ) ? () : ( $_ => $value->{$_} ) }
  0         0  
255             keys %$value;
256            
257 0         0 my $explain = $slurpy->validate_explain( \%tmp, '$slurpy' );
258             return [
259 0 0       0 sprintf(
260             '"%s" requires the hashref of additional key/value pairs to conform to "%s"',
261             $type, $slurpy
262             ),
263             @$explain,
264             ] if $explain;
265             } #/ if ( $slurpy )
266             else {
267 3         27 for my $k ( sort keys %$value ) {
268             return [
269             sprintf(
270             '"%s" does not allow key %s to appear in hash', $type, B::perlstring( $k )
271             )
272             ]
273 6 100       40 unless exists $constraints{$k};
274             }
275             } #/ else [ if ( $slurpy ) ]
276            
277             # This should never happen...
278 0         0 return; # uncoverable statement
279             } #/ sub __deep_explanation
280              
281             my $label_counter = 0;
282             our ( $keycheck_counter, @KEYCHECK ) = -1;
283              
284             sub __coercion_generator {
285 32 100 66 32   1224 my $slurpy =
286             @_
287             && Types::TypeTiny::is_TypeTiny( $_[-1] )
288             && $_[-1]->is_strictly_a_type_of( $_Slurpy )
289             ? pop->my_unslurpy
290             : undef;
291 32         234 my ( $parent, $child, %dict ) = @_;
292 32         269 my $C = "Type::Coercion"->new( type_constraint => $child );
293            
294 32         83 my $all_inlinable = 1;
295 32         70 my $child_coercions_exist = 0;
296 32         120 for my $tc ( values %dict ) {
297 59 100       210 $all_inlinable = 0 if !$tc->can_be_inlined;
298 59 100 100     329 $all_inlinable = 0 if $tc->has_coercion && !$tc->coercion->can_be_inlined;
299 59 100       202 $child_coercions_exist++ if $tc->has_coercion;
300             }
301 32 50 66     178 $all_inlinable = 0 if $slurpy && !$slurpy->can_be_inlined;
302 32 100 100     149 $all_inlinable = 0
      100        
303             if $slurpy
304             && $slurpy->has_coercion
305             && !$slurpy->coercion->can_be_inlined;
306            
307 32 100 100     239 $child_coercions_exist++ if $slurpy && $slurpy->has_coercion;
308 32 100       317 return unless $child_coercions_exist;
309            
310 18 100       94 if ( $all_inlinable ) {
311             $C->add_type_coercions(
312             $parent => Types::Standard::Stringable {
313 11     11   78 require B;
314            
315             my $keycheck = join "|", map quotemeta,
316 11 50       76 sort { length( $b ) <=> length( $a ) or $a cmp $b } keys %dict;
  10         74  
317 11         328 $keycheck = $KEYCHECK[ ++$keycheck_counter ] = qr{^($keycheck)$}ms; # regexp for legal keys
318            
319 11         49 my $label = sprintf( "DICTLABEL%d", ++$label_counter );
320 11         20 my @code;
321 11         27 push @code, 'do { my ($orig, $return_orig, $tmp, %new) = ($_, 0);';
322 11         36 push @code, "$label: {";
323 11 100       36 if ( $slurpy ) {
324 4         21 push @code,
325             sprintf(
326             'my $slurped = +{ map +($_=~$%s::KEYCHECK[%d])?():($_=>$orig->{$_}), keys %%$orig };',
327             __PACKAGE__, $keycheck_counter
328             );
329 4 100       13 if ( $slurpy->has_coercion ) {
330 3         11 push @code,
331             sprintf(
332             'my $coerced = %s;',
333             $slurpy->coercion->inline_coercion( '$slurped' )
334             );
335 3         13 push @code,
336             sprintf(
337             '((%s)&&(%s))?(%%new=%%$coerced):(($return_orig = 1), last %s);',
338             $_hash->inline_check( '$coerced' ), $slurpy->inline_check( '$coerced' ),
339             $label
340             );
341             } #/ if ( $slurpy->has_coercion)
342             else {
343 1         5 push @code,
344             sprintf(
345             '(%s)?(%%new=%%$slurped):(($return_orig = 1), last %s);',
346             $slurpy->inline_check( '$slurped' ), $label
347             );
348             }
349             } #/ if ( $slurpy )
350             else {
351 7         52 push @code,
352             sprintf(
353             '($_ =~ $%s::KEYCHECK[%d])||(($return_orig = 1), last %s) for sort keys %%$orig;',
354             __PACKAGE__, $keycheck_counter, $label
355             );
356             }
357 11         43 for my $k ( keys %dict ) {
358 20         46 my $ct = $dict{$k};
359 20         54 my $ct_coerce = $ct->has_coercion;
360 20         68 my $ct_optional = $ct->is_a_type_of( $_optional );
361 20         74 my $K = B::perlstring( $k );
362            
363 20 100       113 push @code, sprintf(
364             'if (exists $orig->{%s}) { $tmp = %s; (%s) ? ($new{%s}=$tmp) : (($return_orig=1), last %s) }',
365             $K,
366             $ct_coerce
367             ? $ct->coercion->inline_coercion( "\$orig->{$K}" )
368             : "\$orig->{$K}",
369             $ct->inline_check( '$tmp' ),
370             $K,
371             $label,
372             );
373             } #/ for my $k ( keys %dict )
374 11         34 push @code, '}';
375 11         29 push @code, '$return_orig ? $orig : \\%new';
376 11         26 push @code, '}';
377            
378             #warn "CODE:: @code";
379 11         136 "@code";
380             }
381 12         176 );
382             } #/ if ( $all_inlinable )
383            
384             else {
385             my %is_optional = map {
386 6         25 ;
387 12         30 $_ => !!$dict{$_}->is_strictly_a_type_of( $_optional )
388             } sort keys %dict;
389             $C->add_type_coercions(
390             $parent => sub {
391 7 50   7   201 my $value = @_ ? $_[0] : $_;
392 7         17 my %new;
393            
394 7 100       20 if ( $slurpy ) {
395 3 100       21 my %slurped = map exists( $dict{$_} ) ? () : ( $_ => $value->{$_} ),
396             keys %$value;
397            
398 3 100       12 if ( $slurpy->check( \%slurped ) ) {
    100          
399 1         4 %new = %slurped;
400             }
401             elsif ( $slurpy->has_coercion ) {
402 1         5 my $coerced = $slurpy->coerce( \%slurped );
403 1 50       5 $slurpy->check( $coerced ) ? ( %new = %$coerced ) : ( return $value );
404             }
405             else {
406 1         8 return $value;
407             }
408             } #/ if ( $slurpy )
409             else {
410 4         15 for my $k ( keys %$value ) {
411 8 50       22 return $value unless exists $dict{$k};
412             }
413             }
414            
415 6         20 for my $k ( keys %dict ) {
416 12 100 100     46 next if $is_optional{$k} and not exists $value->{$k};
417            
418 10         17 my $ct = $dict{$k};
419 10 100       28 my $x = $ct->has_coercion ? $ct->coerce( $value->{$k} ) : $value->{$k};
420            
421 10 50       98 return $value unless $ct->check( $x );
422            
423 10         41 $new{$k} = $x;
424             } #/ for my $k ( keys %dict )
425            
426 6         40 return \%new;
427             },
428 6         62 );
429             } #/ else [ if ( $all_inlinable ) ]
430            
431 18         63 return $C;
432             } #/ sub __coercion_generator
433              
434             sub __dict_is_slurpy {
435 97     97   221 my $self = shift;
436            
437 97 50       332 return !!0 if $self == Types::Standard::Dict();
438            
439             my $dict = $self->find_parent(
440 97 50   99   858 sub { $_->has_parent && $_->parent == Types::Standard::Dict() } );
  99         286  
441             my $slurpy =
442 97 100 100     480 @{ $dict->parameters }
443             && Types::TypeTiny::is_TypeTiny( $dict->parameters->[-1] )
444             && $dict->parameters->[-1]->is_strictly_a_type_of( $_Slurpy )
445             ? $dict->parameters->[-1]
446             : undef;
447             } #/ sub __dict_is_slurpy
448              
449             sub __hashref_allows_key {
450 75     75   214 my $self = shift;
451 75         198 my ( $key ) = @_;
452            
453 75 100       322 return Types::Standard::is_Str( $key ) if $self == Types::Standard::Dict();
454            
455             my $dict = $self->find_parent(
456 69 50   92   695 sub { $_->has_parent && $_->parent == Types::Standard::Dict() } );
  92         431  
457 69         403 my %params;
458 69         556 my $slurpy = $dict->my_dict_is_slurpy;
459 69 100       303 if ( $slurpy ) {
460 47         206 my @args = @{ $dict->parameters };
  47         159  
461 47         100 pop @args;
462 47         185 %params = @args;
463 47         362 $slurpy = $slurpy->my_unslurpy;
464             }
465             else {
466 22         43 %params = @{ $dict->parameters };
  22         129  
467             }
468            
469             return !!1
470 69 100       449 if exists( $params{$key} );
471 46 100       304 return !!0
472             if !$slurpy;
473 32 100 66     249 return Types::Standard::is_Str( $key )
      66        
      66        
474             if $slurpy == Types::Standard::Any()
475             || $slurpy == Types::Standard::Item()
476             || $slurpy == Types::Standard::Defined()
477             || $slurpy == Types::Standard::Ref();
478 20 100       188 return $slurpy->my_hashref_allows_key( $key )
479             if $slurpy->is_a_type_of( Types::Standard::HashRef() );
480 2         76 return !!0;
481             } #/ sub __hashref_allows_key
482              
483             sub __hashref_allows_value {
484 18     18   44 my $self = shift;
485 18         53 my ( $key, $value ) = @_;
486            
487 18 100       91 return !!0 unless $self->my_hashref_allows_key( $key );
488 16 50       93 return !!1 if $self == Types::Standard::Dict();
489            
490             my $dict = $self->find_parent(
491 16 50   18   108 sub { $_->has_parent && $_->parent == Types::Standard::Dict() } );
  18         53  
492 16         78 my %params;
493 16         96 my $slurpy = $dict->my_dict_is_slurpy;
494 16 100       60 if ( $slurpy ) {
495 12         23 my @args = @{ $dict->parameters };
  12         30  
496 12         23 pop @args;
497 12         48 %params = @args;
498 12         72 $slurpy = $slurpy->my_unslurpy;
499             }
500             else {
501 4         10 %params = @{ $dict->parameters };
  4         15  
502             }
503            
504             return !!1
505 16 100 100     102 if exists( $params{$key} ) && $params{$key}->check( $value );
506 10 100       72 return !!0
507             if !$slurpy;
508 8 50 33     62 return !!1
      33        
      33        
509             if $slurpy == Types::Standard::Any()
510             || $slurpy == Types::Standard::Item()
511             || $slurpy == Types::Standard::Defined()
512             || $slurpy == Types::Standard::Ref();
513 8 50       59 return $slurpy->my_hashref_allows_value( $key, $value )
514             if $slurpy->is_a_type_of( Types::Standard::HashRef() );
515 0         0 return !!0;
516             } #/ sub __hashref_allows_value
517              
518             sub combine {
519 3     3 1 39 require Type::Tiny::Union;
520 3         12 my @key_order;
521             my %keys;
522 3         0 my @slurpy;
523            
524 3         9 for my $dict ( @_ ) {
525 8 50 33     318 Types::TypeTiny::is_TypeTiny( $dict ) && $dict->is_a_type_of( Types::Standard::Dict() )
526             or _croak "Unexpected non-Dict argument: $dict";
527            
528 8         22 my @args;
529 8 100       69 if ( my $s = $dict->my_dict_is_slurpy ) {
530 2         6 @args = @{ $dict->parameters };
  2         7  
531 2         4 pop @args;
532 2         17 push @slurpy, $s->my_unslurpy;
533             }
534             else {
535 6         12 @args = @{ $dict->parameters };
  6         19  
536             }
537            
538 8         30 while ( @args ) {
539 8         34 my ( $key, $type ) = splice @args, 0, 2;
540 8 100       29 if ( not exists $keys{ $key } ) {
541 5         12 push @key_order, $key;
542 5         16 $keys{$key} = [];
543             }
544 8         18 push @{ $keys{$key} }, $type;
  8         40  
545             }
546             }
547            
548 3         9 my @args;
549 3         9 for my $key ( @key_order ) {
550 5 100       12 if ( @{ $keys{$key} } == 1 ) {
  5         19  
551 2         9 push @args, $key => $keys{$key}[0];
552             }
553             else {
554 3         7 my %seen;
555 3         22 my @uniq = grep { not $seen{$_->{uniq}}++ } @{ $keys{$key} };
  6         34  
  3         39  
556 3         42 my $union = 'Type::Tiny::Union'->new( type_constraints => \@uniq );
557 3         37 push @args, $key => $union;
558             }
559             }
560            
561 3 100       14 if ( @slurpy ) {
562 1         2 my %seen;
563 1         3 my @uniq = grep { not $seen{$_->{uniq}}++ } @slurpy;
  2         12  
564 1         7 my $union = 'Type::Tiny::Union'->new( type_constraints => \@uniq );
565 1         9 push @args, Types::Standard::Slurpy->of( $union );
566             }
567            
568 3         34 return Types::Standard::Dict->of( @args );
569             }
570              
571             1;
572              
573             __END__