File Coverage

blib/lib/Raisin/Middleware/Formatter.pm
Criterion Covered Total %
statement 103 103 100.0
branch 30 32 93.7
condition 4 4 100.0
subroutine 17 17 100.0
pod 4 5 80.0
total 158 161 98.1


line stmt bran cond sub pod time code
1             #!perl
2             #PODNAME: Raisin::Middleware::Formatter
3             #ABSTRACT: A parser/formatter middleware for L.
4              
5 13     13   602 use strict;
  13         31  
  13         410  
6 13     13   74 use warnings;
  13         28  
  13         687  
7              
8             package Raisin::Middleware::Formatter;
9             $Raisin::Middleware::Formatter::VERSION = '0.93';
10 13     13   85 use parent 'Plack::Middleware';
  13         31  
  13         85  
11              
12 13     13   22305 use File::Basename qw(fileparse);
  13         68  
  13         1413  
13 13     13   103 use HTTP::Status qw(:constants);
  13         29  
  13         5448  
14 13     13   103 use Plack::Request;
  13         30  
  13         362  
15 13     13   611 use Plack::Response;
  13         1290  
  13         327  
16 13     13   82 use Plack::Util;
  13         27  
  13         536  
17 13         66 use Plack::Util::Accessor qw(
18             default_format
19             format
20             encoder
21             decoder
22             raisin
23 13     13   86 );
  13         27  
24              
25             sub call {
26 45     45 1 377838 my ($self, $env) = @_;
27              
28             # Pre-process
29 45         323 my $req = Plack::Request->new($env);
30              
31 45 100       526 if ($req->content) {
32 15         4904 my %media_types_map_flat_hash = $self->decoder->media_types_map_flat_hash;
33              
34 15         71 my ($ctype) = split /;/, $req->content_type, 2;
35 15         134 my $format = $media_types_map_flat_hash{ $ctype};
36 15 100       48 unless ($format) {
37 4         13 Raisin::log(info => "unsupported media type: ${ \$req->content_type }");
  4         15  
38 4         2002 return Plack::Response->new(HTTP_UNSUPPORTED_MEDIA_TYPE)->finalize;
39             }
40 11         28 $env->{'raisinx.decoder'} = $format;
41              
42 11         35 my $d = Plack::Util::load_class($self->decoder->for($format));
43 11         160 $env->{'raisinx.body_params'} = $d->deserialize($req->content);
44             }
45              
46 41         41935 my $format = $self->negotiate_format($req);
47 41 100       119 unless ($format) {
48 3         26 return Plack::Response->new(HTTP_NOT_ACCEPTABLE)->finalize;
49             }
50 38         102 $env->{'raisinx.encoder'} = $format;
51              
52 38         180 my $res = $self->app->($env);
53             # Post-process
54             Plack::Util::response_cb($res, sub {
55             # TODO: delayed responses
56              
57 38     38   533 my $res = shift;
58 38         152 my $r = Plack::Response->new(@$res);
59              
60 38 50       3315 if (ref $r->body) {
61 38         250 my $s = Plack::Util::load_class($self->encoder->for($format));
62              
63 38 50       632 $r->content_type($s->content_type) unless $r->content_type;
64 38         654 $r->body($s->serialize($r->body));
65             }
66              
67 38         89733 @$res = @{ $r->finalize };
  38         118  
68 38         4238 return;
69 38         4247 });
70             }
71              
72 41   100 41   8105 sub _accept_header_set { length(shift || '') }
73             sub _path_has_extension {
74 58     58   5354 my $path = shift;
75 58         1867 my (undef, undef, $suffix) = fileparse($path, qr/\..[^.]*$/);
76 58         247 $suffix;
77             }
78              
79             sub negotiate_format {
80 54     54 1 424 my ($self, $req) = @_;
81              
82 54         161 my @allowed_formats = $self->allowed_formats_for_requested_route($req);
83              
84             # PRECEDENCE:
85             # - extension
86             # - headers
87             # - default
88              
89 54         151 my @wanted_formats = do {
90 54         181 my $ext = _path_has_extension($req->path);
91 54 100       236 if ($ext) {
    100          
92 13         51 $self->format_from_extension($ext);
93             }
94             elsif (_accept_header_set($req->header('Accept'))) {
95             # In case of wildcard matches, we default to first allowed format
96 18         55 $self->format_from_header($req->header('Accept'), $allowed_formats[0]);
97             }
98             else {
99 23         66 $self->default_format;
100             }
101             };
102              
103             my @matching_formats = grep {
104 54         225 my $format = $_;
  52         108  
105 52         88 grep { $format eq $_ } @allowed_formats
  169         373  
106             } @wanted_formats;
107              
108 54         189 shift @matching_formats;
109             }
110              
111             sub format_from_extension {
112 19     19 1 129 my ($self, $ext) = @_;
113 19 100       58 return unless $ext;
114              
115             # Trim leading dot in the extension.
116 18         41 $ext = substr($ext, 1);
117              
118 18         54 my %media_types_map_flat_hash = $self->encoder->media_types_map_flat_hash;
119 18         49 my $format = $media_types_map_flat_hash{ $ext };
120 18 100       65 return unless $format;
121              
122 15         64 $format;
123             }
124              
125             sub format_from_header {
126 28     28 1 684 my ($self, $accept, $assumed_wildcard_format) = @_;
127 28 100       91 return unless $accept;
128              
129 27         81 my %media_types_map_flat_hash = $self->encoder->media_types_map_flat_hash;
130             # Add a default format as a `*/*`
131 27         85 $media_types_map_flat_hash{'*/*'} = $assumed_wildcard_format;
132              
133 27         48 my @media_types;
134 27         142 for my $type (split /\s*,\s*/, $accept) {
135 43         128 my ($media, $params) = split /;/, $type, 2;
136             # Cleaning up media type by deleting a Vendor tree
137 43         112 $media =~ s/vnd\.[^+]+\+//g;
138              
139 43 100       126 next unless my $format = $media_types_map_flat_hash{$media};
140              
141 32 100 100     186 my $q = ($params // '') =~ /q=([\d\.]+)/ ? $1 : 1;
142              
143 32         129 push @media_types, { format => $format, q => $q };
144             }
145              
146 27         99 map { $_->{format} } sort { $b->{q} <=> $a->{q} } @media_types;
  32         163  
  11         35  
147             }
148              
149             sub allowed_formats_for_requested_route {
150 54     54 0 124 my ($self, $req) = @_;
151             # Global format has been forced upon entire app
152 54 100       173 return $self->format if $self->format;
153              
154             # Route specific `produces` restrictions
155 48 100       551 if ( $self->raisin ) {
156 31         206 my $route = $self->raisin->routes->find($req->method, $req->path);
157 31 100       112 return @{$route->{produces}} if $route->{produces};
  4         20  
158             }
159              
160             # Prefer Default, allow all others
161 44         122 my @allowed = keys %{ $self->encoder->all };
  44         114  
162 44 100       153 unshift @allowed, $self->default_format if $self->default_format;
163 44         462 return @allowed;
164             }
165              
166             1;
167              
168             __END__