File Coverage

blib/lib/Mojo/UserAgent/Mockable/Serializer.pm
Criterion Covered Total %
statement 117 143 81.8
branch 21 40 52.5
condition 12 15 80.0
subroutine 23 26 88.4
pod 4 4 100.0
total 177 228 77.6


line stmt bran cond sub pod time code
1 22     22   1598196 use 5.014;
  22         117  
2              
3             package Mojo::UserAgent::Mockable::Serializer;
4             $Mojo::UserAgent::Mockable::Serializer::VERSION = '1.57';
5 22     22   156 use warnings::register;
  22         46  
  22         2708  
6              
7 22     22   150 use Carp;
  22         57  
  22         1431  
8 22     22   9476 use Class::Load ':all';
  22         289106  
  22         3399  
9 22     22   10463 use English qw/-no_match_vars/;
  22         38619  
  22         130  
10 22     22   24634 use Path::Tiny;
  22         237690  
  22         1370  
11 22     22   3315 use JSON::MaybeXS qw/encode_json decode_json/;
  22         16859  
  22         1373  
12 22     22   1106 use Mojo::Base 'Mojo::EventEmitter';
  22         340534  
  22         193  
13 22     22   21071 use Safe::Isa (qw/$_isa/);
  22         9912  
  22         2255  
14 22     22   169 use Try::Tiny;
  22         50  
  22         40797  
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 2195283 my ( $self, @transactions ) = @_;
23              
24 74         236 my @serialized = map { $self->_serialize_tx($_) } @transactions;
  326         895  
