File Coverage

blib/lib/PawsX/FakeImplementation/Instance.pm
Criterion Covered Total %
statement 6 8 75.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 9 11 81.8


line stmt bran cond sub pod time code
1             package PawsX::FakeImplementation::Instance {
2 4     4   639553 use Moose;
  4         36  
3 4     4   33608 use Paws;
  4         754082  
  4         133  
4 4     4   4602 use UUID qw/uuid/;
  0            
  0            
5              
6             our $VERSION = '0.02';
7              
8             with 'Paws::Net::CallerRole';
9              
10             sub caller_to_response {}
11              
12             has api_class => (
13             is => 'ro',
14             isa => 'Str',
15             required => 1,
16             );
17              
18             has params => (
19             is => 'ro',
20             isa => 'HashRef',
21             default => sub { {} },
22             );
23              
24             has instance => (
25             is => 'ro',
26             lazy => 1,
27             default => sub {
28             my $self = shift;
29             Paws->load_class($self->api_class);
30             return $self->api_class->new(%{ $self->params });
31             }
32             );
33              
34             sub do_call {
35             my ($self, $service, $call_obj) = @_;
36              
37             my $uuid = uuid;
38              
39             my ($method_name) = ($call_obj->meta->name =~ m/^Paws::.*?::(.*)$/);
40              
41             my $return = eval { $self->instance->$method_name($call_obj) };
42             if ($@) {
43             if (ref($@)) {
44             if ($@->isa('Paws::Exception')){
45             $@->throw;
46             } else {
47             Paws::Exception->throw(message => "$@", code => 'InternalError', request_id => $uuid);
48             }
49             } else {
50             Paws::Exception->throw(message => "$@", code => 'InternalError', request_id => $uuid);
51             }
52             } else {
53             if (not defined $call_obj->_returns or $call_obj->_returns eq 'Paws::API::Response') {
54             $return = Paws::API::Response->new(request_id => $uuid);
55             } else {
56             $return = $self->new_with_coercions($call_obj->_returns, %$return);
57             }
58             }
59             return $return;
60             }
61              
62             sub new_with_coercions {
63             my ($self, $class, %params) = @_;
64              
65             Paws->load_class($class);
66             my %p;
67              
68             if ($class->does('Paws::API::StrToObjMapParser')) {
69             my ($subtype) = ($class->meta->find_attribute_by_name('Map')->type_constraint =~ m/^HashRef\[(.*?)\]$/);
70             if (my ($array_of) = ($subtype =~ m/^ArrayRef\[(.*?)\]$/)){
71             $p{ Map } = { map { $_ => [ map { $self->new_with_coercions("$array_of", %$_) } @{ $params{ $_ } } ] } keys %params };
72             } else {
73             $p{ Map } = { map { $_ => $self->new_with_coercions("$subtype", %{ $params{ $_ } }) } keys %params };
74             }
75             } elsif ($class->does('Paws::API::StrToNativeMapParser')) {
76             $p{ Map } = { %params };
77             } else {
78             foreach my $att (keys %params){
79             my $att_meta = $class->meta->find_attribute_by_name($att);
80            
81             Moose->throw_error("$class doesn't have an $att") if (not defined $att_meta);
82             my $type = $att_meta->type_constraint;
83            
84             if ($type eq 'Bool') {
85             $p{ $att } = ($params{ $att } == 1)?1:0;
86             } elsif ($type eq 'Str' or $type eq 'Num' or $type eq 'Int') {
87             $p{ $att } = $params{ $att };
88             } elsif ($type =~ m/^ArrayRef\[(.*?)\]$/){
89             my $subtype = "$1";
90             if ($subtype eq 'Str' or $subtype eq 'Str|Undef' or $subtype eq 'Num' or $subtype eq 'Int' or $subtype eq 'Bool') {
91             $p{ $att } = $params{ $att };
92             } else {
93             $p{ $att } = [ map { $self->new_with_coercions("$subtype", %{ $_ }) } @{ $params{ $att } } ];
94             }
95             } elsif ($type->isa('Moose::Meta::TypeConstraint::Enum')){
96             $p{ $att } = $params{ $att };
97             } else {
98             $p{ $att } = $self->new_with_coercions("$type", %{ $params{ $att } });
99             }
100             }
101             }
102             return $class->new(%p);
103             }
104             }
105             1;
106             ### main pod documentation begin ###
107              
108             =encoding UTF-8
109              
110             =head1 NAME
111              
112             PawsX::FakeImplementation::Instance - A Paws extension to help you write fake AWS services
113              
114             =head1 SYNOPSIS
115              
116             use Paws;
117             use Paws::Net::MultiplexCaller;
118             use PawsX::FakeImplementation::Instance;
119              
120             my $paws = Paws->new(
121             config => {
122             caller => Paws::Net::MultiplexCaller->new(
123             caller_for => {
124             SQS => PawsX::FakeImplementation::Instance->new(
125             api_class => 'FakeSQS',
126             ),
127             }
128             ),
129             }
130             );
131              
132             my $sqs = $paws->service('SQS', region => 'test');
133             my $new_queue = $sqs->CreateQueue(QueueName => 'MyQueue');
134             # the FakeSQS implementation has returned a $new_queue object just
135             # like SQS would:
136             # $new_queue->QueueUrl eq 'http://sqs.fake.amazonaws.com/123456789012/MyQueue'
137              
138             my $qurl = $result->QueueUrl;
139            
140             my $sent_mess = $sqs->SendMessage(MessageBody => 'Message 1', QueueUrl => $new_queue->QueueUrl);
141             # $sent_mess->MessageId has a unique id
142              
143             my $rec_mess = $sqs->ReceiveMessage(QueueUrl => $qurl);
144             # $rec_mess->Messages->[0]->Body eq 'Message 1'
145              
146             =head1 DESCRIPTION
147              
148             When working heavily with AWS services you will sometimes have special needs:
149              
150             =over
151              
152             =item Working on a plane (or in situations with limited connectivity)
153              
154             =item Testing your application
155              
156             =item Generating faults
157              
158             =back
159              
160             PawsX::FakeImplementation::Instance will help you create fake implementations for any service you
161             want. You will be able to emulate any service of your choice, and implement the behaviour you need to
162             be tested.
163              
164             PawsX::FakeImplementation::Instance teams up with Paws::Net::MultiplexCaller to route the appropiate
165             service calls to the appropiate fake implementation. See L<Paws::Net::MultiplexCaller> for more info
166              
167             =head1 Creating a fake implementation
168              
169             PawsX::FakeImplementation::Instance defines an interface between the fake implementations and Paws so
170             that it's easy to write these fake implementations. Here's a guide by example to do it:
171              
172             =head2 Create your fake implementation class
173              
174             We start out creating a new class for our fake service:
175              
176             package My::Fake::SQS;
177             use Moose;
178              
179             1;
180              
181             Be careful with namespacing: take into account that your fake implementation could be partial
182             (not a full emulation of the service), or your fake could have specialized behaviour like:
183              
184             =over
185              
186             =item Only implementing a subset of calls
187              
188             =item Only implementing partial behaviour for some calls (feature incomplete)
189              
190             =item Implements some type of failure mode (fails one of every 10 calls to the service)
191              
192             =back
193              
194             So please try to name your fakes accordingly: C<My::Fake::BasicSQS>, C<My::Fake::SQS::OutOfOrder>,
195             C<My::FakeSQS::OnlyAdministrativeCalls>, C<My::FakeSQS::FailSomeCalls>
196              
197             If you are going to write a generic fake, trying to closely emulate the AWS service, you can use
198             the C<PawsX::FakeService::SERVICE_NAME> namespace.
199              
200             Please have the behaviour of these generic fakes well tested and be willing to accept contributions
201             from third parties to these fakes, as people will probably turn to those implementations by default
202             to test services. Please document any already known differences between the real service and the
203             fake service.
204              
205             =head2 Write a fake method
206              
207             Just create a sub named like the method you want to fake in your fake service class. It will receive
208             an object with the parameters that were passed to the service:
209              
210             sub CreateQueue {
211             my ($self, $params) = @_;
212             # $params->QueueName holds what the user passed to
213             # $sqs->CreateQueue(QueueName => '...');
214             return { QueueUrl => 'http://myqueue' };
215             }
216              
217             The $params object in this case is a C<Paws::SQS::CreateQueue> object (that represents the parameters
218             to the CreateQueue call.
219              
220             =head2 Return values
221              
222             The return of CreateQueue is a hashref that contains the attributes for inflating a C<Paws::SQS::CreateQueueResult>.
223             PawsX::FakeImplementation::Instance will convert the hashref to the appropiate return object that the calling
224             code is expecting.
225            
226             sub CreateQueue {
227             return { QueueUrl => 'http://myqueue' };
228             }
229              
230             from the fake implementation will be received in the calling side as always:
231              
232             my $return = $sqs->CreateQueue(QueueName => 'x');
233             print $return->QueueUrl; # http://myqueue
234              
235             =head2 Controlled exceptions
236              
237             If the code inside a fake implementation throws or returns a Paws::Exception, the "user code" will recieve the
238             Paws::Exception just like if Paws had generated it.
239              
240             sub CreateQueue {
241             Paws::Exception->throw(message => 'The name is duplicate', code => 'DuplicateName');
242             }
243              
244             This helps emulate error conditions just like Paws/AWS returns them
245              
246             =head2 Uncontrolled exceptions
247              
248             If the code inside a fake implementation dies, PawsX::FakeImplementation::Instance will wrap it inside a generic
249             Paws::Exception object with code 'InternalError' and the exception as the message. Paws' contract with the
250             outside world is to throw Paws::Exception objects in case of problems, so PawsX::FakeImplementation::Instance
251             tries to not bubble non-Paws-compliant exceptions.
252              
253             =head2 Instance Storage
254              
255             PawsX::FakeImplementation::Instance instances your object one time only, and after that routes method calls
256             to your object. This lets you use an attribute to store data for the lifetime of your object, and use it
257             as "storage". A queue service, for example could use
258              
259             has _queue => (is => 'ro', isa => 'ArrayRef');
260              
261             as storage for the messages it enqueues. Every call to the fake services methods will see $self->_queue, and
262             be able to manipulate it:
263              
264             sub ReceiveMessage {
265             my ($self, $params) = @_;
266              
267             my $message = pop @{ $self->_queue };
268             return { Messages => $message };
269             }
270              
271             =head2 Externally configurable attributes
272              
273             If you want your fake service to be configurable in some way, you can specify an attribute in your
274             fake service class.
275              
276             package My::Fake::FailingSQS;
277             use Moose;
278              
279             has failing_call_ratio => (is => 'ro', isa => 'Num', default => 0.5);
280              
281             sub CreateQueue {
282             my ($self, $params) = @_;
283             die "Strange error" if (rand() < $self->failing_call_ratio);
284             }
285              
286             1;
287              
288             The user of the fake service can then initialize it in the following way:
289              
290             my $paws = Paws->new(
291             config => {
292             caller => Paws::Net::MultiplexCaller->new(
293             caller_for => {
294             SQS => PawsX::FakeImplementation::Instance->new(
295             api_class => 'My::Fake::FailingSQS',
296             params => {
297             failing_call_ratio => 1 # all calls fail
298             }
299             ),
300             }
301             ),
302             }
303             );
304              
305             It's recommended that your attribute either has a default (for easy usage), or declares itself as required
306             as to guide the consumer of the fake service what parameters need to be passed.
307              
308             =head1 AUTHOR
309              
310             Jose Luis Martinez
311             CPAN ID: JLMARTIN
312             CAPSiDE
313             jlmartinez@capside.com
314              
315             =head1 SEE ALSO
316              
317             L<Paws>
318              
319             L<Paws::Net::MultiplexCaller>
320              
321             L<https://github.com/pplu/aws-sdk-perl>
322              
323             =head1 BUGS and SOURCE
324              
325             The source code is located here: L<https://github.com/pplu/pawsx-fakeimplementation-instance>
326              
327             Please report bugs to: L<https://github.com/pplu/pawsx-fakeimplementation-instance/issues>
328              
329             =head1 COPYRIGHT and LICENSE
330              
331             Copyright (c) 2017 by CAPSiDE
332              
333             This code is distributed under the Apache 2 License. The full text of the license can be found in the LICENSE file included with this module.