v1.21
[catagits/Catalyst-Action-REST.git] / lib / Catalyst / Action / Deserialize.pm
1 package Catalyst::Action::Deserialize;
2
3 use Moose;
4 use namespace::autoclean;
5
6 extends 'Catalyst::Action::SerializeBase';
7 use Module::Pluggable::Object;
8 use MRO::Compat;
9 use Moose::Util::TypeConstraints;
10
11 has plugins => ( is => 'rw' );
12
13 has deserialize_http_methods => (
14     traits  => ['Hash'],
15     isa     => do {
16         my $tc = subtype as 'HashRef[Str]';
17         coerce $tc, from 'ArrayRef[Str]',
18             via { +{ map { ($_ => 1) } @$_ } };
19         $tc;
20     },
21     coerce  => 1,
22     builder => '_build_deserialize_http_methods',
23     handles => {
24         deserialize_http_methods         => 'keys',
25         _deserialize_handles_http_method => 'exists',
26     },
27 );
28
29 sub _build_deserialize_http_methods { [qw(POST PUT OPTIONS DELETE)] }
30
31 sub execute {
32     my $self = shift;
33     my ( $controller, $c ) = @_;
34
35     if ( !defined($c->req->data) && $self->_deserialize_handles_http_method($c->request->method) ) {
36         my ( $sclass, $sarg, $content_type ) =
37           $self->_load_content_plugins( 'Catalyst::Action::Deserialize',
38             $controller, $c );
39         return 1 unless defined($sclass);
40         my $rc;
41         if ( defined($sarg) ) {
42             $rc = $sclass->execute( $controller, $c, $sarg );
43         } else {
44             $rc = $sclass->execute( $controller, $c );
45         }
46         if ( $rc eq "0" ) {
47             return $self->unsupported_media_type( $c, $content_type );
48         } elsif ( $rc ne "1" ) {
49             return $self->serialize_bad_request( $c, $content_type, $rc );
50         }
51     }
52
53     $self->maybe::next::method(@_);
54
55     return 1;
56 }
57
58 __PACKAGE__->meta->make_immutable;
59
60 =head1 NAME
61
62 Catalyst::Action::Deserialize - Deserialize Data in a Request
63
64 =head1 SYNOPSIS
65
66     package Foo::Controller::Bar;
67
68     __PACKAGE__->config(
69         'default'   => 'text/x-yaml',
70         'stash_key' => 'rest',
71         'map'       => {
72             'text/x-yaml'        => 'YAML',
73             'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
74         },
75     );
76
77     sub begin :ActionClass('Deserialize') {}
78
79 =head1 DESCRIPTION
80
81 This action will deserialize HTTP POST, PUT, OPTIONS and DELETE requests.
82 It assumes that the body of the HTTP Request is a serialized object.
83 The serializer is selected by introspecting the requests content-type
84 header.
85
86 If you want deserialize any other HTTP method besides POST, PUT,
87 OPTIONS and DELETE you can do this by setting the
88 C<< deserialize_http_methods >> list via C<< action_args >>.
89 Just modify the config in your controller and define a list of HTTP
90 methods the deserialization should happen for:
91
92     __PACKAGE__->config(
93         action_args => {
94             '*' => {
95                 deserialize_http_methods => [qw(POST PUT OPTIONS DELETE GET)]
96             }
97         }
98     );
99
100 See also L<Catalyst::Controller/action_args>.
101
102 The specifics of deserializing each content-type is implemented as
103 a plugin to L<Catalyst::Action::Deserialize>.  You can see a list
104 of currently implemented plugins in L<Catalyst::Controller::REST>.
105
106 The results of your Deserializing will wind up in $c->req->data.
107 This is done through the magic of L<Catalyst::Request::REST>.
108
109 While it is common for this Action to be called globally as a
110 C<begin> method, there is nothing stopping you from using it on a
111 single routine:
112
113    sub foo :Local :Action('Deserialize') {}
114
115 Will work just fine.
116
117 When you use this module, the request class will be changed to
118 L<Catalyst::Request::REST>.
119
120 =head1 RFC 7231 Compliance Mode
121
122 To maintain backwards compatibility with the module's original functionality,
123 where it was assumed the deserialize and serialize content types are the same,
124 an optional compliance mode can be enabled to break this assumption.
125
126     __PACKAGE__->config(
127         'compliance_mode'    => 1,
128         'default'            => 'text/x-yaml',
129         'stash_key'          => 'rest',
130         'map'                => {
131             'text/x-yaml'        => 'YAML',
132             'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
133         },
134         'deserialize_default => 'application/json',
135         'deserialize_map'    => {
136             'application/json'   => 'JSON',
137         },
138     );
139
140 Three extra keys are added to the controller configuration. compliance_mode, a
141 boolean to enable the mode. And a parallel set of content type mappings
142 'deserialize_default' and 'deserialize_map' to mirror the default/map
143 configuration keys.
144
145 The module will use the default/map keys when negotiating the serializing
146 content type specified by the client in the Accept header. And will use the
147 deserialize_default/deserialize_map in conjunction with the Content-Type
148 header where the client is giving the content type being sent in the request.
149
150 =head1 CUSTOM ERRORS
151
152 For building custom error responses when de-serialization fails, you can create
153 an ActionRole (and use L<Catalyst::Controller::ActionRole> to apply it to the
154 C<begin> action) which overrides C<unsupported_media_type> and/or C<serialize_bad_request>
155 methods.
156
157 =head1 SEE ALSO
158
159 You likely want to look at L<Catalyst::Controller::REST>, which implements
160 a sensible set of defaults for a controller doing REST.
161
162 L<Catalyst::Action::Serialize>, L<Catalyst::Action::REST>
163
164 =head1 AUTHORS
165
166 See L<Catalyst::Action::REST> for authors.
167
168 =head1 LICENSE
169
170 You may distribute this code under the same terms as Perl itself.
171
172 =cut