25 74         351 for (0 .. $#serialized) {
26 326         640 $serialized[$_]->{txn_num} = $_;
27             }
28 74         5767 return encode_json( \@serialized );
29             }
30              
31             sub _serialize_tx {
32 326     326   876 my ( $self, $transaction ) = @_;
33              
34 326 50       993 if ( !$transaction->$_isa('Mojo::Transaction') ) {
35 0         0 croak q{Only instances of Mojo::Transaction may be serialized using this class};
36             }
37              
38 326         5475 $transaction->emit('pre_freeze');
39 326         3564 my $slush = {
40             request => $self->_serialize_message( $transaction->req ),
41             response => $self->_serialize_message( $transaction->res ),
42             class => ref $transaction,
43             };
44 326         654 for my $event ( keys %{ $transaction->{'events'} } ) {
  326         953  
45 327 50 100     2174 next if $event eq 'pre_freeze' or $event eq 'post_freeze' or $event eq 'resume'
      100        
      66        
46             or $event eq 'finish'; # 'finish' comes from Mojo::IOLoop; we probably don't need to serialize it
47 0 0       0 carp(qq{Subscriber for event "$event" not serialized}) if warnings::enabled;
48 0         0 push @{ $slush->{'events'} }, $event;
  0         0  
49             }
50              
51 326         1063 $transaction->emit( 'post_freeze', $slush );
52              
53 326         4485 return $slush;
54             }
55              
56             sub _serialize_message {
57 652     652   3274 my ( $self, $message ) = @_;
58              
59 652         1816 $message->emit('pre_freeze');
60 652         6523 my $slush = {
61             class => ref $message,
62             body => $message->to_string,
63             };
64 652 100       139473 if ( $message->can('url') ) {
65 326         821 $slush->{url} = _freeze_url( $message->url );
66             }
67 652         1128 for my $event ( keys %{ $message->{'events'} } ) {
  652         1756  
68 4 50 66     25 next if $event eq 'pre_freeze' or $event eq 'post_freeze';
69 0 0       0 carp(qq{Subscriber for event "$event" not serialized}) if warnings::enabled;
70 0         0 push @{ $slush->{'events'} }, $event;
  0         0  
71             }
72              
73 652         2077 $message->emit( 'post_freeze', $slush );
74 652         7166 return $slush;
75             }
76              
77             sub _freeze_url {
78 326     326   1582 my $url = shift;
79 326 50       777 if ( !$url->$_isa('Mojo::URL') ) {
80 0         0 $url = Mojo::URL->new($url);
81             }
82 326         4263 my $slush;
83 326         771 for my $attr (qw/scheme userinfo host port path query fragment/) {
84 2282 100       162710 $slush->{$attr} = sprintf '%s', $url->$attr if defined $url->$attr;
85             }
86 326 50       1679 if ( %{ $url->base } ) {
  326         791  
87 0         0 $slush->{base} = _freeze_url( $url->base );
88             }
89 326         3017 return $slush;
90             }
91              
92             sub deserialize {
93 30     30 1 30675 my ( $self, $frozen ) = @_;
94              
95 30         2851 my $slush = decode_json($frozen);
96              
97 30 50       201 if ( ref $slush ne 'ARRAY' ) {
98 0         0 croak q{Invalid serialized data: not stored as array.};
99             }
100 30         271 $self->emit( 'pre_thaw', $slush );
101              
102 30         1200 my @transactions;
103 30         69 for my $tx_num ( 0 .. $#{$slush} ) {
  30         139  
104 145         282 my $tx;
105             try {
106 145     145   7211 $tx = $self->_deserialize_tx( $slush->[$tx_num] );
107             }
108             catch {
109 0     0   0 my $tx_num = ( $tx_num + 1 );
110 0         0 croak qq{Error deserializing transaction $tx_num: $_};
111 145         978 };
112              
113 145         1954 push @transactions, $tx;
114             }
115              
116 30         167 $self->emit( 'post_thaw', \@transactions, $slush );
117 30         9510 return @transactions;
118             }
119              
120             sub _deserialize_tx {
121 145     145   321 my ( $self, $slush ) = @_;
122              
123 145         323 for my $key (qw/class request response/) {
124 435 50       1098 if ( !defined $slush->{$key} ) {
125 0         0 croak qq{Invalid serialized data: Missing required key '$key'};
126             }
127             }
128              
129 145         587 load_class( $slush->{'class'} );
130 145         5301 my $obj = $slush->{'class'}->new();
131              
132 145 50       1060 if ( !$obj->$_isa('Mojo::Transaction') ) {
133 0         0 croak q{Only instances of Mojo::Transaction may be deserialized using this class};
134             }
135              
136 145         2168 my $response;
137             try {
138 145     145   5958 $response = $self->_deserialize_message( $slush->{response} );
139             }
140             catch {
141 0     0   0 die qq{Response deserialization failed: $_\n};
142 145         887 };
143 145         2899 $obj->res($response);
144              
145 145         922 my $request;
146             try {
147 145     145   6598 $request = $self->_deserialize_message( $slush->{request} );
148             }
149             catch {
150 0     0   0 die qq{Request deserialization failed: $_\n};
151 145         827 };
152 145         2384 $obj->req($request);
153              
154 145 50       1033 if ( $slush->{'events'} ) {
155 0         0 for my $event ( @{ $slush->{'events'} } ) {
  0         0  
156 0         0 $obj->emit($event);
157             }
158             }
159 145         360 return $obj;
160             }
161              
162             sub _deserialize_message {
163 290     290   651 my ( $self, $slush ) = @_;
164 290         597 for my $key (qw/body class/) {
165 580 50       1516 if ( !$slush->{$key} ) {
166 0         0 croak qq{Invalid serialized data: missing required key "$key"};
167             }
168             }
169              
170 290         913 load_class( $slush->{'class'} );
171 290         9192 my $obj = $slush->{'class'}->new;
172 290 100 66     2534 if ( $slush->{'url'} && $obj->can('url') ) {
173 145         395 $obj->url( _thaw_url( $slush->{url} ) );
174             }
175 290 50       1792 if ( !$obj->can('parse') ) {
176 0         0 die qq{Message class "$slush->{class}" must define the 'parse' method\n};
177             }
178 290         1089 $obj->parse( $slush->{'body'} );
179              
180 290 50       208135 if ( !$obj->can('emit') ) {
181 0         0 die qq{Message class "$slush->{class}" must define the 'emit' method\n};
182             }
183 290 50       821 if ( $slush->{'events'} ) {
184 0         0 for my $event ( @{ $slush->{'events'} } ) {
  0         0  
185 0         0 $obj->emit($event);
186             }
187             }
188              
189 290         850 return $obj;
190             }
191              
192             sub _thaw_url {
193 145     145   250 my $slush = shift;
194             # FIXME: Temporary workaround
195 145 50       389 return Mojo::URL->new($slush) unless ref $slush;
196              
197 145         522 my $url = Mojo::URL->new;
198              
199 145         1175 for my $attr ( keys %{$slush} ) {
  145         622  
200 656         8983 $url->$attr( $slush->{$attr} );
201             }
202 145 50       2644 if ( $slush->{base} ) {
203 0         0 $url->base( _thaw_url( $slush->{base} ) );
204             }
205 145         444 return $url;
206             }
207              
208             sub store {
209 69     69 1 1490430 my ( $self, $file, @transactions ) = @_;
210              
211 69         352 my $serialized = $self->serialize(@transactions);
212 69         453 path($file)->spew_utf8($serialized);
213             }
214              
215             sub retrieve {
216 25     25 1 54631 my ( $self, $file ) = @_;
217              
218 25         122 my $contents = path($file)->slurp_utf8;
219 25         6850 return $self->deserialize($contents);
220             }
221             1;
222              
223             __END__
224              
225             =pod
226              
227             =encoding UTF-8
228              
229             =head1 NAME
230              
231             Mojo::UserAgent::Mockable::Serializer - A class that serializes Mojo transactions created by Mojo::UserAgent::Mockable.
232              
233             =head1 VERSION
234              
235             version 1.57
236              
237             =head1 SYNOPSIS
238              
239             # This module is not intended to be used directly. Synopsis here is given to show how
240             # Mojo::UserAgent::Mockable uses the module to record transactions.
241            
242             use Mojo::UserAgent::Mockable::Serializer;
243             use Mojo::UserAgent;
244             use File::Slurper qw(read_text write_text);
245              
246             my $ua = Mojo::UserAgent->new;
247             my $serializer = Mojo::UserAgent::Mockable::Serializer->new;
248            
249             my @transactions;
250             push @transactions, $ua->get('http://example.com');
251             push @transactions, $ua->get('http://example.com/object/123');
252             push @transactions, $ua->get('http://example.com/subobject/456');
253              
254             my $json = $serializer->serialize(@transactions);
255             write_text('/path/to/file/json', $json);
256              
257             # OR
258              
259             $serializer->store('/path/to/file.json', @transactions);
260              
261             # Later...
262              
263             my $json = read_text('/path/to/file.json');
264             my @reconstituted_transactions = $serializer->deserialize($json);
265              
266             # OR
267             #
268             my @reconstituted_transactions = Mojo::UserAgent::Mockable::Serializer->retrieve('/path/to/file.json');
269              
270             =head1 METHODS
271              
272             =head2 serialize
273              
274             Serialize or freeze one or more instances of L<Mojo::Transaction>. Takes an array of transactions
275             to be serialized as the single argument. This method will generate a warning if the instance has
276             any subscribers (see L<Mojo::EventEmitter/on>). Suppress this warning with (e.g.):
277              
278             no warnings 'Mojo::UserAgent::Mock::Serializer';
279             $serializer->serialize(@transactions);
280             use warnings 'Mojo::UserAgent::Mock::Serializer';
281              
282             =head2 deserialize
283              
284             Deserialize or thaw a previously serialized array of L<Mojo:Transaction>. Arguments:
285              
286             =over 4
287              
288             =item $data
289              
290             JSON containing the serialized objects.
291              
292             =back
293              
294             =head2 store
295              
296             Serialize an instance of L<Mojo::Transaction> and write it to the given file or file handle. Takes two
297             arguments:
298              
299             =over 4
300              
301             =item $file
302              
303             File or handle to write serialized object to.
304              
305             =item @transactions
306              
307             Array of L<Mojo::Transaction> to serialize
308              
309             =back
310              
311             =head2 retrieve
312              
313             Read from the specified file or file handle and deserialize one or more instances of
314             L<Mojo::Transaction> from the data read. If a file handle is passed, data will be
315             read until an EOF is received. Arguments:
316              
317             =over 4
318              
319             =item $file
320              
321             File containing serialized object
322              
323             =back
324              
325             =head1 EVENTS
326              
327             This module emits the following events:
328              
329             =head2 pre_thaw
330              
331             $serializer->on( pre_thaw => sub {
332             my ($serializer, $slush) = @_;
333             ...
334             });
335              
336             Emitted immediately before transactions are deserialized. See L</DATA STRUCTURE> below for details
337             of the format of $slush.
338              
339             =head2 post_thaw
340              
341             # Note that $transactions is an arrayref here.
342             $serializer->on( post_thaw => sub {
343             my ($serializer, $transactions, $slush) = @_;
344             ...
345             }
346              
347             Emitted immediately after transactions are deserialized. See L</DATA STRUCTURE> below for details
348             of the format of $slush.
349              
350             In addition, each transaction, as well as each message therein, serialized using this module will
351             emit the following events:
352              
353             =head2 pre_freeze
354              
355             $transaction->on(freeze => sub {
356             my $tx = shift;
357             ...
358             });
359              
360             Emitted immediately before the transaction is serialized.
361              
362             =head2 post_freeze
363              
364             Emitted immediately after the transaction is serialized. See L</Messages> for details of the
365             frozen format.
366              
367             $transaction->on(post_freeze => sub {
368             my $tx = shift;
369             my $frozen = shift;
370             ...
371             });
372              
373             =head1 DATA STRUCTURE
374              
375             L<serialize> produces, and L<deserialize> expects, JSON data. Transactions are stored as an array
376             of JSON objects (i.e. hashes). Each transaction object has the keys:
377              
378             =over 4
379              
380             =item 'class'
381              
382             The original class of the transaction.
383              
384             =item 'request'
385              
386             The request portion of the transaction (e.g. "GET /foo/bar ..."). See L</Messages> below for
387             encoding details.
388              
389             =item 'response'
390              
391             The response portion of the transaction (e.g. "200 OK ..."). See L</Messages> below for encoding
392             details.
393              
394             =back
395              
396             =head2 Messages
397              
398             Individual messages are stored as JSON objects (i.e. hashes) with the keys:
399              
400             =over 4
401              
402             =item 'class'
403              
404             The class name of the serialized object. This should be a subclass of L<Mojo::Message>
405              
406             =item 'events'
407              
408             Array of events with subscribers in the serialized object. These events will be re-emitted after
409             the L</thaw> event is emitted, but any subscribers present in the original object will be lost.
410              
411             =item 'body'
412              
413             The raw HTTP message body.
414              
415             =back
416              
417             =head1 CAVEATS
418              
419             This module does not serialize any event listeners. This is unlikely to change in future releases.
420              
421             =head1 AUTHOR
422              
423             Kit Peters <popefelix@gmail.com>
424              
425             =head1 COPYRIGHT AND LICENSE
426              
427             This software is copyright (c) 2019 by Kit Peters.
428              
429             This is free software; you can redistribute it and/or modify it under
430             the same terms as the Perl 5 programming language system itself.
431              
432             =cut