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 22     22   1945249 use 5.014;
  22         129  
2              
3             package Mojo::UserAgent::Mockable::Serializer;
4             $Mojo::UserAgent::Mockable::Serializer::VERSION = '1.58';
5 22     22   143 use warnings::register;
  22         43  
  22         2565  
6              
7 22     22   149 use Carp;
  22         64  
  22         1348  
8 22     22   9715 use Class::Load ':all';
  22         312490  
  22         3607  
9 22     22   11069 use English qw/-no_match_vars/;
  22         39864  
  22         146  
10 22     22   29334 use Path::Tiny;
  22         238678  
  22         1488  
11 22     22   3650 use JSON::MaybeXS qw/decode_json/;
  22         17192  
  22         1449  
12 22     22   1129 use Mojo::Base 'Mojo::EventEmitter';
  22         364886  
  22         232  
13 22     22   25378 use Safe::Isa (qw/$_isa/);
  22         10190  
  22         2562  
14 22     22   176 use Try::Tiny;
  22         48  
  22         45247  
15              
16             # ABSTRACT: A class that serializes Mojo transactions created by Mojo::UserAgent::Mockable.
17              
18             # VERSION
19              
20              
21             sub serialize {
22 74     74 1 488833 my ( $self, @transactions ) = @_;
23              
24 74         247 my @serialized = map { $self->_serialize_tx($_) } @transactions;
  326         996  
25 74         381 for (0 .. $#serialized) {
26 326         679 $serialized[$_]->{txn_num} = $_;
27             }
28 74         858 my $JSON = JSON::MaybeXS->new(pretty => 1, sort_by => 1, utf8 => 1);
29 74         9735 return $JSON->encode( \@serialized );
30             }
31              
32             sub _serialize_tx {
33 326     326   1026 my ( $self, $transaction ) = @_;
34              
35 326 50       1042 if ( !$transaction->$_isa('Mojo::Transaction') ) {
36 0         0 croak q{Only instances of Mojo::Transaction may be serialized using this class};
37             }
38              
39 326         5958 $transaction->emit('pre_freeze');
40 326         3876 my $slush = {
41             request => $self->_serialize_message( $transaction->req ),
42             response => $self->_serialize_message( $transaction->res ),
43             class => ref $transaction,
44             };
45 326         661 for my $event ( keys %{ $transaction->{'events'} } ) {
  326         1011  
46 327 50 100     2343 next if $event eq 'pre_freeze' or $event eq 'post_freeze' or $event eq 'resume'
      100        
      66        
47             or $event eq 'finish'; # 'finish' comes from Mojo::IOLoop; we probably don't need to serialize it
48 0 0       0 carp(qq{Subscriber for event "$event" not serialized}) if warnings::enabled;
49 0         0 push @{ $slush->{'events'} }, $event;
  0         0  
50             }
51              
52 326         1124 $transaction->emit( 'post_freeze', $slush );
53              
54 326         4670 return $slush;
55             }
56              
57             sub _serialize_message {
58 652     652   3629 my ( $self, $message ) = @_;
59              
60 652         1930 $message->emit('pre_freeze');
61 652         7239 my $slush = {
62             class => ref $message,
63             body => $message->to_string,
64             };
65 652 100       151470 if ( $message->can('url') ) {
66 326         941 $slush->{url} = _freeze_url( $message->url );
67             }
68 652         1311 for my $event ( keys %{ $message->{'events'} } ) {
  652         1887  
69 4 50 66     34 next if $event eq 'pre_freeze' or $event eq 'post_freeze';
70 0 0       0 carp(qq{Subscriber for event "$event" not serialized}) if warnings::enabled;
71 0         0 push @{ $slush->{'events'} }, $event;
  0         0  
72             }
73              
74 652         2294 $message->emit( 'post_freeze', $slush );
75 652         7815 return $slush;
76             }
77              
78             sub _freeze_url {
79 326     326   1674 my $url = shift;
80 326 50       858 if ( !$url->$_isa('Mojo::URL') ) {
81 0         0 $url = Mojo::URL->new($url);
82             }
83 326         4722 my $slush;
84 326         799 for my $attr (qw/scheme userinfo host port path query fragment/) {
85 2282 100       166234 $slush->{$attr} = sprintf '%s', $url->$attr if defined $url->$attr;
86             }
87 326 50       1825 if ( %{ $url->base } ) {
  326         854  
88 0         0 $slush->{base} = _freeze_url( $url->base );
89             }
90 326         3557 return $slush;
91             }
92              
93             sub deserialize {
94 30     30 1 34397 my ( $self, $frozen ) = @_;
95              
96 30         3373 my $slush = decode_json($frozen);
97              
98 30 50       174 if ( ref $slush ne 'ARRAY' ) {
99 0         0 croak q{Invalid serialized data: not stored as array.};
100             }
101 30         339 $self->emit( 'pre_thaw', $slush );
102              
103 30         1509 my @transactions;
104 30         130 for my $tx_num ( 0 .. $#{$slush} ) {
  30         156  
105 145         287 my $tx;
106             try {
107 145     145   7747 $tx = $self->_deserialize_tx( $slush->[$tx_num] );
108             }
109             catch {
110 0     0   0 my $tx_num = ( $tx_num + 1 );
111 0         0 croak qq{Error deserializing transaction $tx_num: $_};
112 145         1120 };
113              
114 145         2167 push @transactions, $tx;
115             }
116              
117 30         181 $self->emit( 'post_thaw', \@transactions, $slush );
118 30         2588 return @transactions;
119             }
120              
121             sub _deserialize_tx {
122 145     145   374 my ( $self, $slush ) = @_;
123              
124 145         327 for my $key (qw/class request response/) {
125 435 50       1257 if ( !defined $slush->{$key} ) {
126 0         0 croak qq{Invalid serialized data: Missing required key '$key'};
127             }
128             }
129              
130 145         646 load_class( $slush->{'class'} );
131 145         5959 my $obj = $slush->{'class'}->new();
132              
133 145 50       1119 if ( !$obj->$_isa('Mojo::Transaction') ) {
134 0         0 croak q{Only instances of Mojo::Transaction may be deserialized using this class};
135             }
136              
137 145         2372 my $response;
138             try {
139 145     145   6355 $response = $self->_deserialize_message( $slush->{response} );
140             }
141             catch {
142 0     0   0 die qq{Response deserialization failed: $_\n};
143 145         908 };
144 145         2799 $obj->res($response);
145              
146 145         903 my $request;
147             try {
148 145     145   6673 $request = $self->_deserialize_message( $slush->{request} );
149             }
150             catch {
151 0     0   0 die qq{Request deserialization failed: $_\n};
152 145         907 };
153 145         2574 $obj->req($request);
154              
155 145 50       1123 if ( $slush->{'events'} ) {
156 0         0 for my $event ( @{ $slush->{'events'} } ) {
  0         0  
157 0         0 $obj->emit($event);
158             }
159             }
160 145         403 return $obj;
161             }
162              
163             sub _deserialize_message {
164 290     290   640 my ( $self, $slush ) = @_;
165 290         618 for my $key (qw/body class/) {
166 580 50       1563 if ( !$slush->{$key} ) {
167 0         0 croak qq{Invalid serialized data: missing required key "$key"};
168             }
169             }
170              
171 290         956 load_class( $slush->{'class'} );
172 290         9539 my $obj = $slush->{'class'}->new;
173 290 100 66     2678 if ( $slush->{'url'} && $obj->can('url') ) {
174 145         460 $obj->url( _thaw_url( $slush->{url} ) );
175             }
176 290 50       1826 if ( !$obj->can('parse') ) {
177 0         0 die qq{Message class "$slush->{class}" must define the 'parse' method\n};
178             }
179 290         1325 $obj->parse( $slush->{'body'} );
180              
181 290 50       224691 if ( !$obj->can('emit') ) {
182 0         0 die qq{Message class "$slush->{class}" must define the 'emit' method\n};
183             }
184 290 50       911 if ( $slush->{'events'} ) {
185 0         0 for my $event ( @{ $slush->{'events'} } ) {
  0         0  
186 0         0 $obj->emit($event);
187             }
188             }
189              
190 290         941 return $obj;
191             }
192              
193             sub _thaw_url {
194 145     145   280 my $slush = shift;
195             # FIXME: Temporary workaround
196 145 50       395 return Mojo::URL->new($slush) unless ref $slush;
197              
198 145         642 my $url = Mojo::URL->new;
199              
200 145         1257 for my $attr ( keys %{$slush} ) {
  145         653  
201 656         9709 $url->$attr( $slush->{$attr} );
202             }
203 145 50       2835 if ( $slush->{base} ) {
204 0         0 $url->base( _thaw_url( $slush->{base} ) );
205             }
206 145         468 return $url;
207             }
208              
209             sub store {
210 69     69 1 1216653 my ( $self, $file, @transactions ) = @_;
211              
212 69         349 my $serialized = $self->serialize(@transactions);
213 69         552 path($file)->spew_utf8($serialized);
214             }
215              
216             sub retrieve {
217 25     25 1 68464 my ( $self, $file ) = @_;
218              
219 25         147 my $contents = path($file)->slurp_utf8;
220 25         7533 return $self->deserialize($contents);
221             }
222             1;
223              
224             __END__
225              
226             =pod
227              
228             =encoding UTF-8
229              
230             =head1 NAME
231              
232             Mojo::UserAgent::Mockable::Serializer - A class that serializes Mojo transactions created by Mojo::UserAgent::Mockable.
233              
234             =head1 VERSION
235              
236             version 1.58
237              
238             =head1 SYNOPSIS
239              
240             # This module is not intended to be used directly. Synopsis here is given to show how
241             # Mojo::UserAgent::Mockable uses the module to record transactions.
242            
243             use Mojo::UserAgent::Mockable::Serializer;
244             use Mojo::UserAgent;
245             use File::Slurper qw(read_text write_text);
246              
247             my $ua = Mojo::UserAgent->new;
248             my $serializer = Mojo::UserAgent::Mockable::Serializer->new;
249            
250             my @transactions;
251             push @transactions, $ua->get('http://example.com');
252             push @transactions, $ua->get('http://example.com/object/123');
253             push @transactions, $ua->get('http://example.com/subobject/456');
254              
255             my $json = $serializer->serialize(@transactions);
256             write_text('/path/to/file/json', $json);
257              
258             # OR
259              
260             $serializer->store('/path/to/file.json', @transactions);
261              
262             # Later...
263              
264             my $json = read_text('/path/to/file.json');
265             my @reconstituted_transactions = $serializer->deserialize($json);
266              
267             # OR
268             #
269             my @reconstituted_transactions = Mojo::UserAgent::Mockable::Serializer->retrieve('/path/to/file.json');
270              
271             =head1 METHODS
272              
273             =head2 serialize
274              
275             Serialize or freeze one or more instances of L<Mojo::Transaction>. Takes an array of transactions
276             to be serialized as the single argument. This method will generate a warning if the instance has
277             any subscribers (see L<Mojo::EventEmitter/on>). Suppress this warning with (e.g.):
278              
279             no warnings 'Mojo::UserAgent::Mock::Serializer';
280             $serializer->serialize(@transactions);
281             use warnings 'Mojo::UserAgent::Mock::Serializer';
282              
283             =head2 deserialize
284              
285             Deserialize or thaw a previously serialized array of L<Mojo:Transaction>. Arguments:
286              
287             =over 4
288              
289             =item $data
290              
291             JSON containing the serialized objects.
292              
293             =back
294              
295             =head2 store
296              
297             Serialize an instance of L<Mojo::Transaction> and write it to the given file or file handle. Takes two
298             arguments:
299              
300             =over 4
301              
302             =item $file
303              
304             File or handle to write serialized object to.
305              
306             =item @transactions
307              
308             Array of L<Mojo::Transaction> to serialize
309              
310             =back
311              
312             =head2 retrieve
313              
314             Read from the specified file or file handle and deserialize one or more instances of
315             L<Mojo::Transaction> from the data read. If a file handle is passed, data will be
316             read until an EOF is received. Arguments:
317              
318             =over 4
319              
320             =item $file
321              
322             File containing serialized object
323              
324             =back
325              
326             =head1 EVENTS
327              
328             This module emits the following events:
329              
330             =head2 pre_thaw
331              
332             $serializer->on( pre_thaw => sub {
333             my ($serializer, $slush) = @_;
334             ...
335             });
336              
337             Emitted immediately before transactions are deserialized. See L</DATA STRUCTURE> below for details
338             of the format of $slush.
339              
340             =head2 post_thaw
341              
342             # Note that $transactions is an arrayref here.
343             $serializer->on( post_thaw => sub {
344             my ($serializer, $transactions, $slush) = @_;
345             ...
346             }
347              
348             Emitted immediately after transactions are deserialized. See L</DATA STRUCTURE> below for details
349             of the format of $slush.
350              
351             In addition, each transaction, as well as each message therein, serialized using this module will
352             emit the following events:
353              
354             =head2 pre_freeze
355              
356             $transaction->on(freeze => sub {
357             my $tx = shift;
358             ...
359             });
360              
361             Emitted immediately before the transaction is serialized.
362              
363             =head2 post_freeze
364              
365             Emitted immediately after the transaction is serialized. See L</Messages> for details of the
366             frozen format.
367              
368             $transaction->on(post_freeze => sub {
369             my $tx = shift;
370             my $frozen = shift;
371             ...
372             });
373              
374             =head1 DATA STRUCTURE
375              
376             L<serialize> produces, and L<deserialize> expects, JSON data. Transactions are stored as an array
377             of JSON objects (i.e. hashes). Each transaction object has the keys:
378              
379             =over 4
380              
381             =item 'class'
382              
383             The original class of the transaction.
384              
385             =item 'request'
386              
387             The request portion of the transaction (e.g. "GET /foo/bar ..."). See L</Messages> below for
388             encoding details.
389              
390             =item 'response'
391              
392             The response portion of the transaction (e.g. "200 OK ..."). See L</Messages> below for encoding
393             details.
394              
395             =back
396              
397             =head2 Messages
398              
399             Individual messages are stored as JSON objects (i.e. hashes) with the keys:
400              
401             =over 4
402              
403             =item 'class'
404              
405             The class name of the serialized object. This should be a subclass of L<Mojo::Message>
406              
407             =item 'events'
408              
409             Array of events with subscribers in the serialized object. These events will be re-emitted after
410             the L</thaw> event is emitted, but any subscribers present in the original object will be lost.
411              
412             =item 'body'
413              
414             The raw HTTP message body.
415              
416             =back
417              
418             =head1 CAVEATS
419              
420             This module does not serialize any event listeners. This is unlikely to change in future releases.
421              
422             =head1 AUTHOR
423              
424             Kit Peters <popefelix@gmail.com>
425              
426             =head1 COPYRIGHT AND LICENSE
427              
428             This software is copyright (c) 2021 by Kit Peters.
429              
430             This is free software; you can redistribute it and/or modify it under
431             the same terms as the Perl 5 programming language system itself.
432              
433             =cut