File Coverage

blib/lib/Catalyst/Utils/StructuredParameters.pm
Criterion Covered Total %
statement 150 162 92.5
branch 66 88 75.0
condition 36 47 76.6
subroutine 17 19 89.4
pod 0 8 0.0
total 269 324 83.0


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