File Coverage

blib/lib/CatalystX/RequestModel/ContentBodyParser.pm
Criterion Covered Total %
statement 91 96 94.7
branch 35 44 79.5
condition 17 24 70.8
subroutine 17 19 89.4
pod 0 11 0.0
total 160 194 82.4


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