File Coverage

blib/lib/CatalystX/RequestModel/ContentBodyParser.pm
Criterion Covered Total %
statement 88 93 94.6
branch 32 38 84.2
condition 14 18 77.7
subroutine 17 19 89.4
pod 0 11 0.0
total 151 179 84.3


line stmt bran cond sub pod time code
1             package CatalystX::RequestModel::ContentBodyParser;
2              
3 6     6   65 use warnings;
  6         15  
  6         235  
4 6     6   33 use strict;
  6         15  
  6         155  
5 6     6   40 use Module::Runtime ();
  6         15  
  6         127  
6 6     6   2709 use CatalystX::RequestModel::Utils::InvalidJSONForValue;
  6         2403  
  6         235  
7 6     6   3337 use CatalystX::RequestModel::Utils::InvalidRequestNamespace;
  6         2188  
  6         232  
8 6     6   3089 use CatalystX::RequestModel::Utils::InvalidRequestNotIndexed;
  6         2193  
  6         217  
9 6     6   90 use Catalyst::Utils;
  6         19  
  6         6718  
10              
11 0     0 0 0 sub content_type { die "Must be overridden" }
12              
13 0     0 0 0 sub default_attr_rules { die "Must be overridden" }
14              
15             sub parse {
16 41     41 0 118 my ($self, $ns, $rules) = @_;
17 41         115 my %parsed = %{ $self->handle_data_encoded($self->{context}, $ns, $rules) };
  41         182  
18 41         440 return %parsed;
19             }
20              
21             sub _sorted {
22 11 100   11   29 return 1 if $a eq '';
23 9 100       23 return -1 if $b eq '';
24 7         21 return $a <=> $b;
25             }
26              
27             sub handle_data_encoded {
28 58     58 0 147 my ($self, $context, $ns, $rules, $indexed) = @_;
29 58         116 my $response = +{};
30              
31             # point $context to the namespace or die if not a valid namespace
32 58         142 foreach my $pointer (@$ns) {
33 7 100       24 if(exists($context->{$pointer})) {
34 6         17 $context = $context->{$pointer};
35             } else {
36 1         4 return $response
37             ## TODO maybe need a 'namespace_required 1' or something?
38             ##CatalystX::RequestModel::Utils::InvalidRequestNamespace->throw(ns=>join '.', @$ns);
39             }
40             }
41              
42 57         183 while(@$rules) {
43 156         254 my $current_rule = shift @{$rules};
  156         277  
44 156         612 my ($attr, $attr_rules) = %$current_rule;
45 156         433 my $data_name = $attr_rules->{name};
46 156         423 $attr_rules = $self->default_attr_rules($attr_rules);
47              
48 156 100       450 next unless exists $context->{$data_name}; # required handled by Moo/se required attribute
49              
50 130 100 100     534 if( !$indexed && $attr_rules->{indexed}) {
    100          
51              
52             # TODO move this into stand alone method and set some sort of condition
53 6 100 50     28 unless((ref($context->{$data_name})||'') eq 'ARRAY') {
54 4 50 50     12 if((ref($context->{$data_name})||'') eq 'HASH') {
55 4         9 my @values = ();
56 4         9 foreach my $index (sort _sorted keys %{$context->{$data_name}}) {
  4         33  
57 12         32 push @values, $context->{$data_name}{$index};
58             }
59 4         16 $context->{$data_name} = \@values;
60             } else {
61 0         0 CatalystX::RequestModel::Utils::InvalidRequestNotIndexed->throw(param=>$data_name);
62             }
63             }
64            
65 6         13 my @response_data;
66 6         12 foreach my $indexed_value(@{$context->{$data_name}}) {
  6         16  
67 17         72 my $indexed_response = $self->handle_data_encoded(+{ $data_name => $indexed_value}, [], [$current_rule], 1);
68 17         64 push @response_data, $indexed_response->{$data_name};
69             }
70              
71 6 50       23 if(@response_data) {
    0          
72 6         27 $response->{$data_name} = \@response_data;
73             } elsif(!$attr_rules->{omit_empty}) {
74 0         0 $response->{$data_name} = [];
75             }
76              
77             } elsif(my $nested_model = $attr_rules->{model}) {
78             $response->{$attr} = $self->{ctx}->model(
79             $self->normalize_nested_model_name($nested_model),
80             current_parser=>$self,
81 16         67 context=>$context->{$data_name},
82             );
83             } else {
84 108         215 my $value = $context->{$data_name};
85 108         268 $response->{$data_name} = $self->normalize_value($data_name, $value, $attr_rules);
86             }
87             }
88              
89 57         294 return $response;
90             }
91              
92             sub normalize_value {
93 108     108 0 240 my ($self, $param, $value, $key_rules) = @_;
94              
95 108 100       291 if($key_rules->{always_array}) {
    100          
96 2         13 $value = $self->normalize_always_array($value);
97             } elsif($key_rules->{flatten}) {
98 77         195 $value = $self->normalize_flatten($value);
99             }
100              
101 108 100 100     423 $value = $self->normalize_json($value, $param) if (($key_rules->{expand}||'') eq 'JSON');
102 108 100 100     375 $value = $self->normalize_boolean($value) if ($key_rules->{boolean}||'');
103              
104 108         493 return $value;
105             }
106              
107             sub normalize_always_array {
108 2     2 0 5 my ($self, $value) = @_;
109 2 50 50     16 $value = [$value] unless (ref($value)||'') eq 'ARRAY';
110 2         7 return $value;
111             }
112              
113             sub normalize_flatten{
114 77     77 0 165 my ($self, $value) = @_;
115 77 100 100     292 $value = $value->[-1] if (ref($value)||'') eq 'ARRAY';
116 77         171 return $value;
117             }
118              
119             sub normalize_boolean {
120 2     2 0 14 my ($self, $value) = @_;
121 2 100       33 return $value ? 1:0
122             }
123              
124             sub normalize_nested_model_name {
125 16     16 0 45 my ($self, $nested_model) = @_;
126 16 100       55 if($nested_model =~ /^::/) {
127 7         28 my $model_class_base = ref($self->{request_model});
128 7         30 my $prefix = Catalyst::Utils::class2classprefix($model_class_base);
129 7         156 $model_class_base =~s/^${prefix}\:\://;
130 7         26 $nested_model = "${model_class_base}${nested_model}";
131             }
132              
133 16         92 return $nested_model;
134             }
135              
136             my $_JSON_PARSER;
137             sub get_json_parser {
138 2     2 0 6 my $self = shift;
139 2   66     17 return $_JSON_PARSER ||= Module::Runtime::use_module('JSON::MaybeXS')->new(utf8 => 1);
140             }
141              
142             sub normalize_json {
143 2     2 0 13 my ($self, $value, $param) = @_;
144              
145             eval {
146 2         8 $value = $self->get_json_parser->decode($value);
147 2 50       4 } || do {
148 0         0 CatalystX::RequestModel::Utils::InvalidJSONForValue->throw(param=>$param, parsing_error=>$@);
149             };
150              
151 2         135 return $value;
152             }
153              
154             1;
155              
156             =head1 NAME
157              
158             CatalystX::RequestModel::ContentBodyParser - Content Parser base class
159              
160             =head1 SYNOPSIS
161              
162             TBD
163              
164             =head1 DESCRIPTION
165              
166             Base class for content parsers. Basically we need the ability to take a given POSTed
167             or PUTed (or PATCHed even I guess) content body and normalized it to a hash of data that
168             can be used to instantiate the request model. As well you need to be able to read the
169             meta data for each field and do things like flatten arrays (or inflate them, etc) and
170             so forth.
171              
172             This is lightly documented for now but there's not a lot of code and you can refer to the
173             packaged subclasses of this for hints on how to deal with your odd incoming content types.
174              
175             =head1 EXCEPTIONS
176              
177             This class can throw the following exceptions:
178              
179             =head2 Invalid JSON in value
180              
181             If you mark an attribute as "expand=>'JSON'" and the value isn't valid JSON then we throw
182             an L<CatalystX::RequestModel::Utils::InvalidJSONForValue> exception which if you are using
183             L<CatalystX::Errors> will be converted into a HTTP 400 Bad Request response (and also logging
184             to the error log the JSON parsing error).
185              
186             =head2 Invalid request parameter not indexed
187              
188             If a request parameter is marked as indexed but no indexed values (not arrayref) are found
189             we throw L<CatalystX::RequestModel::Utils::InvalidRequestNamespace>
190              
191             =head2 Invalid request no namespace
192              
193             If your request model defines a namespace but there's no matching namespace in the request
194             we throw a L<CatalystX::RequestModel::Utils::InvalidRequestNamespace>.
195              
196             =head1 METHODS
197              
198             This class defines the following public API
199              
200             =head2
201              
202             =head1 AUTHOR
203              
204             See L<CatalystX::RequestModel>.
205            
206             =head1 COPYRIGHT
207            
208             See L<CatalystX::RequestModel>.
209              
210             =head1 LICENSE
211            
212             See L<CatalystX::RequestModel>.
213            
214             =cut