File Coverage

blib/lib/TUWF/Validate/Interop.pm
Criterion Covered Total %
statement 68 176 38.6
branch 57 146 39.0
condition 45 184 24.4
subroutine 13 19 68.4
pod 0 7 0.0
total 183 532 34.4


line stmt bran cond sub pod time code
1             package TUWF::Validate::Interop;
2              
3 1     1   750 use strict;
  1         15  
  1         30  
4 1     1   5 use warnings;
  1         2  
  1         23  
5 1     1   5 use TUWF::Validate;
  1         1  
  1         34  
6 1     1   5 use Exporter 'import';
  1         2  
  1         25  
7 1     1   5 use Carp 'croak';
  1         1  
  1         3188  
8              
9             our @EXPORT_OK = ('analyze');
10             our $VERSION = '1.5';
11              
12              
13             # Analyzed ("flattened") object:
14             # { type => scalar | bool | num | int | array | hash | any
15             # , min, max, minlength, maxlength, required, regexes
16             # , keys, values, unknown
17             # }
18              
19             sub _merge_type {
20 103     103   144 my($c, $o) = @_;
21 103   100     231 my $n = $c->{name}||'';
22              
23 103 50 33     306 return if $o->{type} eq 'int' || $o->{type} eq 'bool';
24 103 100 100     276 $o->{type} = 'int' if $n eq 'int' || $n eq 'uint';
25 103 100 66     390 $o->{type} = 'bool' if $n eq 'anybool' || $n eq 'undefbool' || $n eq 'jsonbool';
      100        
26 103 100       213 $o->{type} = 'num' if $n eq 'num';
27             }
28              
29              
30             sub _merge {
31 103     103   166 my($c, $o) = @_;
32              
33 103         202 _merge_type $c, $o;
34              
35 103 100 100     263 $o->{required} = 1 if ($c->{name}||'') eq 'anybool';
36              
37 103 100 50     191 $o->{values} = _merge_toplevel($c->{schema}{values}, $o->{values}||{}) if $c->{schema}{values};
38              
39 103 100       189 if($c->{schema}{keys}) {
40 6   50     26 $o->{keys} ||= {};
41 6   50     11 $o->{keys}{$_} = _merge_toplevel($c->{schema}{keys}{$_}, $o->{keys}{$_}||{}) for keys %{$c->{schema}{keys}};
  6         29  
42             }
43              
44 103 50 33     216 $o->{minlength} = $c->{schema}{_analyze_minlength} if defined $c->{schema}{_analyze_minlength} && (!defined $o->{minlength} || $o->{minlength} < $c->{schema}{_analyze_minlength});
      66        
45 103 50 33     234 $o->{maxlength} = $c->{schema}{_analyze_maxlength} if defined $c->{schema}{_analyze_maxlength} && (!defined $o->{maxlength} || $o->{maxlength} > $c->{schema}{_analyze_maxlength});
      66        
46 103 100 100     195 $o->{min} = $c->{schema}{_analyze_min} if defined $c->{schema}{_analyze_min} && (!defined $o->{min} || $o->{min} < $c->{schema}{_analyze_min} );
      100        
47 103 50 66     228 $o->{max} = $c->{schema}{_analyze_max} if defined $c->{schema}{_analyze_max} && (!defined $o->{max} || $o->{max} > $c->{schema}{_analyze_max} );
      66        
48 103 100       188 push @{$o->{regexes}}, $c->{schema}{_analyze_regex} if defined $c->{schema}{_analyze_regex};
  25         62  
49              
50 103         138 _merge($_, $o) for @{$c->{validations}};
  103         250  
51             }
52              
53              
54             sub _merge_toplevel {
55 49     49   90 my($c, $o) = @_;
56 49   66     193 $o->{required} ||= $c->{schema}{required};
57 49   33     177 $o->{unknown} ||= $c->{schema}{unknown};
58 49 100       102 $o->{default} = $c->{schema}{default} if exists $c->{schema}{default};
59 49 50 33     145 $o->{type} = $c->{schema}{type} if !$o->{type} || $o->{type} eq 'any';
60              
61 49         123 _merge $c, $o;
62 49         159 bless $o, __PACKAGE__;
63             }
64              
65              
66             sub analyze {
67 41     41 0 63 my $c = shift;
68 41   33     151 $c->{analysis} ||= _merge_toplevel $c, {};
69             $c->{analysis}
70 41         113 }
71              
72              
73             # Assumes that $obj already has the required format/structure, odd things may
74             # happen if this is not the case.
75             # unknown => remove|reject|pass
76             sub coerce_for_json {
77 35     35 0 74 my($o, $obj, %opt) = @_;
78 35   66     109 $opt{unknown} ||= $o->{unknown};
79 35 100       101 return undef if !defined $obj;
80 29 100       79 return $obj+0 if $o->{type} eq 'num';
81 27 100       95 return int $obj if $o->{type} eq 'int';
82 22 100       87 return $obj ? \1 : \0 if $o->{type} eq 'bool';
    100          
83 14 100       39 return "$obj" if $o->{type} eq 'scalar';
84 13 100 100     35 return [map $o->{values}->coerce_for_json($_, %opt), @$obj] if $o->{type} eq 'array' && $o->{values};
85             return {map {
86             $o->{keys}{$_} ? ($_, $o->{keys}{$_}->coerce_for_json($obj->{$_}, %opt)) :
87             $opt{unknown} eq 'pass' ? ($_, $obj->{$_}) :
88 10 100       214 $opt{unknown} eq 'remove' ? ()
    100          
    100          
89             : croak "Unknown key '$_' in hash in coerce_for_json()"
90 12 100 100     49 } keys %$obj} if $o->{type} eq 'hash' && $o->{keys};
91 6         46 $obj
92             }
93              
94              
95             # Returns a Cpanel::JSON::XS::Type; Behavior is subtly different compared to coerce_for_json():
96             # - Unknown keys in hashes will cause Cpanel::JSON::XS to die()
97             # - Numbers are always formatted as floats (e.g. 10.0) even if it's a round nunmber
98             sub json_type {
99 0     0 0 0 my $o = shift;
100 0         0 require Cpanel::JSON::XS::Type;
101 0 0       0 return Cpanel::JSON::XS::Type::JSON_TYPE_FLOAT_OR_NULL() if $o->{type} eq 'num';
102 0 0       0 return Cpanel::JSON::XS::Type::JSON_TYPE_INT_OR_NULL() if $o->{type} eq 'int';
103 0 0       0 return Cpanel::JSON::XS::Type::JSON_TYPE_BOOL_OR_NULL() if $o->{type} eq 'bool';
104 0 0       0 return Cpanel::JSON::XS::Type::JSON_TYPE_STRING_OR_NULL() if $o->{type} eq 'scalar';
105 0 0       0 return Cpanel::JSON::XS::Type::json_type_null_or_anyof(Cpanel::JSON::XS::Type::json_type_arrayof($o->{values} ? $o->{values}->json_type : undef)) if $o->{type} eq 'array';
    0          
106 0 0 0     0 return Cpanel::JSON::XS::Type::json_type_null_or_anyof({ map +($_, $o->{keys}{$_}->json_type), keys %{$o->{keys}} }) if $o->{type} eq 'hash' && $o->{keys};
  0         0  
107 0 0       0 return Cpanel::JSON::XS::Type::json_type_null_or_anyof(Cpanel::JSON::XS::Type::json_type_hashof(undef)) if $o->{type} eq 'hash';
108             undef
109 0         0 }
110              
111              
112             # Attempts to convert a stringified Perl regex into something that is compatible with JS.
113             # - @ should not be escaped
114             # - (?^: is a perl alias for (?d-imnsx:
115             # - Javascript doesn't officially support embedded modifiers in the first place, so these are removed
116             # Regexes compiled with any of /imsx will not work properly.
117             sub _re_compat {
118 17     17   564 local $_ = $_[0];
119 17         45 s/\\@/@/g;
120 17         162 s{\(\?\^?[alupimnsx]*(?:-[imnsx]+)?(?=[:\)])}{(?}g;
121 17         123 $_
122             }
123              
124              
125             sub _join_regexes {
126 9     9   13 my %r = map +($_,1), @{$_[0]};
  9         47  
127 9         31 my @r = sort keys %r;
128 9         44 _re_compat join('', map "(?=$_)", @r[0..$#r-1]).$r[$#r]
129             }
130              
131              
132             # Returns a few HTML5 validation properties. Doesn't include the 'type'
133             sub html5_validation {
134 15     15 0 23 my $o = shift;
135             +(
136             $o->{required} ? (required => 'required') : (),
137             defined $o->{minlength} ? (minlength => $o->{minlength}) : (),
138             defined $o->{maxlength} ? (maxlength => $o->{maxlength}) : (),
139             defined $o->{min} ? (min => $o->{min} ) : (),
140             defined $o->{max} ? (max => $o->{max} ) : (),
141 15 100       112 $o->{regexes} ? (pattern => _join_regexes $o->{regexes}) : (),
    100          
    100          
    100          
    100          
    100          
142             );
143             }
144              
145              
146              
147             # The elm_ are experimental, unstable, not very well-tested and for Elm 0.19
148              
149             # Options: required any array values keys indent level
150             sub elm_type {
151 0     0 0   my($o, %opt) = @_;
152 0 0   0     my $par = delete $opt{_need_parens} ? sub { "($_[0])" } : sub { $_[0] };
  0            
  0            
153 0 0 0       return $par->('Maybe ' . $o->elm_type(%opt, required => 1, _need_parens => 1)) if !$o->{required} && !defined $o->{default} && !$opt{required};
      0        
154 0           delete $opt{required};
155 0 0         return 'String' if $o->{type} eq 'scalar';
156 0 0         return 'Bool' if $o->{type} eq 'bool';
157 0 0         return 'Float' if $o->{type} eq 'num';
158 0 0         return 'Int' if $o->{type} eq 'int';
159 0 0 0       return $opt{any} if $o->{type} eq 'any' && $opt{any};
160             return $par->( ($opt{array} || 'List') . ' ' . ($opt{values} || $o->{values}->elm_type(%opt, _need_parens => 1)) )
161 0 0 0       if $o->{type} eq 'array' && ($opt{values} || $o->{values});
      0        
      0        
      0        
162              
163 0 0 0       if($o->{type} eq 'hash' && ($o->{keys} || $opt{keys})) {
      0        
164 0   0       $opt{indent} //= 2;
165 0   0       $opt{level} //= 1;
166 0           my $len = 0;
167 0 0         $len = length $_ > $len ? length $_ : $len for keys %{$o->{keys}};
  0            
168              
169             my $r = "\n{ " . join("\n, ", map {
170 0   0       sprintf "%-*s : %s", $len, $_, $opt{keys}{$_} || $o->{keys}{$_}->elm_type(%opt, level => $opt{level}+1);
171 0           } sort keys %{$o->{keys}}) . "\n}";;
  0            
172              
173 0 0         $r =~ s/\n/$opt{indent} ? "\n" . (' 'x($opt{indent}*$opt{level})) : ''/eg;
  0            
174 0           return $r;
175             }
176              
177 0           croak "Unknown type '$o->{type}' or missing option";
178             }
179              
180              
181             # Elm JSON encoder for values of elm_type()
182             # options: elm_type() options + json_encode var_prefix
183             sub elm_encoder {
184 0     0 0   my($o, %opt) = @_;
185 0   0       $opt{json_encode} //= '';
186 0   0       $opt{var_prefix} //= 'e';
187 0   0       $opt{var_num} //= 0;
188              
189             return sprintf '(Maybe.withDefault %snull << Maybe.map %s)',
190             $opt{json_encode}, $opt{values} || $o->elm_encoder(%opt, required => 1)
191 0 0 0       if !$o->{required} && !defined $o->{default} && !$opt{required};
      0        
      0        
192              
193 0           delete $opt{required};
194 0 0         return "$opt{json_encode}string" if $o->{type} eq 'scalar';
195 0 0         return "$opt{json_encode}bool" if $o->{type} eq 'bool';
196 0 0         return "$opt{json_encode}float" if $o->{type} eq 'num';
197 0 0         return "$opt{json_encode}int" if $o->{type} eq 'int';
198 0 0 0       return $opt{any} if $o->{type} eq 'any' && $opt{any};
199             return sprintf '(%slist %s)', $opt{json_encode}, $opt{values} || $o->{values}->elm_encoder(%opt)
200 0 0 0       if $o->{type} eq 'array' && ($opt{values} || $o->{values});
      0        
      0        
201              
202 0 0 0       if($o->{type} eq 'hash' && ($o->{keys} || $opt{keys})) {
      0        
203 0   0       $opt{indent} //= 2;
204 0   0       $opt{level} //= 1;
205 0           my $len = 0;
206 0 0         $len = length $_ > $len ? length $_ : $len for keys %{$o->{keys}};
  0            
207              
208 0           my $var = $opt{var_prefix}.$opt{var_num};
209             my $r = sprintf "(\\%s -> %sobject\n[ %s\n])", $var, $opt{json_encode}, join "\n, ", map {
210             sprintf '("%s",%s %s %s.%1$s)', $_,
211             ' 'x($len-(length $_)),
212 0   0       $opt{keys}{$_} || $o->{keys}{$_}->elm_encoder(%opt, level => $opt{level}+1, var_num => $opt{var_num}+1),
213             $var;
214 0           } sort keys %{$o->{keys}};
  0            
215              
216 0 0         $r =~ s/\n/$opt{indent} ? "\n" . (' 'x($opt{indent}*$opt{level})) : ''/eg;
  0            
217 0           return $r;
218             }
219              
220 0           croak "Unknown type '$o->{type}' or missing option";
221             }
222              
223              
224             # Elm JSON decoder for values of elm_type()
225             # options: elm_type() options + json_decode var_prefix
226             sub elm_decoder {
227 0     0 0   my($o, %opt) = @_;
228 0   0       $opt{json_decode} //= '';
229 0   0       $opt{var_prefix} //= 'd';
230              
231             return sprintf '(%snullable %s)',
232             $opt{json_decode}, $opt{values} || $o->elm_decoder(%opt, required => 1)
233 0 0 0       if !$o->{required} && !defined $o->{default} && !$opt{required};
      0        
      0        
234              
235 0           delete $opt{required};
236 0 0         return "$opt{json_decode}string" if $o->{type} eq 'scalar';
237 0 0         return "$opt{json_decode}bool" if $o->{type} eq 'bool';
238 0 0         return "$opt{json_decode}float" if $o->{type} eq 'num';
239 0 0         return "$opt{json_decode}int" if $o->{type} eq 'int';
240 0 0 0       return $opt{any} if $o->{type} eq 'any' && $opt{any};
241 0 0         return "$opt{json_decode}value" if $o->{type} eq 'any';
242             return sprintf '(%slist %s)', $opt{json_decode}, $opt{values} || $o->{values}->elm_decoder(%opt)
243 0 0 0       if $o->{type} eq 'array' && ($opt{values} || $o->{values});
      0        
      0        
244              
245 0 0 0       if($o->{type} eq 'hash' && ($o->{keys} || $opt{keys})) {
      0        
246 0   0       $opt{indent} //= 2;
247 0   0       $opt{level} //= 1;
248 0           my $len = 0;
249 0 0         $len = length $_ > $len ? length $_ : $len for keys %{$o->{keys}};
  0            
250              
251 0           my $r;
252 0           my $num = keys %{$o->{keys}};
  0            
253 0           my $varnum = 1;
254 0     0     my $getvar = sub { $opt{var_prefix}.($varnum++) };
  0            
255              
256             # For 8 members or less we can use the simple Json.Decode.map* functions.
257 0 0         if($num <= 8) {
258 0           my(@fnarg, @assign, @fetch);
259 0           for (sort keys %{$o->{keys}}) {
  0            
260 0           my $var = $getvar->();
261 0           push @fnarg, $var;
262 0           push @assign, "$_ = $var";
263             push @fetch, sprintf '(%sfield "%s"%s %s)', $opt{json_decode}, $_,
264             ' 'x($len-(length $_)),
265 0   0       $opt{keys}{$_} || $o->{keys}{$_}->elm_decoder(%opt, var_prefix => $var, level => $opt{level}+1);
266             }
267             $r = sprintf "(%smap%s\n(\\%s -> { %s })\n%s)",
268 0 0         $opt{json_decode}, $num == 1 ? '' : $num, join(' ', @fnarg), join(', ', @assign), join("\n", @fetch);
269              
270             # For larger hashes we go through Json.Decode.dict and a little custom decoding logic.
271             # Json.Decode only allows failing with an error string, so the error messages aren't as good.
272             } else {
273 0           my($dict, $fn, $name, $dec, $next, $cap) = map $getvar->(), 1..6;
274 0           my(@assign, @fn);
275 0           for (sort keys %{$o->{keys}}) {
  0            
276 0           my $var = $getvar->();
277 0           push @assign, "$_ = $var";
278             push @fn, sprintf '%s "%s"%s %s (\%s ->', $fn, $_,
279             ' 'x($len-(length $_)),
280 0   0       $opt{keys}{$_} || $o->{keys}{$_}->elm_decoder(%opt, var_prefix => "${var}_", level => $opt{level}+1),
281             $var;
282             }
283 0           my $spc = ' 'x(12 + length($fn) + length($name) + length($dec) + length($next));
284 0           $r = "($opt{json_decode}andThen (\\$dict -> \n"
285             ."let $fn $name $dec $next = case Maybe.map ($opt{json_decode}decodeValue $dec) (Dict.get $name $dict) of\n"
286             ."${spc}Nothing -> $opt{json_decode}fail (\"Missing key '\"++$name++\"'\")\n"
287             ."${spc}Just (Err $cap) -> $opt{json_decode}fail (\"Error decoding value of '\"++$name++\"': \"++($opt{json_decode}errorToString $cap))\n"
288             ."${spc}Just (Ok $cap) -> $next $cap\n"
289             ."in ".join("\n ", @fn)."\n"
290             ." $opt{json_decode}succeed { ".join(', ', @assign)." }\n"
291             .')'.(')'x@fn)." ($opt{json_decode}dict $opt{json_decode}value))";
292             }
293              
294 0 0         $r =~ s/\n/$opt{indent} ? "\n" . (' 'x($opt{indent}*$opt{level})) : ''/eg;
  0            
295 0           return $r;
296             }
297              
298 0           croak "Unknown type '$o->{type}' or missing option";
299             }
300              
301              
302             1;