File Coverage

blib/lib/Mojo/UserAgent/Mockable/Serializer.pm
Criterion Covered Total %
statement 118 144 81.9
branch 21 40 52.5
condition 12 15 80.0
subroutine 23 26 88.4
pod 4 4 100.0
total 178 229 77.7


line stmt bran cond sub pod time code
1 24     24   1553597 use 5.014;
  24         124  
2              
3             $Mojo::UserAgent::Mockable::Serializer::VERSION = '1.59';
4             use warnings::register;
5 24     24   132  
  24         62  
  24         2530  
6             use Carp;
7 24     24   138 use Class::Load ':all';
  24         42  
  24         1237  
8 24     24   8247 use English qw/-no_match_vars/;
  24         299115  
  24         3291  
9 24     24   9395 use Path::Tiny;
  24         37104  
  24         123  
10 24     24   22415 use JSON::MaybeXS qw/decode_json/;
  24         239032  
  24         1265  
11 24     24   2543 use Mojo::Base 'Mojo::EventEmitter';
  24         13776  
  24         1275  
12 24     24   1009 use Safe::Isa (qw/$_isa/);
  24         303646  
  24         154  
13 24     24   19429 use Try::Tiny;
  24         10849  
  24         2165  
14 24     24   160  
  24         54  
  24         43030  
15             # ABSTRACT: A class that serializes Mojo transactions created by Mojo::UserAgent::Mockable.
16              
17             # VERSION
18              
19              
20             my ( $self, @transactions ) = @_;
21              
22 88     88 1 402743 my @serialized = map { $self->_serialize_tx($_) } @transactions;
23             for (0 .. $#serialized) {
24 88         317 $serialized[$_]->{txn_num} = $_;
  442         1107  
25 88         375 }
26 442         811 my $JSON = JSON::MaybeXS->new(pretty => 1, canonical => 1, utf8 => 1);
27             return $JSON->encode( \@serialized );
28 88         957 }
29 88         11170  
30             my ( $self, $transaction ) = @_;
31              
32             if ( !$transaction->$_isa('Mojo::Transaction') ) {
33 442     442   1020 croak q{Only instances of Mojo::Transaction may be serialized using this class};
34             }
35 442 50       1186  
36 0         0 $transaction->emit('pre_freeze');
37             my $slush = {
38             request => $self->_serialize_message( $transaction->req ),
39 442         7037 response => $self->_serialize_message( $transaction->res ),
40 442         4210 class => ref $transaction,
41             };
42             for my $event ( keys %{ $transaction->{'events'} } ) {
43             next if $event eq 'pre_freeze' or $event eq 'post_freeze' or $event eq 'resume'
44             or $event eq 'finish'; # 'finish' comes from Mojo::IOLoop; we probably don't need to serialize it
45 442         823 carp(qq{Subscriber for event "$event" not serialized}) if warnings::enabled;
  442         1201  
46 443 50 100     2609 push @{ $slush->{'events'} }, $event;
      100        
      66        
47             }
48 0 0       0  
49 0         0 $transaction->emit( 'post_freeze', $slush );
  0         0  
50              
51             return $slush;
52 442         1217 }
53              
54 442         4698 my ( $self, $message ) = @_;
55              
56             $message->emit('pre_freeze');
57             my $slush = {
58 884     884   4064 class => ref $message,
59             body => $message->to_string,
60 884         2234 };
61 884         8140 if ( $message->can('url') ) {
62             $slush->{url} = _freeze_url( $message->url );
63             }
64             for my $event ( keys %{ $message->{'events'} } ) {
65 884 100       168849 next if $event eq 'pre_freeze' or $event eq 'post_freeze';
66 442         1063 carp(qq{Subscriber for event "$event" not serialized}) if warnings::enabled;
67             push @{ $slush->{'events'} }, $event;
68 884         1364 }
  884         2212  
69 4 50 66     25  
70 0 0       0 $message->emit( 'post_freeze', $slush );
71 0         0 return $slush;
  0         0  
72             }
73              
74 884         2595 my $url = shift;
75 884         8584 if ( !$url->$_isa('Mojo::URL') ) {
76             $url = Mojo::URL->new($url);
77             }
78             my $slush;
79 442     442   1885 for my $attr (qw/scheme userinfo host port path query fragment/) {
80 442 50       954 $slush->{$attr} = sprintf '%s', $url->$attr if defined $url->$attr;
81 0         0 }
82             if ( %{ $url->base } ) {
83 442         5218 $slush->{base} = _freeze_url( $url->base );
84 442         869 }
85 3094 100       212279 return $slush;
86             }
87 442 50       2025  
  442         1057  
