File Coverage

blib/lib/TUWF/Validate/Interop.pm
Criterion Covered Total %
statement 68 130 52.3
branch 57 120 47.5
condition 43 140 30.7
subroutine 13 17 76.4
pod 0 6 0.0
total 181 413 43.8


line stmt bran cond sub pod time code
1             package TUWF::Validate::Interop;
2              
3 1     1   631 use strict;
  1         13  
  1         24  
4 1     1   5 use warnings;
  1         2  
  1         21  
5 1     1   4 use TUWF::Validate;
  1         2  
  1         27  
6 1     1   5 use Exporter 'import';
  1         12  
  1         24  
7 1     1   4 use Carp 'croak';
  1         2  
  1         1867  
8              
9             our @EXPORT_OK = ('analyze');
10             our $VERSION = '1.4';
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   113 my($c, $o) = @_;
21 103   100     188 my $n = $c->{name}||'';
22              
23 103 50 33     260 return if $o->{type} eq 'int' || $o->{type} eq 'bool';
24 103 100 100     218 $o->{type} = 'int' if $n eq 'int' || $n eq 'uint';
25 103 100 100     204 $o->{type} = 'bool' if $n eq 'anybool' || $n eq 'jsonbool';
26 103 100       163 $o->{type} = 'num' if $n eq 'num';
27             }
28              
29              
30             sub _merge {
31 103     103   137 my($c, $o) = @_;
32              
33 103         158 _merge_type $c, $o;
34              
35 103 100 100     218 $o->{required} = 1 if ($c->{name}||'') eq 'anybool';
36              
37 103 100 50     160 $o->{values} = _merge_toplevel($c->{schema}{values}, $o->{values}||{}) if $c->{schema}{values};
38              
39 103 100       137 if($c->{schema}{keys}) {
40 6   50     22 $o->{keys} ||= {};
41 6   50     8 $o->{keys}{$_} = _merge_toplevel($c->{schema}{keys}{$_}, $o->{keys}{$_}||{}) for keys %{$c->{schema}{keys}};
  6         30  
42             }
43              
44 103 50 33     158 $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     171 $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     146 $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     163 $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       147 push @{$o->{regexes}}, $c->{schema}{_analyze_regex} if defined $c->{schema}{_analyze_regex};
  25         50  
49              
50 103         104 _merge($_, $o) for @{$c->{validations}};
  103         204  
51             }
52              
53              
54             sub _merge_toplevel {
55 49     49   73 my($c, $o) = @_;
56 49   66     167 $o->{required} ||= $c->{schema}{required};
57 49   33     142 $o->{unknown} ||= $c->{schema}{unknown};
58 49 100       81 $o->{default} = $c->{schema}{default} if exists $c->{schema}{default};
59 49 50 33     100 $o->{type} = $c->{schema}{type} if !$o->{type} || $o->{type} eq 'any';
60              
61 49         96 _merge $c, $o;
62 49         142 bless $o, __PACKAGE__;
63             }
64              
65              
66             sub analyze {
67 41     41 0 52 my $c = shift;
68 41   33     132 $c->{analysis} ||= _merge_toplevel $c, {};
69             $c->{analysis}
70 41         91 }
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 62 my($o, $obj, %opt) = @_;
78 35   66     93 $opt{unknown} ||= $o->{unknown};
79 35 100       73 return undef if !defined $obj;
80 29 100       74 return $obj+0 if $o->{type} eq 'num';
81 27 100       66 return int $obj if $o->{type} eq 'int';
82 22 100       73 return $obj ? \1 : \0 if $o->{type} eq 'bool';
    100          
83 14 100       32 return "$obj" if $o->{type} eq 'scalar';
84 13 100 100     62 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       219 $opt{unknown} eq 'remove' ? ()
    100          
    100          
89             : croak "Unknown key '$_' in hash in coerce_for_json()"
90 12 100 100     47 } keys %$obj} if $o->{type} eq 'hash' && $o->{keys};
91 6         41 $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   490 local $_ = $_[0];
119 17         46 s/\\@/@/g;
120 17         155 s{\(\?\^?[alupimnsx]*(?:-[imnsx]+)?(?=[:\)])}{(?}g;
121 17         128 $_
122             }
123              
124              
125             sub _join_regexes {
126 9     9   13 my %r = map +($_,1), @{$_[0]};
  9         39  
127 9         27 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 18 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       88 $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             1;