]> Dogcows Code - chaz/graphql-client/blob - lib/GraphQL/Client.pm
c4a61192950bfa4d5c9d9f077def3ec232cf07e2
[chaz/graphql-client] / lib / GraphQL / Client.pm
1 package GraphQL::Client;
2 # ABSTRACT: A GraphQL client
3
4 use warnings;
5 use strict;
6
7 use Module::Load qw(load);
8 use Scalar::Util qw(reftype);
9 use namespace::clean;
10
11 our $VERSION = '0.602'; # VERSION
12
13 sub _croak { require Carp; goto &Carp::croak }
14 sub _throw { GraphQL::Client::Error->throw(@_) }
15
16 sub new {
17 my $class = shift;
18 bless {@_}, $class;
19 }
20
21 sub execute {
22 my $self = shift;
23 my ($query, $variables, $operation_name, $options) = @_;
24
25 if ((reftype($operation_name) || '') eq 'HASH') {
26 $options = $operation_name;
27 $operation_name = undef;
28 }
29
30 my $request = {
31 query => $query,
32 ($variables && %$variables) ? (variables => $variables) : (),
33 $operation_name ? (operationName => $operation_name) : (),
34 };
35
36 return $self->_handle_result($self->transport->execute($request, $options));
37 }
38
39 sub _handle_result {
40 my $self = shift;
41 my ($result) = @_;
42
43 my $handle_result = sub {
44 my $result = shift;
45 my $resp = $result->{response};
46 if (my $exception = $result->{error}) {
47 unshift @{$resp->{errors}}, {
48 message => "$exception",
49 };
50 }
51 if ($self->unpack) {
52 if ($resp->{errors}) {
53 _throw $resp->{errors}[0]{message}, {
54 type => 'graphql',
55 response => $resp,
56 details => $result->{details},
57 };
58 }
59 return $resp->{data};
60 }
61 return $resp;
62 };
63
64 if (eval { $result->isa('Future') }) {
65 return $result->transform(
66 done => sub {
67 my $result = shift;
68 my $resp = eval { $handle_result->($result) };
69 if (my $err = $@) {
70 Future::Exception->throw("$err", $err->{type}, $err->{response}, $err->{details});
71 }
72 return $resp;
73 },
74 );
75 }
76 else {
77 return $handle_result->($result);
78 }
79 }
80
81 sub url {
82 my $self = shift;
83 $self->{url};
84 }
85
86 sub transport_class {
87 my $self = shift;
88 $self->{transport_class};
89 }
90
91 sub transport {
92 my $self = shift;
93 $self->{transport} //= do {
94 my $class = $self->_autodetermine_transport_class;
95 eval { load $class };
96 if ((my $err = $@) || !$class->can('execute')) {
97 $err ||= "Loaded $class, but it doesn't look like a proper transport.\n";
98 warn $err if $ENV{GRAPHQL_CLIENT_DEBUG};
99 _croak "Failed to load transport for \"${class}\"";
100 }
101 $class->new(%$self);
102 };
103 }
104
105 sub unpack {
106 my $self = shift;
107 $self->{unpack} //= 0;
108 }
109
110 sub _url_protocol {
111 my $self = shift;
112
113 my $url = $self->url;
114 my ($protocol) = $url =~ /^([^+:]+)/;
115
116 return $protocol;
117 }
118
119 sub _autodetermine_transport_class {
120 my $self = shift;
121
122 my $class = $self->transport_class;
123 return _expand_class($class) if $class;
124
125 my $protocol = $self->_url_protocol;
126 _croak 'Failed to determine transport from URL' if !$protocol;
127
128 $class = lc($protocol);
129 $class =~ s/[^a-z]/_/g;
130
131 return _expand_class($class);
132 }
133
134 sub _expand_class {
135 my $class = shift;
136 $class = "GraphQL::Client::$class" unless $class =~ s/^\+//;
137 $class;
138 }
139
140 {
141 package GraphQL::Client::Error;
142
143 use warnings;
144 use strict;
145
146 use overload '""' => \&error, fallback => 1;
147
148 sub new { bless {%{$_[2] || {}}, error => $_[1] || 'Something happened'}, $_[0] }
149
150 sub error { "$_[0]->{error}" }
151 sub type { "$_[0]->{type}" }
152
153 sub throw {
154 my $self = shift;
155 die $self if ref $self;
156 die $self->new(@_);
157 }
158 }
159
160 1;
161
162 __END__
163
164 =pod
165
166 =encoding UTF-8
167
168 =head1 NAME
169
170 GraphQL::Client - A GraphQL client
171
172 =head1 VERSION
173
174 version 0.602
175
176 =head1 SYNOPSIS
177
178 my $graphql = GraphQL::Client->new(url => 'http://localhost:4000/graphql');
179
180 # Example: Hello world!
181
182 my $response = $graphql->execute('{hello}');
183
184 # Example: Kitchen sink
185
186 my $query = q[
187 query GetHuman {
188 human(id: $human_id) {
189 name
190 height
191 }
192 }
193 ];
194 my $variables = {
195 human_id => 1000,
196 };
197 my $operation_name = 'GetHuman';
198 my $transport_options = {
199 headers => {
200 authorization => 'Bearer s3cr3t',
201 },
202 };
203 my $response = $graphql->execute($query, $variables, $operation_name, $transport_options);
204
205 # Example: Asynchronous with Mojo::UserAgent (promisify requires Future::Mojo)
206
207 my $ua = Mojo::UserAgent->new;
208 my $graphql = GraphQL::Client->new(ua => $ua, url => 'http://localhost:4000/graphql');
209
210 my $future = $graphql->execute('{hello}');
211
212 $future->promisify->then(sub {
213 my $response = shift;
214 ...
215 });
216
217 =head1 DESCRIPTION
218
219 C<GraphQL::Client> provides a simple way to execute L<GraphQL|https://graphql.org/> queries and
220 mutations on a server.
221
222 This module is the programmatic interface. There is also a L<"CLI program"|graphql>.
223
224 GraphQL servers are usually served over HTTP. The provided transport, L<GraphQL::Client::http>, lets
225 you plug in your own user agent, so this client works naturally with L<HTTP::Tiny>,
226 L<Mojo::UserAgent>, and more. You can also use L<HTTP::AnyUA> middleware.
227
228 =head1 ATTRIBUTES
229
230 =head2 url
231
232 The URL of a GraphQL endpoint, e.g. C<"http://myapiserver/graphql">.
233
234 =head2 unpack
235
236 Whether or not to "unpack" the response, which enables a different style for error-handling.
237
238 Default is 0.
239
240 See L</ERROR HANDLING>.
241
242 =head2 transport_class
243
244 The package name of a transport.
245
246 This is optional if the correct transport can be correctly determined from the L</url>.
247
248 =head2 transport
249
250 The transport object.
251
252 By default this is automatically constructed based on L</transport_class> or L</url>.
253
254 =head1 METHODS
255
256 =head2 new
257
258 $graphql = GraphQL::Client->new(%attributes);
259
260 Construct a new client.
261
262 =head2 execute
263
264 $response = $graphql->execute($query);
265 $response = $graphql->execute($query, \%variables);
266 $response = $graphql->execute($query, \%variables, $operation_name);
267 $response = $graphql->execute($query, \%variables, $operation_name, \%transport_options);
268 $response = $graphql->execute($query, \%variables, \%transport_options);
269
270 Execute a request on a GraphQL server, and get a response.
271
272 By default, the response will either be a hashref with the following structure or a L<Future> that
273 resolves to such a hashref, depending on the transport and how it is configured.
274
275 {
276 data => {
277 field1 => {...}, # or [...]
278 ...
279 },
280 errors => [
281 { message => 'some error message blah blah blah' },
282 ...
283 ],
284 }
285
286 Note: Setting the L</unpack> attribute affects the response shape.
287
288 =head1 ERROR HANDLING
289
290 There are two different styles for handling errors.
291
292 If L</unpack> is 0 (off, the default), every response -- whether success or failure -- is enveloped
293 like this:
294
295 {
296 data => {...},
297 errors => [...],
298 }
299
300 where C<data> might be missing or undef if errors occurred (though not necessarily) and C<errors>
301 will be missing if the response completed without error.
302
303 It is up to you to check for errors in the response, so your code might look like this:
304
305 my $response = $graphql->execute(...);
306 if (my $errors = $response->{errors}) {
307 # handle $errors
308 }
309 else {
310 my $data = $response->{data};
311 # do something with $data
312 }
313
314 If C<unpack> is 1 (on), then L</execute> will return just the data if there were no errors,
315 otherwise it will throw an exception. So your code would instead look like this:
316
317 my $data = eval { $graphql->execute(...) };
318 if (my $error = $@) {
319 my $resp = $error->{response};
320 # handle errors
321 }
322 else {
323 # do something with $data
324 }
325
326 Or if you want to handle errors in a different stack frame, your code is simply this:
327
328 my $data = $graphql->execute(...);
329 # do something with $data
330
331 Both styles map to L<Future> responses intuitively. If C<unpack> is 0, the response always resolves
332 to the envelope structure. If C<unpack> is 1, successful responses will resolve to just the data and
333 errors will fail/reject.
334
335 =head1 SEE ALSO
336
337 =over 4
338
339 =item *
340
341 L<graphql> - CLI program
342
343 =item *
344
345 L<GraphQL> - Perl implementation of a GraphQL server
346
347 =item *
348
349 L<https://graphql.org/> - GraphQL project website
350
351 =back
352
353 =head1 BUGS
354
355 Please report any bugs or feature requests on the bugtracker website
356 L<https://github.com/chazmcgarvey/graphql-client/issues>
357
358 When submitting a bug or request, please include a test-file or a
359 patch to an existing test-file that illustrates the bug or desired
360 feature.
361
362 =head1 AUTHOR
363
364 Charles McGarvey <chazmcgarvey@brokenzipper.com>
365
366 =head1 COPYRIGHT AND LICENSE
367
368 This software is copyright (c) 2020 by Charles McGarvey.
369
370 This is free software; you can redistribute it and/or modify it under
371 the same terms as the Perl 5 programming language system itself.
372
373 =cut
This page took 0.056373 seconds and 3 git commands to generate.