File Coverage

blib/lib/Catalyst/Utils/StructuredParameters.pm
Criterion Covered Total %
statement 135 147 91.8
branch 57 78 73.0
condition 32 40 80.0
subroutine 17 19 89.4
pod 0 8 0.0
total 241 292 82.5


line stmt bran cond sub pod time code
1             package Catalyst::Utils::StructuredParameters;
2              
3 2     2   17 use Moose;
  2         5  
  2         19  
4 2     2   15414 use Storable qw(dclone);
  2         7  
  2         193  
5 2     2   15 use Catalyst::Utils;
  2         7  
  2         87  
6 2     2   1185 use Catalyst::Exception::MissingParameter;
  2         932  
  2         22  
7 2     2   1858 use Catalyst::Exception::InvalidArrayPointer;
  2         860  
  2         24  
8 2     2   1407 use Catalyst::Exception::InvalidArrayLength;
  2         867  
  2         23  
9              
10             our $MAX_ARRAY_DEPTH = 1000;
11              
12             has context => (is=>'ro', required=>1);
13             has _namespace => (is=>'rw', required=>0, isa=>'ArrayRef', predicate=>'has_namespace', init_arg=>'namespace');
14             has _flatten_array_value => (is=>'ro', required=>1, init_arg=>'flatten_array_value');
15             has _current => (is=>'rw', required=>0, init_arg=>undef);
16             has _required => (is=>'rw', required=>0, init_arg=>undef);
17             has _src => (is=>'ro', required=>1, init_arg=>'src');
18             has _max_array_depth => (is=>'ro', required=>1, init_arg=>'max_array_depth', default=>$MAX_ARRAY_DEPTH);
19              
20             sub namespace {
21 2     2 0 8 my ($self, $arg) = @_;
22 2 50       93 $self->_namespace($arg) if defined($arg);
23 2         36 return $self;
24             }
25              
26             sub flatten_array_value {
27 0     0 0 0 my ($self, $arg) = @_;
28 0 0       0 $self->_flatten_array_value($arg) if defined($arg);
29 0         0 return $self;
30             }
31              
32             sub max_array_depth {
33 34     34 0 74 my ($self, $arg) = @_;
34 34 50       87 $self->_max_array_depth($arg) if defined($arg);
35 34         101 return $self;
36             }
37              
38             sub permitted {
39 10     10 0 45 my ($self, @proto) = @_;
40 10   100     435 my $namespace = $self->_namespace ||[];
41 10         448 $self->_required(0);
42              
43 10 100       40 if(ref $proto[0]) {
44 4         20 my $namespace_affix = shift @proto;
45 4         25 $namespace = [ @$namespace, @$namespace_affix ];
46             }
47              
48 10         393 my $context = dclone($self->context);
49 10         89 my $parsed = $self->_parse($context, $namespace, [@proto]);
50 10   100     417 my $current = $self->_current ||+{};
51 10         67 $current = Catalyst::Utils::merge_hashes($current, $parsed);
52 10         767 $self->_current($current);
53              
54 10         104 return $self;
55             }
56              
57             sub required {
58 4     4 0 17 my ($self, @proto) = @_;
59 4   50     149 my $namespace = $self->_namespace ||[];
60 4         149 $self->_required(1);
61              
62 4 50       15 if(ref $proto[0]) {
63 0         0 my $namespace_affix = shift @proto;
64 0         0 $namespace = [ @$namespace, @$namespace_affix ];
65             }
66              
67 4         147 my $context = dclone($self->context);
68 4         28 my $parsed = $self->_parse($context, $namespace, [@proto]);
69 2   50     77 my $current = $self->_current ||+{};
70 2         11 $current = Catalyst::Utils::merge_hashes($current, $parsed);
71 2         116 $self->_current($current);
72              
73 2         13 return $self;
74             }
75              
76             sub to_hash {
77 6     6 0 29 my $self = shift;
78 6 50       13 return %{ $self->_current || +{} };
  6         225  
79             }
80              
81             sub keys {
82 0     0 0 0 my $self = shift;
83 0 0       0 return CORE::keys %{ $self->_current || +{} };
  0         0  
84             }
85              
86             sub get {
87 1     1 0 380 my $self = shift;
88 1 50       4 return @{ $self->_current || +{} }{@_};
  1         38  
89             }
90              
91             sub _sorted {
92 25 100   25   57 return 1 if $a eq '';
93 23 100       53 return -1 if $b eq '';
94 20         59 return $a <=> $b;
95             }
96              
97             sub _normalize_array_value {
98 112     112   249 my ($self, $value) = @_;
99 112 100       4601 return $value unless $self->_flatten_array_value;
100 62 100 100     352 return ((ref($value)||'') eq 'ARRAY') ? $value->[-1] : $value;
101             }
102              
103             sub _parse {
104 14     14   52 my ($self, @args) = @_;
105 14 100       697 return $self->_src eq 'data' ? $self->_parse_data(@args) : $self->_parse_formlike(@args);
106             }
107              
108             sub _parse_formlike {
109 56     56   134 my ($self, $context, $ns, $rules) = @_;
110 56         106 my $current = +{};
111 56         105 while(@{$rules}) {
  184         426  
112 130         201 my $rule = shift @{$rules};
  130         248  
113 130 100 100     429 if(ref($rule)||'' eq 'HASH') {
114 24         89 my ($local_ns, $rules) = %$rule;
115 24         70 my $key = join('.', @$ns, $local_ns);
116 24         46 my %indexes = ();
117 24         220 foreach my $context_field (CORE::keys %$context) {
118 698         2502 my ($i, $under) = ($context_field =~m/^\Q$key\E\[(\d*)\]\.?(.*)$/);
119 698 100       1704 next unless defined $i;
120 62         156 $indexes{$i} = $under;
121             }
122              
123 24         90 my $found_array_depth = scalar CORE::keys %indexes;
124 24 50       87 Catalyst::Exception::InvalidArrayLength->throw(
125             pointer=>$local_ns,
126             max=>$self->max_array_depth,
127             attempted=>$found_array_depth
128             ) if $found_array_depth > $self->max_array_depth;
129              
130 24         137 foreach my $index(sort _sorted CORE::keys %indexes) {
131 27         725 my $cloned_rules = dclone($rules); # each iteration in the loop needs its own copy of the rules;
132 27 100       110 $cloned_rules = [''] unless @$cloned_rules; # to handle the bare array case
133 27         144 my $value = $self->_parse_formlike( $context, [@$ns, "${local_ns}[$index]"], $cloned_rules);
134             ## I don't think these are missing params, just a row with invalid fields
135 27 100 100     142 next if( (ref($value)||'') eq 'HASH') && !%$value;
      100        
136 26         49 push @{$current->{$local_ns}}, $value;
  26         110  
137             }
138             } else {
139 106 100 100     403 if((ref($rules->[0])||'') eq 'ARRAY') {
140 17         110 my $value = $self->_parse_formlike( $context, [@$ns, $rule], shift(@$rules) );
141 17 100       94 next unless %$value; # For 'permitted';
142 10         47 $current->{$rule} = $value;
143             } else {
144 89 100       192 if($rule eq '') {
145 14         43 my $key = join('.', @$ns);
146 14 50       53 unless(defined $context->{$key}) {
147 0 0       0 $self->_required ? Catalyst::Exception::MissingParameter->throw(param=>$key) : next;
148             }
149 14         45 $current = $self->_normalize_array_value($context->{$key});
150             } else {
151 75         203 my $key = join('.', @$ns, $rule);
152 75 100       230 unless(defined $context->{$key}) {
153 27 100       1010 $self->_required ? Catalyst::Exception::MissingParameter->throw(param=>$key) : next;
154             }
155 48         124 $current->{$rule} = $self->_normalize_array_value($context->{$key});
156             }
157             }
158             }
159             }
160 54         125 return $current;
161             }
162              
163             sub _parse_data {
164 38     38   98 my ($self, $context, $ns, $rules) = @_;
165 38         65 my $current = +{};
166 38         69 MAIN: while(@{$rules}) {
  136         307  
167 98         160 my $rule = shift @{$rules};
  98         184  
168 98 100 100     312 if(ref($rule)||'' eq 'HASH') {
169 20         68 my ($local_ns, $rules) = %$rule;
170 20         37 my $value = $context;
171 20         44 foreach my $pointer (@$ns, $local_ns) {
172 30 100       73 if(exists($value->{$pointer})) {
173 20         43 $value = $value->{$pointer};
174             } else {
175 10 50       368 $self->_required ? Catalyst::Exception::MissingParameter->throw(param=>join('.', (@$ns, $local_ns))) : next MAIN;
176             }
177             }
178              
179 10 50 50     40 Catalyst::Exception::InvalidArrayPointer->throw(pointer=>join('.', (@$ns, $local_ns))) unless (ref($value)||'') eq 'ARRAY';
180              
181 10         19 my $found_array_depth = scalar @$value;
182 10 50       35 Catalyst::Exception::InvalidArrayLength->throw(
183             pointer=>$local_ns,
184             max=>$self->max_array_depth,
185             attempted=>$found_array_depth
186             ) if $found_array_depth > $self->max_array_depth;
187            
188 10         22 my @gathered = ();
189              
190 10         22 foreach my $item (@$value) {
191 25         577 my $cloned_rules = dclone($rules); # each iteration in the loop needs its own copy of the rules;
192 25 100       93 $cloned_rules = [''] unless @$cloned_rules; # to handle the bare array case
193 25         72 my $value = $self->_parse_data($item, [], $cloned_rules);
194             ## I don't think these are missing params, just a row with invalid fields
195 25 100 100     119 next if( (ref($value)||'') eq 'HASH') && !%$value;
      100        
196 24         68 push @gathered, $value;
197             }
198 10         38 $current->{$local_ns} = \@gathered;
199             } else {
200 78 100 100     249 if((ref($rules->[0])||'') eq 'ARRAY') {
201 11         72 my $value = $self->_parse_data( $context, [@$ns, $rule], shift(@$rules) );
202 11 100       40 next unless %$value; # For 'permitted';
203 6         20 $current->{$rule} = $value;
204             } else {
205 67 100       126 if($rule eq '') {
206 14         25 my $value = $context;
207 14         33 foreach my $pointer (@$ns) {
208 0 0 0     0 if(((ref($value)||'') eq 'HASH') && exists($value->{$pointer})) {
      0        
209 0         0 $value = $value->{$pointer};
210             } else {
211 0 0       0 $self->_required ? Catalyst::Exception::MissingParameter->throw(param=>join('.', (@$ns, $rule))) : next MAIN;
212             }
213             }
214 14         34 $current = $self->_normalize_array_value($value);
215             } else {
216 53         91 my $value = $context;
217 53         107 foreach my $pointer (@$ns, $rule) {
218 91 100 100     332 if(((ref($value)||'') eq 'HASH') && exists($value->{$pointer})) {
      100        
219 74         161 $value = $value->{$pointer};
220             } else {
221 17 50       628 $self->_required ? Catalyst::Exception::MissingParameter->throw(param=>join('.', (@$ns, $rule))) : next MAIN;
222             }
223             }
224 36         90 $current->{$rule} = $self->_normalize_array_value($value);
225             }
226             }
227             }
228             }
229 38         91 return $current;
230             }
231              
232              
233             __PACKAGE__->meta->make_immutable;
234              
235             1;
236              
237             =head1 NAME
238              
239             Catalyst::Utils::StructuredParameters - Enforce structural rules on your body and data parameters
240              
241             =head1 SYNOPSIS
242              
243             =head1 DESCRIPTION
244              
245             See L<Catalyst::TraitFor::Request::StructuredParameters> for usage. These are just utility classes
246             and not likely useful for end user unless you are rolling your own parsing or something. All
247             the publically useful docs are there.
248              
249             =head1 ATTRIBUTES
250              
251             This role defines the following attributes:
252              
253             TBD
254              
255             =head1 METHODS
256              
257             This role defines the following methods:
258              
259             TBD
260              
261             =head1 AUTHOR
262              
263             See L<Catalyst::TraitFor::Request::StructuredParameters>
264              
265             =head1 SEE ALSO
266              
267             L<Catalyst>, L<Catalyst::Request>
268              
269             =head1 COPYRIGHT & LICENSE
270              
271             See L<Catalyst::TraitFor::Request::StructuredParameters>
272              
273             =cut