File Coverage

blib/lib/Dancer2/Serializer/Mutable.pm
Criterion Covered Total %
statement 24 24 100.0
branch 5 6 83.3
condition n/a
subroutine 6 6 100.0
pod n/a
total 35 36 97.2


line stmt bran cond sub pod time code
1             package Dancer2::Serializer::Mutable;
2             # ABSTRACT: Serialize and deserialize content based on HTTP header
3             $Dancer2::Serializer::Mutable::VERSION = '1.0.0';
4 2     2   132691 use Moo;
  2         14201  
  2         10  
5 2     2   2951 use Carp 'croak';
  2         6  
  2         84  
6 2     2   983 use Encode;
  2         29775  
  2         160  
7 2     2   979 use Module::Runtime 'require_module';
  2         3466  
  2         17  
8             with 'Dancer2::Core::Role::Serializer';
9              
10 2     2   124 use constant DEFAULT_CONTENT_TYPE => 'application/json';
  2         4  
  2         1262  
11              
12             has '+content_type' => ( default => DEFAULT_CONTENT_TYPE() );
13              
14             my $serializer = {
15             'YAML' => {
16             to => sub { Dancer2::Core::DSL::to_yaml(@_) },
17             from => sub { Dancer2::Core::DSL::from_yaml(@_) },
18             },
19             'Dumper' => {
20             to => sub { Dancer2::Core::DSL::to_dumper(@_) },
21             from => sub { Dancer2::Core::DSL::from_dumper(@_) },
22             },
23             'JSON' => {
24             to => sub { Dancer2::Core::DSL::to_json(@_) },
25             from => sub { Dancer2::Core::DSL::from_json(@_) },
26             },
27             };
28              
29             has mapping => (
30             is => 'ro',
31             lazy => 1,
32             default => sub {
33             my $self = shift;
34              
35             if ( my $mapping = $self->config->{mapping} ) {
36              
37             # initialize non-default serializers
38             for my $s ( values %$mapping ) {
39             # TODO allow for arguments via the config
40             next if $serializer->{$s};
41             my $serializer_class = "Dancer2::Serializer::$s";
42             require_module($serializer_class);
43             my $serializer_object = $serializer_class->new;
44             $serializer->{$s} = {
45             from => sub { shift; $serializer_object->deserialize(@_) },
46             to => sub { shift; $serializer_object->serialize(@_) },
47             };
48             }
49              
50             return $mapping;
51             }
52              
53              
54             return {
55             'text/x-yaml' => 'YAML',
56             'text/html' => 'YAML',
57             'text/x-data-dumper' => 'Dumper',
58             'text/x-json' => 'JSON',
59             'application/json' => 'JSON',
60             }
61             },
62             );
63              
64             sub serialize {
65             my ( $self, $entity ) = @_;
66              
67             # Look for valid format in the headers
68             my $format = $self->_get_content_type('accept');
69              
70             # Match format with a serializer and return
71             $format and return $serializer->{$format}{'to'}->(
72             $self, $entity
73             );
74              
75             # If none is found then just return the entity without change
76             return $entity;
77             }
78              
79             sub deserialize {
80             my ( $self, $content ) = @_;
81              
82             my $format = $self->_get_content_type('content_type');
83             $format and return $serializer->{$format}{'from'}->($self, $content);
84              
85             return $content;
86             }
87              
88             sub _get_content_type {
89 51     51   111 my ($self, $header) = @_;
90              
91 51 50       147 if ( $self->has_request ) {
92             # Search for the first HTTP header variable which specifies
93             # supported content. Both content_type and accept are checked
94             # for backwards compatibility.
95 51         111 foreach my $method ( $header, qw<content_type accept> ) {
96 91 100       5655 if ( my $value = $self->request->header($method) ) {
97 50 100       6679 if ( my $serializer = $self->mapping->{$value} ) {
98 41         1029 $self->set_content_type($value);
99 41         1124 return $serializer;
100             }
101             }
102             }
103             }
104              
105             # If none if found, return the default, 'JSON'.
106 10         451 $self->set_content_type( DEFAULT_CONTENT_TYPE() );
107 10         267 return 'JSON';
108             }
109              
110             1;
111              
112             __END__
113              
114             =pod
115              
116             =encoding UTF-8
117              
118             =head1 NAME
119              
120             Dancer2::Serializer::Mutable - Serialize and deserialize content based on HTTP header
121              
122             =head1 VERSION
123              
124             version 1.0.0
125              
126             =head1 SYNOPSIS
127              
128             # in config.yml
129             serializer: Mutable
130              
131             engines:
132             serializer:
133             Mutable:
134             mapping:
135             'text/x-yaml' : YAML
136             'text/html' : YAML
137             'text/x-data-dumper' : Dumper
138             'text/x-json' : JSON
139             'application/json' : JSON
140              
141             # in the app
142             put '/something' => sub {
143             # deserialized from request
144             my $name = param( 'name' );
145              
146             ...
147              
148             # will be serialized to the most
149             # fitting format
150             return { message => "user $name added" };
151             };
152              
153             =head1 DESCRIPTION
154              
155             This serializer will try find the best (de)serializer for a given request.
156             For this, it will pick the first valid content type found from the following list
157             and use its related serializer.
158              
159             =over
160              
161             =item
162              
163             The B<content_type> from the request headers
164              
165             =item
166              
167             the B<accept> from the request headers
168              
169             =item
170              
171             The default is B<application/json>
172              
173             =back
174              
175             The content-type/serializer mapping that C<Dancer2::Serializer::Mutable>
176             uses is
177              
178             serializer | content types
179             ----------------------------------------------------------
180             Dancer2::Serializer::YAML | text/x-yaml, text/html
181             Dancer2::Serializer::Dumper | text/x-data-dumper
182             Dancer2::Serializer::JSON | text/x-json, application/json
183              
184             A different mapping can be provided via the config file. For example,
185             the default mapping would be configured as
186              
187             engines:
188             serializer:
189             Mutable:
190             mapping:
191             'text/x-yaml' : YAML
192             'text/html' : YAML
193             'text/x-data-dumper' : Dumper
194             'text/x-json' : JSON
195             'application/json' : JSON
196              
197             The keys of the mapping are the content-types to serialize,
198             and the values the serializers to use. Serialization for C<YAML>, C<Dumper>
199             and C<JSON> are done using internal Dancer mechanisms. Any other serializer will
200             be taken to be as Dancer2 serialization class (minus the C<Dancer2::Serializer::> prefix)
201             and an instance of it will be used
202             to serialize/deserialize data. For example, adding L<Dancer2::Serializer::XML>
203             to the mapping would be:
204              
205             engines:
206             serializer:
207             Mutable:
208             mapping:
209             'text/x-yaml' : YAML
210             'text/html' : YAML
211             'text/x-data-dumper' : Dumper
212             'text/x-json' : JSON
213             'text/xml' : XML
214              
215             =head2 INTERNAL METHODS
216              
217             The following methods are used internally by C<Dancer2> and are not made
218             accessible via the DSL.
219              
220             =head2 serialize
221              
222             Serialize a data structure. The format it is serialized to is determined
223             automatically as described above. It can be one of YAML, Dumper, JSON, defaulting
224             to JSON if there's no clear preference from the request.
225              
226             =head2 deserialize
227              
228             Deserialize the provided serialized data to a data structure. The type of
229             serialization format depends on the request's content-type. For now, it can
230             be one of YAML, Dumper, JSON.
231              
232             =head2 content_type
233              
234             Returns the content-type that was used during the last C<serialize> /
235             C<deserialize> call. B<WARNING> : you must call C<serialize> / C<deserialize>
236             before calling C<content_type>. Otherwise the return value will be C<undef>.
237              
238             =head1 AUTHOR
239              
240             Dancer Core Developers
241              
242             =head1 COPYRIGHT AND LICENSE
243              
244             This software is copyright (c) 2023 by Alexis Sukrieh.
245              
246             This is free software; you can redistribute it and/or modify it under
247             the same terms as the Perl 5 programming language system itself.
248              
249             =cut