File Coverage

blib/lib/Swagger2/Client.pm
Criterion Covered Total %
statement 125 126 99.2
branch 44 54 81.4
condition 20 28 71.4
subroutine 24 29 82.7
pod 1 1 100.0
total 214 238 89.9


line stmt bran cond sub pod time code
1             package Swagger2::Client;
2 4     4   364374 use Mojo::Base -base;
  4         7  
  4         33  
3              
4 4     4   2290 use Mojo::JSON;
  4         48080  
  4         233  
5 4     4   2031 use Mojo::UserAgent;
  4         421243  
  4         42  
6 4     4   144 use Mojo::Util;
  4         6  
  4         142  
7 4     4   20 use Carp ();
  4         6  
  4         53  
8 4     4   1442 use Swagger2;
  4         12  
  4         30  
9 4     4   122 use JSON::Validator::OpenAPI;
  4         6  
  4         186  
10              
11 4   50 4   17 use constant DEBUG => $ENV{SWAGGER2_DEBUG} || 0;
  4         5  
  4         6982  
12              
13             has base_url => sub { Mojo::URL->new(shift->_swagger->base_url) };
14             has ua => sub { Mojo::UserAgent->new };
15             has _validator => sub { JSON::Validator::OpenAPI->new; };
16              
17             sub generate {
18 4     4 1 42 my $class = shift;
19 4         15 my ($swagger, $url) = _swagger_url(shift);
20 4   50     48 my $paths = $swagger->api_spec->get('/paths') || {};
21 4         110 my $generated;
22              
23 4 50       18 $generated
24             = 40 < length $url ? Mojo::Util::md5_sum($url) : $url; # 40 is a bit random: not too long
25 4         145 $generated =~ s!\W!_!g;
26 4         250 $generated = "$class\::$generated";
27              
28 4 50       45 return $generated->new if $generated->isa($class); # already generated
29 4         17 _init_package($generated, $class);
30 4     5   75 Mojo::Util::monkey_patch($generated, _swagger => sub {$swagger});
  5     5   689  
31              
32 4         82 for my $path (keys %$paths) {
33 11         131 for my $http_method (keys %{$paths->{$path}}) {
  11         38  
34 20         191 my $op_spec = $paths->{$path}{$http_method};
35 20   66     68 my $method = $op_spec->{operationId} || $path;
36 20         77 my $code = $generated->_generate_method(lc $http_method, $path, $op_spec);
37              
38 20         50 $method =~ s![^\w]!_!g;
39 20         17 warn "[$generated] Add method $generated\::$method()\n" if DEBUG;
40 20         44 Mojo::Util::monkey_patch($generated, $method => $code);
41              
42 20         382 my $snake = Mojo::Util::decamelize(ucfirst $method);
43 20         358 warn "[$generated] Add method $generated\::$snake()\n" if DEBUG;
44 20         49 Mojo::Util::monkey_patch($generated, $snake => $code);
45             }
46             }
47              
48 4         120 return $generated->new;
49             }
50              
51             sub _generate_method {
52 20     20   33 my ($class, $http_method, $path, $op_spec) = @_;
53 20         65 my @path = grep {length} split '/', $path;
  49         91  
54              
55             return sub {
56 15 100   15   23597 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
        15      
        15      
        15      
        15      
        15      
        0      
        0      
        0      
        0      
        0      
57 15         23 my $self = shift;
58 15   100     63 my $args = shift || {};
59 15         57 my $req = [$self->base_url->clone];
60 15         1026 my @e = $self->_validate_request($args, $op_spec, $req);
61              
62 15 100       38 if (@e) {
63 5 100       15 unless ($cb) {
64 3 100       13 return _invalid_input_res(\@e) if $self->return_on_error;
65 2         23 Carp::croak('Invalid input: ' . join ' ', @e);
66             }
67 2         10 $self->$cb(\@e, undef);
68 2         51 return $self;
69             }
70              
71 10         49 push @{$req->[0]->path->parts},
72 10   50     13 map { local $_ = $_; s,\{(\w+)\},{$args->{$1}//''},ge; $_; } @path;
  15         563  
  15         42  
  3         5  
  3         20  
  15         44  
73              
74 10 100       26 if ($cb) {
75 3         10 Scalar::Util::weaken($self);
76             $self->ua->$http_method(
77             @$req,
78             sub {
79 3     3   6190 my ($ua, $tx) = @_;
80 3 100       11 return $self->$cb('', $tx->res) unless my $err = $tx->error;
81 1         16 return $self->$cb($err->{message}, $tx->res);
82             }
83 3         12 );
84 3         2841 return $self;
85             }
86             else {
87 7         26 my $tx = $self->ua->$http_method(@$req);
88 7 100 100     17372 return $tx->res if !$tx->error or $self->return_on_error;
89 2         54 Carp::croak(join ': ', grep {defined} $tx->error->{message}, $tx->res->body);
  4         393  
90             }
91 20         168 };
92             }
93              
94             sub _init_package {
95 4     4   17 my ($package, $base) = @_;
96 4 50   4   35 eval <<"HERE" or die "package $package: $@";
  4         5  
  4         33  
  4         597  
97             package $package;
98             use Mojo::Base '$base';
99             has return_on_error => 0;
100             1;
101             HERE
102             }
103              
104             sub _invalid_input_res {
105 1     1   22 my $res = Mojo::Message::Response->new;
106 1         21 $res->headers->content_type('application/json');
107 1         86 $res->body(Mojo::JSON::encode_json({errors => $_[0]}));
108 1         191 $res->code(400)->message($res->default_message);
109 1         24 $res->error({message => 'Invalid input', code => 400});
110             }
111              
112             sub _swagger_url {
113 4 100   4   28 if (UNIVERSAL::isa($_[0], 'Swagger2')) {
114 1         4 my $swagger = shift->load->expand;
115 1         12 return ($swagger, $swagger->url);
116             }
117             else {
118 3         8 my $url = shift;
119 3         15 return (Swagger2->new->load($url)->expand, $url);
120             }
121             }
122              
123             sub _validate_request {
124 15     15   26 my ($self, $args, $op_spec, $req) = @_;
125 15         44 my $query = $req->[0]->query;
126 15         112 my (%data, $body, @e);
127              
128 15 50       19 for my $p (@{$op_spec->{parameters} || []}) {
  15         66  
129 15         66 my ($in, $name, $type) = @$p{qw(in name type)};
130 15 100       57 my $value = exists $args->{$name} ? $args->{$name} : $p->{default};
131              
132 15 100 100     86 if (defined $value or Swagger2::_is_true($p->{required})) {
133 11   100     36 $type ||= 'object';
134              
135 11 100       36 if (defined $value) {
136 7 100 100     77 $value += 0 if $type =~ /^(?:integer|number)/ and $value =~ /^\d/;
137 7 0 0     30 $value = ($value eq 'false' or !$value) ? Mojo::JSON->false : Mojo::JSON->true
    50          
138             if $type eq 'boolean';
139             }
140              
141 11 100 66     60 if ($in eq 'body') {
    100          
142 3         4 warn "[Swagger2::Client] Validate $in\n" if DEBUG;
143             push @e,
144 2 50       224 map { $_->{path} = $_->{path} eq "/" ? "/$name" : "/$name$_->{path}"; $_; }
  2         7  
145 3         14 $self->_validator->validate($value, $p->{schema});
146             }
147             elsif ($in eq 'formData' and $type eq 'file') {
148 2         4 warn "[Swagger2::Client] Validate $in file=$name (@{[defined $value ? 1 : 0]})\n" if DEBUG;
149             }
150             else {
151 6         19 warn "[Swagger2::Client] Validate $in $name=$value\n" if DEBUG;
152 6         38 push @e, $self->_validator->validate({$name => $value}, {properties => {$name => $p}});
153             }
154             }
155              
156 15 100       1320 if (not defined $value) {
    100          
    50          
    100          
    100          
157 8         23 next;
158             }
159             elsif ($in eq 'query') {
160 2         9 $query->param($name => $value);
161             }
162             elsif ($in eq 'header') {
163 0         0 $req->[1]{$name} = $value;
164             }
165             elsif ($in eq 'body') {
166 1         4 $data{json} = $value;
167             }
168             elsif ($in eq 'formData') {
169 1         4 $data{form}{$name} = $value;
170             }
171             }
172              
173 15         127 push @$req, map { ($_ => $data{$_}) } keys %data;
  2         7  
174 15 50       41 push @$req, $body if defined $body;
175              
176 15         43 return @e;
177             }
178              
179             1;
180              
181             =encoding utf8
182              
183             =head1 NAME
184              
185             Swagger2::Client - A client for talking to a Swagger powered server
186              
187             =head1 DEPRECATION WARNING
188              
189             See L.
190              
191             =head1 DESCRIPTION
192              
193             L is a base class for autogenerated classes that can
194             talk to a server using a swagger specification.
195              
196             Note that this is a DRAFT, so there will probably be bugs and changes.
197              
198             =head1 SYNOPSIS
199              
200             =head2 Swagger specification
201              
202             The input L given to L need to point to a valid
203             L
204             document.
205              
206             ---
207             swagger: 2.0
208             basePath: /api
209             paths:
210             /foo:
211             get:
212             operationId: listPets
213             parameters:
214             - name: limit
215             in: query
216             type: integer
217             responses:
218             200: { ... }
219              
220             =head2 Client
221              
222             The swagger specification will the be turned into a sub class of
223             L, where the "parameters" rules are used to do input
224             validation.
225              
226             use Swagger2::Client;
227             $ua = Swagger2::Client->generate("file:///path/to/api.json");
228              
229             # blocking (will croak() on error)
230             $pets = $ua->listPets;
231              
232             # blocking (will not croak() on error)
233             $ua->return_on_error(1);
234             $pets = $ua->listPets;
235              
236             # non-blocking
237             $ua = $ua->listPets(sub { my ($ua, $err, $pets) = @_; });
238              
239             # with arguments, where the key map to the "parameters" name
240             $pets = $ua->listPets({limit => 10});
241              
242             The method name added will both be the original C, but a "snake
243             case" version will also be added. Example:
244              
245             "operationId": "listPets"
246             => $client->listPets()
247             => $client->list_pets()
248              
249             =head2 Customization
250              
251             If you want to request a different server than what is specified in
252             the swagger document:
253              
254             $ua->base_url->host("other.server.com");
255              
256             =head1 ATTRIBUTES
257              
258             =head2 base_url
259              
260             $base_url = $self->base_url;
261              
262             Returns a L object with the base URL to the API.
263              
264             =head2 ua
265              
266             $ua = $self->ua;
267              
268             Returns a L object which is used to execute requests.
269              
270             =head1 METHODS
271              
272             =head2 generate
273              
274             $client = Swagger2::Client->generate(Swagger2->new($specification_url));
275             $client = Swagger2::Client->generate($specification_url);
276              
277             Returns an object of a generated class, with the rules from the
278             C<$specification_url>.
279              
280             Note that the class is cached by perl, so loading a new specification from the
281             same URL will not generate a new class.
282              
283             =head1 COPYRIGHT AND LICENSE
284              
285             Copyright (C) 2014-2015, Jan Henning Thorsen
286              
287             This program is free software, you can redistribute it and/or modify it under
288             the terms of the Artistic License version 2.0.
289              
290             =head1 AUTHOR
291              
292             Jan Henning Thorsen - C
293              
294             =cut