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   54 use warnings;
  6         17  
  6         193  
4 6     6   33 use strict;
  6         12  
  6         174  
5 6     6   32 use Module::Runtime ();
  6         13  
  6         132  
6 6     6   2682 use CatalystX::RequestModel::Utils::InvalidJSONForValue;
  6         2298  
  6         226  
7 6     6   3322 use CatalystX::RequestModel::Utils::InvalidRequestNamespace;
  6         2145  
  6         225  
8 6     6   2982 use CatalystX::RequestModel::Utils::InvalidRequestNotIndexed;
  6         2279  
  6         289  
9 6     6   69 use Catalyst::Utils;
  6         16  
  6         6756  
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 122 my ($self, $ns, $rules) = @_;
17 41         87 my %parsed = %{ $self->handle_data_encoded($self->{context}, $ns, $rules) };
  41         178  
18 41         399 return %parsed;
19             }
20              
21             sub _sorted {
22 12 100   12   28 return 1 if $a eq '';
23 10 100       29 return -1 if $b eq '';
24 8         20 return $a <=> $b;
25             }
26              
27             sub handle_data_encoded {
28 58     58 0 153 my ($self, $context, $ns, $rules, $indexed) = @_;
29 58         108 my $response = +{};
30              
31             # point $context to the namespace or die if not a valid namespace
32 58         137 foreach my $pointer (@$ns) {
33 7 100       26 if(exists($context->{$pointer})) {
34 6         21 $context = $context->{$pointer};
35             } else {
36 1         5 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         146 while(@$rules) {
43 156         262 my $current_rule = shift @{$rules};
  156         285  
44 156         608 my ($attr, $attr_rules) = %$current_rule;
45 156         438 my $data_name = $attr_rules->{name};
46 156         419 $attr_rules = $self->default_attr_rules($attr_rules);
47              
48 156 100       490 next unless exists $context->{$data_name}; # required handled by Moo/se required attribute
49              
50 130 100 100     547 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     32 unless((ref($context->{$data_name})||'') eq 'ARRAY') {
54 4 50 50     15 if((ref($context->{$data_name})||'') eq 'HASH') {
55 4         7 my @values = ();
56 4         9 foreach my $index (sort _sorted keys %{$context->{$data_name}}) {
  4         28  
57 12         36 push @values, $context->{$data_name}{$index};
58             }
59 4         17 $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         16 foreach my $indexed_value(@{$context->{$data_name}}) {
  6         16  
67 17         75 my $indexed_response = $self->handle_data_encoded(+{ $data_name => $indexed_value}, [], [$current_rule], 1);
68 17         61 push @response_data, $indexed_response->{$data_name};
69             }
70              
71 6 50       24 if(@response_data) {
    0          
72 6         30 $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         52 context=>$context->{$data_name},
82             );
83             } else {
84 108         213 my $value = $context->{$data_name};
85 108         281 $response->{$data_name} = $self->normalize_value($data_name, $value, $attr_rules);
86             }
87             }
88              
89 57         267 return $response;
90             }
91              
92             sub normalize_value {
93 108     108 0 253 my ($self, $param, $value, $key_rules) = @_;
94              
95 108 100       295 if($key_rules->{always_array}) {
    100          
96 2         9 $value = $self->normalize_always_array($value);
97             } elsif($key_rules->{flatten}) {
98 77         190 $value = $self->normalize_flatten($value);
99             }
100              
101 108 100 100     391 $value = $self->normalize_json($value, $param) if (($key_rules->{expand}||'') eq 'JSON');
102 108 100 100     376 $value = $self->normalize_boolean($value) if ($key_rules->{boolean}||'');
103              
104 108         506 return $value;
105             }
106              
107             sub normalize_always_array {
108 2     2 0 7 my ($self, $value) = @_;
109 2 50 50     17 $value = [$value] unless (ref($value)||'') eq 'ARRAY';
110 2         9 return $value;
111             }
112              
113             sub normalize_flatten{
114 77     77 0 157 my ($self, $value) = @_;
115 77 100 100     294 $value = $value->[-1] if (ref($value)||'') eq 'ARRAY';
116 77         188 return $value;
117             }
118              
119             sub normalize_boolean {
120 2     2 0 8 my ($self, $value) = @_;
121 2 100       46 return $value ? 1:0
122             }
123              
124             sub normalize_nested_model_name {
125 16     16 0 35 my ($self, $nested_model) = @_;
126 16 100       59 if($nested_model =~ /^::/) {
127 7         44 my $model_class_base = ref($self->{request_model});
128 7         33 my $prefix = Catalyst::Utils::class2classprefix($model_class_base);
129 7         153 $model_class_base =~s/^${prefix}\:\://;
130 7         45 $nested_model = "${model_class_base}${nested_model}";
131             }
132              
133 16         74 return $nested_model;
134             }
135              
136             my $_JSON_PARSER;
137             sub get_json_parser {
138 2     2 0 5 my $self = shift;
139 2   66     20 return $_JSON_PARSER ||= Module::Runtime::use_module('JSON::MaybeXS')->new(utf8 => 1);
140             }
141              
142             sub normalize_json {
143 2     2 0 7 my ($self, $value, $param) = @_;
144              
145             eval {
146 2         8 $value = $self->get_json_parser->decode($value);
147 2 50       5 } || do {
148 0         0 CatalystX::RequestModel::Utils::InvalidJSONForValue->throw(param=>$param, parsing_error=>$@);
149             };
150              
151 2         152 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