File Coverage

lib/Types/Standard/Dict.pm
Criterion Covered Total %
statement 191 199 96.4
branch 114 134 85.0
condition 56 71 78.8
subroutine 24 24 100.0
pod 0 1 0.0
total 385 429 89.9


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 21     21   467 use 5.008001;
  21         92  
6 21     21   122 use strict;
  21         41  
  21         466  
7 21     21   114 use warnings;
  21         54  
  21         1027  
8              
9             BEGIN {
10 21     21   81 $Types::Standard::Dict::AUTHORITY = 'cpan:TOBYINK';
11 21         814 $Types::Standard::Dict::VERSION = '2.004000';
12             }
13              
14             $Types::Standard::Dict::VERSION =~ tr/_//d;
15              
16 21     21   147 use Types::Standard ();
  21         45  
  21         435  
17 21     21   117 use Types::TypeTiny ();
  21         41  
  21         3005  
18              
19             sub _croak ($;@) {
20 3     3   28 require Carp;
21 3         377 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 21     21   156 no warnings;
  21         56  
  21         61785  
33              
34             sub pair_iterator {
35 117 100   117 0 389 _croak( "Expected even-sized list" ) if @_ % 2;
36 116         353 my @array = @_;
37             sub {
38 300 100   300   898 return unless @array;
39 189         690 splice( @array, 0, 2 );
40 116         503 };
41             }
42              
43             sub __constraint_generator {
44 56 100 100 56   1349 my $slurpy =
45             @_
46             && Types::TypeTiny::is_TypeTiny( $_[-1] )
47             && $_[-1]->is_strictly_a_type_of( $_Slurpy )
48             ? pop->my_unslurpy
49             : undef;
50 56         228 my $iterator = pair_iterator @_;
51 55         175 my %constraints;
52             my %is_optional;
53 55         0 my @keys;
54            
55 55         194 while ( my ( $k, $v ) = $iterator->() ) {
56 88         252 $constraints{$k} = $v;
57 88 100       1973 Types::TypeTiny::is_TypeTiny( $v )
58             or _croak(
59             "Parameter for Dict[...] with key '$k' expected to be a type constraint; got $v"
60             );
61 87 100       425 Types::TypeTiny::is_StringLike( $k )
62             or _croak( "Key for Dict[...] expected to be string; got $k" );
63 86         201 push @keys, $k;
64 86         252 $is_optional{$k} = !!$constraints{$k}->is_strictly_a_type_of( $_optional );
65             } #/ while ( my ( $k, $v ) = $iterator...)
66            
67             return sub {
68 121     121   227 my $value = $_[0];
69 121 100       404 if ( $slurpy ) {
70 83 100       514 my %tmp = map +( exists( $constraints{$_} ) ? () : ( $_ => $value->{$_} ) ),
71             keys %$value;
72 83 100       301 return unless $slurpy->check( \%tmp );
73             }
74             else {
75 38   100     270 exists( $constraints{$_} ) || return for sort keys %$value;
76             }
77 91         649 for my $k ( @keys ) {
78 145 100       525 exists( $value->{$k} ) or ( $is_optional{$k} ? next : return );
    100          
79 118 100       299 $constraints{$k}->check( $value->{$k} ) or return;
80             }
81 36         240 return !!1;
82 53         557 };
83             } #/ sub __constraint_generator
84              
85             sub __inline_generator {
86              
87             # We can only inline a parameterized Dict if all the
88             # constraints inside can be inlined.
89            
90 53 100 100 53   1473 my $slurpy =
91             @_
92             && Types::TypeTiny::is_TypeTiny( $_[-1] )
93             && $_[-1]->is_strictly_a_type_of( $_Slurpy )
94             ? pop->my_unslurpy
95             : undef;
96 53 50 66     304 return if $slurpy && !$slurpy->can_be_inlined;
97            
98             # Is slurpy a very loose type constraint?
99             # i.e. Any, Item, Defined, Ref, or HashRef
100 53   100     241 my $slurpy_is_any = $slurpy && $_hash->is_a_type_of( $slurpy );
101            
102             # Is slurpy a parameterized Map, or expressable as a parameterized Map?
103 53   66     226 my $slurpy_is_map =
104             $slurpy
105             && $slurpy->is_parameterized
106             && (
107             ( $slurpy->parent->strictly_equals( $_map ) && $slurpy->parameters )
108             || ( $slurpy->parent->strictly_equals( $_hash )
109             && [ $_any, $slurpy->parameters->[0] ] )
110             );
111            
112 53         190 my $iterator = pair_iterator @_;
113 53         148 my %constraints;
114             my @keys;
115            
116 53         187 while ( my ( $k, $c ) = $iterator->() ) {
117 84 100       305 return unless $c->can_be_inlined;
118 81         252 $constraints{$k} = $c;
119 81         216 push @keys, $k;
120             }
121            
122 50         313 my $regexp = join "|", map quotemeta, @keys;
123             return sub {
124 479     479   2474 require B;
125 479         928 my $h = $_[1];
126             join " and ",
127             Types::Standard::HashRef->inline_check( $h ),
128             (
129             $slurpy_is_any
130             ? ()
131             : $slurpy_is_map ? do {
132 168         671 '(not grep {' . "my \$v = ($h)->{\$_};" . sprintf(
133             'not((/\\A(?:%s)\\z/) or ((%s) and (%s)))',
134             $regexp,
135             $slurpy_is_map->[0]->inline_check( '$_' ),
136             $slurpy_is_map->[1]->inline_check( '$v' ),
137             ) . "} keys \%{$h})";
138             }
139             : $slurpy ? do {
140 19         110 'do {'
141             . "my \$slurpy_tmp = +{ map /\\A(?:$regexp)\\z/ ? () : (\$_ => ($h)->{\$_}), keys \%{$h} };"
142             . $slurpy->inline_check( '$slurpy_tmp' ) . '}';
143             }
144             : "not(grep !/\\A(?:$regexp)\\z/, keys \%{$h})"
145             ),
146             (
147             map {
148 479 100       1443 my $k = B::perlstring( $_ );
  785 100       2705  
    100          
149             $constraints{$_}->is_strictly_a_type_of( $_optional )
150             ? sprintf(
151             '(!exists %s->{%s} or %s)', $h, $k,
152             $constraints{$_}->inline_check( "$h\->{$k}" )
153             )
154             : (
155             "exists($h\->{$k})",
156 785 100       2426 $constraints{$_}->inline_check( "$h\->{$k}" )
157             )
158             } @keys
159             ),
160             ;
161             }
162 50         666 } #/ sub __inline_generator
163              
164             sub __deep_explanation {
165 8     8   45 require B;
166 8         24 my ( $type, $value, $varname ) = @_;
167 8         14 my @params = @{ $type->parameters };
  8         26  
168            
169 8 50 33     237 my $slurpy =
170             @params
171             && Types::TypeTiny::is_TypeTiny( $params[-1] )
172             && $params[-1]->is_strictly_a_type_of( $_Slurpy )
173             ? pop( @params )->my_unslurpy
174             : undef;
175 8         31 my $iterator = pair_iterator @params;
176 8         20 my %constraints;
177             my @keys;
178            
179 8         22 while ( my ( $k, $c ) = $iterator->() ) {
180 17         32 push @keys, $k;
181 17         50 $constraints{$k} = $c;
182             }
183            
184 8         30 for my $k ( @keys ) {
185             next
186             if $constraints{$k}->has_parent
187             && ( $constraints{$k}->parent == Types::Standard::Optional )
188 16 100 100     60 && ( !exists $value->{$k} );
      100        
189 14 100       67 next if $constraints{$k}->check( $value->{$k} );
190            
191             return [
192             sprintf( '"%s" requires key %s to appear in hash', $type, B::perlstring( $k ) )
193             ]
194 5 100       65 unless exists $value->{$k};
195            
196             return [
197             sprintf(
198             '"%s" constrains value at key %s of hash with "%s"',
199             $type,
200             B::perlstring( $k ),
201             $constraints{$k},
202             ),
203             @{
204 2         19 $constraints{$k}->validate_explain(
205 2         14 $value->{$k},
206             sprintf( '%s->{%s}', $varname, B::perlstring( $k ) ),
207             )
208             },
209             ];
210             } #/ for my $k ( @keys )
211            
212 3 50       11 if ( $slurpy ) {
213 0 0       0 my %tmp = map { exists( $constraints{$_} ) ? () : ( $_ => $value->{$_} ) }
  0         0  
214             keys %$value;
215            
216 0         0 my $explain = $slurpy->validate_explain( \%tmp, '$slurpy' );
217             return [
218 0 0       0 sprintf(
219             '"%s" requires the hashref of additional key/value pairs to conform to "%s"',
220             $type, $slurpy
221             ),
222             @$explain,
223             ] if $explain;
224             } #/ if ( $slurpy )
225             else {
226 3         14 for my $k ( sort keys %$value ) {
227             return [
228             sprintf(
229             '"%s" does not allow key %s to appear in hash', $type, B::perlstring( $k )
230             )
231             ]
232 6 100       109 unless exists $constraints{$k};
233             }
234             } #/ else [ if ( $slurpy ) ]
235            
236             # This should never happen...
237 0         0 return; # uncoverable statement
238             } #/ sub __deep_explanation
239              
240             my $label_counter = 0;
241             our ( $keycheck_counter, @KEYCHECK ) = -1;
242              
243             sub __coercion_generator {
244 29 100 66 29   831 my $slurpy =
245             @_
246             && Types::TypeTiny::is_TypeTiny( $_[-1] )
247             && $_[-1]->is_strictly_a_type_of( $_Slurpy )
248             ? pop->my_unslurpy
249             : undef;
250 29         149 my ( $parent, $child, %dict ) = @_;
251 29         166 my $C = "Type::Coercion"->new( type_constraint => $child );
252            
253 29         74 my $all_inlinable = 1;
254 29         67 my $child_coercions_exist = 0;
255 29         101 for my $tc ( values %dict ) {
256 50 100       150 $all_inlinable = 0 if !$tc->can_be_inlined;
257 50 100 100     171 $all_inlinable = 0 if $tc->has_coercion && !$tc->coercion->can_be_inlined;
258 50 100       161 $child_coercions_exist++ if $tc->has_coercion;
259             }
260 29 50 66     112 $all_inlinable = 0 if $slurpy && !$slurpy->can_be_inlined;
261 29 100 100     113 $all_inlinable = 0
      100        
262             if $slurpy
263             && $slurpy->has_coercion
264             && !$slurpy->coercion->can_be_inlined;
265            
266 29 100 100     163 $child_coercions_exist++ if $slurpy && $slurpy->has_coercion;
267 29 100       214 return unless $child_coercions_exist;
268            
269 18 100       57 if ( $all_inlinable ) {
270             $C->add_type_coercions(
271             $parent => Types::Standard::Stringable {
272 11     11   73 require B;
273            
274             my $keycheck = join "|", map quotemeta,
275 11 50       75 sort { length( $b ) <=> length( $a ) or $a cmp $b } keys %dict;
  10         80  
276 11         277 $keycheck = $KEYCHECK[ ++$keycheck_counter ] = qr{^($keycheck)$}ms; # regexp for legal keys
277            
278 11         58 my $label = sprintf( "DICTLABEL%d", ++$label_counter );
279 11         22 my @code;
280 11         26 push @code, 'do { my ($orig, $return_orig, $tmp, %new) = ($_, 0);';
281 11         43 push @code, "$label: {";
282 11 100       38 if ( $slurpy ) {
283 4         21 push @code,
284             sprintf(
285             'my $slurped = +{ map +($_=~$%s::KEYCHECK[%d])?():($_=>$orig->{$_}), keys %%$orig };',
286             __PACKAGE__, $keycheck_counter
287             );
288 4 100       16 if ( $slurpy->has_coercion ) {
289 3         15 push @code,
290             sprintf(
291             'my $coerced = %s;',
292             $slurpy->coercion->inline_coercion( '$slurped' )
293             );
294 3         27 push @code,
295             sprintf(
296             '((%s)&&(%s))?(%%new=%%$coerced):(($return_orig = 1), last %s);',
297             $_hash->inline_check( '$coerced' ), $slurpy->inline_check( '$coerced' ),
298             $label
299             );
300             } #/ if ( $slurpy->has_coercion)
301             else {
302 1         11 push @code,
303             sprintf(
304             '(%s)?(%%new=%%$slurped):(($return_orig = 1), last %s);',
305             $slurpy->inline_check( '$slurped' ), $label
306             );
307             }
308             } #/ if ( $slurpy )
309             else {
310 7         47 push @code,
311             sprintf(
312             '($_ =~ $%s::KEYCHECK[%d])||(($return_orig = 1), last %s) for sort keys %%$orig;',
313             __PACKAGE__, $keycheck_counter, $label
314             );
315             }
316 11         41 for my $k ( keys %dict ) {
317 20         57 my $ct = $dict{$k};
318 20         74 my $ct_coerce = $ct->has_coercion;
319 20         84 my $ct_optional = $ct->is_a_type_of( $_optional );
320 20         84 my $K = B::perlstring( $k );
321            
322 20 100       79 push @code, sprintf(
323             'if (exists $orig->{%s}) { $tmp = %s; (%s) ? ($new{%s}=$tmp) : (($return_orig=1), last %s) }',
324             $K,
325             $ct_coerce
326             ? $ct->coercion->inline_coercion( "\$orig->{$K}" )
327             : "\$orig->{$K}",
328             $ct->inline_check( '$tmp' ),
329             $K,
330             $label,
331             );
332             } #/ for my $k ( keys %dict )
333 11         51 push @code, '}';
334 11         41 push @code, '$return_orig ? $orig : \\%new';
335 11         237 push @code, '}';
336            
337             #warn "CODE:: @code";
338 11         364 "@code";
339             }
340 12         134 );
341             } #/ if ( $all_inlinable )
342            
343             else {
344             my %is_optional = map {
345 6         35 ;
346 12         36 $_ => !!$dict{$_}->is_strictly_a_type_of( $_optional )
347             } sort keys %dict;
348             $C->add_type_coercions(
349             $parent => sub {
350 7 50   7   246 my $value = @_ ? $_[0] : $_;
351 7         14 my %new;
352            
353 7 100       23 if ( $slurpy ) {
354 3 100       23 my %slurped = map exists( $dict{$_} ) ? () : ( $_ => $value->{$_} ),
355             keys %$value;
356            
357 3 100       12 if ( $slurpy->check( \%slurped ) ) {
    100          
358 1         4 %new = %slurped;
359             }
360             elsif ( $slurpy->has_coercion ) {
361 1         5 my $coerced = $slurpy->coerce( \%slurped );
362 1 50       5 $slurpy->check( $coerced ) ? ( %new = %$coerced ) : ( return $value );
363             }
364             else {
365 1         9 return $value;
366             }
367             } #/ if ( $slurpy )
368             else {
369 4         14 for my $k ( keys %$value ) {
370 8 50       23 return $value unless exists $dict{$k};
371             }
372             }
373            
374 6         26 for my $k ( keys %dict ) {
375 12 100 100     57 next if $is_optional{$k} and not exists $value->{$k};
376            
377 10         25 my $ct = $dict{$k};
378 10 100       34 my $x = $ct->has_coercion ? $ct->coerce( $value->{$k} ) : $value->{$k};
379            
380 10 50       158 return $value unless $ct->check( $x );
381            
382 10         177 $new{$k} = $x;
383             } #/ for my $k ( keys %dict )
384            
385 6         43 return \%new;
386             },
387 6         68 );
388             } #/ else [ if ( $all_inlinable ) ]
389            
390 18         62 return $C;
391             } #/ sub __coercion_generator
392              
393             sub __dict_is_slurpy {
394 89     89   159 my $self = shift;
395            
396 89 50       216 return !!0 if $self == Types::Standard::Dict();
397            
398             my $dict = $self->find_parent(
399 89 50   91   567 sub { $_->has_parent && $_->parent == Types::Standard::Dict() } );
  91         225  
400             my $slurpy =
401 89 100 100     300 @{ $dict->parameters }
402             && Types::TypeTiny::is_TypeTiny( $dict->parameters->[-1] )
403             && $dict->parameters->[-1]->is_strictly_a_type_of( $_Slurpy )
404             ? $dict->parameters->[-1]
405             : undef;
406             } #/ sub __dict_is_slurpy
407              
408             sub __hashref_allows_key {
409 75     75   129 my $self = shift;
410 75         154 my ( $key ) = @_;
411            
412 75 100       259 return Types::Standard::is_Str( $key ) if $self == Types::Standard::Dict();
413            
414             my $dict = $self->find_parent(
415 69 50   92   426 sub { $_->has_parent && $_->parent == Types::Standard::Dict() } );
  92         199  
416 69         262 my %params;
417 69         393 my $slurpy = $dict->my_dict_is_slurpy;
418 69 100       216 if ( $slurpy ) {
419 47         76 my @args = @{ $dict->parameters };
  47         111  
420 47         96 pop @args;
421 47         125 %params = @args;
422 47         239 $slurpy = $slurpy->my_unslurpy;
423             }
424             else {
425 22         43 %params = @{ $dict->parameters };
  22         51  
426             }
427            
428             return !!1
429 69 100       646 if exists( $params{$key} );
430 46 100       228 return !!0
431             if !$slurpy;
432 32 100 66     111 return Types::Standard::is_Str( $key )
      66        
      66        
433             if $slurpy == Types::Standard::Any()
434             || $slurpy == Types::Standard::Item()
435             || $slurpy == Types::Standard::Defined()
436             || $slurpy == Types::Standard::Ref();
437 20 100       89 return $slurpy->my_hashref_allows_key( $key )
438             if $slurpy->is_a_type_of( Types::Standard::HashRef() );
439 2         25 return !!0;
440             } #/ sub __hashref_allows_key
441              
442             sub __hashref_allows_value {
443 18     18   36 my $self = shift;
444 18         45 my ( $key, $value ) = @_;
445            
446 18 100       85 return !!0 unless $self->my_hashref_allows_key( $key );
447 16 50       81 return !!1 if $self == Types::Standard::Dict();
448            
449             my $dict = $self->find_parent(
450 16 50   18   118 sub { $_->has_parent && $_->parent == Types::Standard::Dict() } );
  18         56  
451 16         55 my %params;
452 16         91 my $slurpy = $dict->my_dict_is_slurpy;
453 16 100       58 if ( $slurpy ) {
454 12         23 my @args = @{ $dict->parameters };
  12         26  
455 12         25 pop @args;
456 12         33 %params = @args;
457 12         64 $slurpy = $slurpy->my_unslurpy;
458             }
459             else {
460 4         8 %params = @{ $dict->parameters };
  4         18  
461             }
462            
463             return !!1
464 16 100 100     78 if exists( $params{$key} ) && $params{$key}->check( $value );
465 10 100       99 return !!0
466             if !$slurpy;
467 8 50 33     31 return !!1
      33        
      33        
468             if $slurpy == Types::Standard::Any()
469             || $slurpy == Types::Standard::Item()
470             || $slurpy == Types::Standard::Defined()
471             || $slurpy == Types::Standard::Ref();
472 8 50       51 return $slurpy->my_hashref_allows_value( $key, $value )
473             if $slurpy->is_a_type_of( Types::Standard::HashRef() );
474 0           return !!0;
475             } #/ sub __hashref_allows_value
476              
477             1;