88 0         0 my ( $self, $frozen ) = @_;
89              
90 442         3927 my $slush = decode_json($frozen);
91              
92             if ( ref $slush ne 'ARRAY' ) {
93             croak q{Invalid serialized data: not stored as array.};
94 34     34 1 16793 }
95             $self->emit( 'pre_thaw', $slush );
96 34         3334  
97             my @transactions;
98 34 50       398587 for my $tx_num ( 0 .. $#{$slush} ) {
99 0         0 my $tx;
100             try {
101 34         354 $tx = $self->_deserialize_tx( $slush->[$tx_num] );
102             }
103 34         1283 catch {
104 34         79 my $tx_num = ( $tx_num + 1 );
  34         169  
105 217         382 croak qq{Error deserializing transaction $tx_num: $_};
106             };
107 217     217   9199  
108             push @transactions, $tx;
109             }
110 0     0   0  
111 0         0 $self->emit( 'post_thaw', \@transactions, $slush );
112 217         1418 return @transactions;
113             }
114 217         2769  
115             my ( $self, $slush ) = @_;
116              
117 34         269 for my $key (qw/class request response/) {
118 34         2356 if ( !defined $slush->{$key} ) {
119             croak qq{Invalid serialized data: Missing required key '$key'};
120             }
121             }
122 217     217   478  
123             load_class( $slush->{'class'} );
124 217         498 my $obj = $slush->{'class'}->new();
125 651 50       1535  
126 0         0 if ( !$obj->$_isa('Mojo::Transaction') ) {
127             croak q{Only instances of Mojo::Transaction may be deserialized using this class};
128             }
129              
130 217         861 my $response;
131 217         6830 try {
132             $response = $self->_deserialize_message( $slush->{response} );
133 217 50       1480 }
134 0         0 catch {
135             die qq{Response deserialization failed: $_\n};
136             };
137 217         3165 $obj->res($response);
138              
139 217     217   7092 my $request;
140             try {
141             $request = $self->_deserialize_message( $slush->{request} );
142 0     0   0 }
143 217         1101 catch {
144 217         3549 die qq{Request deserialization failed: $_\n};
145             };
146 217         1177 $obj->req($request);
147              
148 217     217   7765 if ( $slush->{'events'} ) {
149             for my $event ( @{ $slush->{'events'} } ) {
150             $obj->emit($event);
151 0     0   0 }
152 217         1227 }
153 217         3325 return $obj;
154             }
155 217 50       1523  
156 0         0 my ( $self, $slush ) = @_;
  0         0  
157 0         0 for my $key (qw/body class/) {
158             if ( !$slush->{$key} ) {
159             croak qq{Invalid serialized data: missing required key "$key"};
160 217         519 }
161             }
162              
163             load_class( $slush->{'class'} );
164 434     434   804 my $obj = $slush->{'class'}->new;
165 434         761 if ( $slush->{'url'} && $obj->can('url') ) {
166 868 50       2025 $obj->url( _thaw_url( $slush->{url} ) );
167 0         0 }
168             if ( !$obj->can('parse') ) {
169             die qq{Message class "$slush->{class}" must define the 'parse' method\n};
170             }
171 434         1263 $obj->parse( $slush->{'body'} );
172 434         11842  
173 434 100 66     3355 if ( !$obj->can('emit') ) {
174 217         582 die qq{Message class "$slush->{class}" must define the 'emit' method\n};
175             }
176 434 50       2322 if ( $slush->{'events'} ) {
177 0         0 for my $event ( @{ $slush->{'events'} } ) {
178             $obj->emit($event);
179 434         1524 }
180             }
181 434 50       286862  
182 0         0 return $obj;
183             }
184 434 50       1111  
185 0         0 my $slush = shift;
  0         0  
186 0         0 # FIXME: Temporary workaround
187             return Mojo::URL->new($slush) unless ref $slush;
188              
189             my $url = Mojo::URL->new;
190 434         1156  
191             for my $attr ( keys %{$slush} ) {
192             $url->$attr( $slush->{$attr} );
193             }
194 217     217   378 if ( $slush->{base} ) {
195             $url->base( _thaw_url( $slush->{base} ) );
196 217 50       534 }
197             return $url;
198 217         862 }
199              
200 217         1520 my ( $self, $file, @transactions ) = @_;
  217         899  
201 944         12176  
202             my $serialized = $self->serialize(@transactions);
203 217 50       3769 path($file)->spew_utf8($serialized);
204 0         0 }
205              
206 217         605 my ( $self, $file ) = @_;
207              
208             my $contents = path($file)->slurp_utf8;
209             return $self->deserialize($contents);
210 84     84 1 993412 }
211             1;
212 84         439  
213 84         35593  
214             =pod
215              
216             =encoding UTF-8
217 30     30 1 40067  
218             =head1 NAME
219 30         193  
220 30         9293 Mojo::UserAgent::Mockable::Serializer - A class that serializes Mojo transactions created by Mojo::UserAgent::Mockable.
221              
222             =head1 VERSION
223              
224             version 1.59
225              
226             =head1 SYNOPSIS
227              
228             # This module is not intended to be used directly. Synopsis here is given to show how
229             # Mojo::UserAgent::Mockable uses the module to record transactions.
230            
231             use Mojo::UserAgent::Mockable::Serializer;
232             use Mojo::UserAgent;
233             use File::Slurper qw(read_text write_text);
234              
235             my $ua = Mojo::UserAgent->new;
236             my $serializer = Mojo::UserAgent::Mockable::Serializer->new;
237            
238             my @transactions;
239             push @transactions, $ua->get('http://example.com');
240             push @transactions, $ua->get('http://example.com/object/123');
241             push @transactions, $ua->get('http://example.com/subobject/456');
242              
243             my $json = $serializer->serialize(@transactions);
244             write_text('/path/to/file/json', $json);
245              
246             # OR
247              
248             $serializer->store('/path/to/file.json', @transactions);
249              
250             # Later...
251              
252             my $json = read_text('/path/to/file.json');
253             my @reconstituted_transactions = $serializer->deserialize($json);
254              
255             # OR
256             #
257             my @reconstituted_transactions = Mojo::UserAgent::Mockable::Serializer->retrieve('/path/to/file.json');
258              
259             =head1 METHODS
260              
261             =head2 serialize
262              
263             Serialize or freeze one or more instances of L<Mojo::Transaction>. Takes an array of transactions
264             to be serialized as the single argument. This method will generate a warning if the instance has
265             any subscribers (see L<Mojo::EventEmitter/on>). Suppress this warning with (e.g.):
266              
267             no warnings 'Mojo::UserAgent::Mock::Serializer';
268             $serializer->serialize(@transactions);
269             use warnings 'Mojo::UserAgent::Mock::Serializer';
270              
271             =head2 deserialize
272              
273             Deserialize or thaw a previously serialized array of L<Mojo:Transaction>. Arguments:
274              
275             =over 4
276              
277             =item $data
278              
279             JSON containing the serialized objects.
280              
281             =back
282              
283             =head2 store
284              
285             Serialize an instance of L<Mojo::Transaction> and write it to the given file or file handle. Takes two
286             arguments:
287              
288             =over 4
289              
290             =item $file
291              
292             File or handle to write serialized object to.
293              
294             =item @transactions
295              
296             Array of L<Mojo::Transaction> to serialize
297              
298             =back
299              
300             =head2 retrieve
301              
302             Read from the specified file or file handle and deserialize one or more instances of
303             L<Mojo::Transaction> from the data read. If a file handle is passed, data will be
304             read until an EOF is received. Arguments:
305              
306             =over 4
307              
308             =item $file
309              
310             File containing serialized object
311              
312             =back
313              
314             =head1 EVENTS
315              
316             This module emits the following events:
317              
318             =head2 pre_thaw
319              
320             $serializer->on( pre_thaw => sub {
321             my ($serializer, $slush) = @_;
322             ...
323             });
324              
325             Emitted immediately before transactions are deserialized. See L</DATA STRUCTURE> below for details
326             of the format of $slush.
327              
328             =head2 post_thaw
329              
330             # Note that $transactions is an arrayref here.
331             $serializer->on( post_thaw => sub {
332             my ($serializer, $transactions, $slush) = @_;
333             ...
334             }
335              
336             Emitted immediately after transactions are deserialized. See L</DATA STRUCTURE> below for details
337             of the format of $slush.
338              
339             In addition, each transaction, as well as each message therein, serialized using this module will
340             emit the following events:
341              
342             =head2 pre_freeze
343              
344             $transaction->on(freeze => sub {
345             my $tx = shift;
346             ...
347             });
348              
349             Emitted immediately before the transaction is serialized.
350              
351             =head2 post_freeze
352              
353             Emitted immediately after the transaction is serialized. See L</Messages> for details of the
354             frozen format.
355              
356             $transaction->on(post_freeze => sub {
357             my $tx = shift;
358             my $frozen = shift;
359             ...
360             });
361              
362             =head1 DATA STRUCTURE
363              
364             L<serialize> produces, and L<deserialize> expects, JSON data. Transactions are stored as an array
365             of JSON objects (i.e. hashes). Each transaction object has the keys:
366              
367             =over 4
368              
369             =item 'class'
370              
371             The original class of the transaction.
372              
373             =item 'request'
374              
375             The request portion of the transaction (e.g. "GET /foo/bar ..."). See L</Messages> below for
376             encoding details.
377              
378             =item 'response'
379              
380             The response portion of the transaction (e.g. "200 OK ..."). See L</Messages> below for encoding
381             details.
382              
383             =back
384              
385             =head2 Messages
386              
387             Individual messages are stored as JSON objects (i.e. hashes) with the keys:
388              
389             =over 4
390              
391             =item 'class'
392              
393             The class name of the serialized object. This should be a subclass of L<Mojo::Message>
394              
395             =item 'events'
396              
397             Array of events with subscribers in the serialized object. These events will be re-emitted after
398             the L</thaw> event is emitted, but any subscribers present in the original object will be lost.
399              
400             =item 'body'
401              
402             The raw HTTP message body.
403              
404             =back
405              
406             =head1 CAVEATS
407              
408             This module does not serialize any event listeners. This is unlikely to change in future releases.
409              
410             =head1 AUTHOR
411              
412             Kit Peters <popefelix@cpan.org>
413              
414             =head1 COPYRIGHT AND LICENSE
415              
416             This software is copyright (c) 2022 by Kit Peters.
417              
418             This is free software; you can redistribute it and/or modify it under
419             the same terms as the Perl 5 programming language system itself.
420              
421             =cut