]> Dogcows Code - chaz/graphql-client/blob - lib/GraphQL/Client.pm
2f844fe5b5c983ca1731675a2af458302eeb6757
[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.600'; # 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 class {
87 my $self = shift;
88 $self->{class};
89 }
90
91 sub transport {
92 my $self = shift;
93 $self->{transport} //= do {
94 my $class = $self->_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 _transport_class {
120 my $self = shift;
121
122 return _expand_class($self->{class}) if $self->{class};
123
124 my $protocol = $self->_url_protocol;
125 _croak 'Failed to determine transport from URL' if !$protocol;
126
127 my $class = lc($protocol);
128 $class =~ s/[^a-z]/_/g;
129
130 return _expand_class($class);
131 }
132
133 sub _expand_class {
134 my $class = shift;
135 $class = "GraphQL::Client::$class" unless $class =~ s/^\+//;
136 $class;
137 }
138
139 {
140 package GraphQL::Client::Error;
141
142 use warnings;
143 use strict;
144
145 use overload '""' => \&error, fallback => 1;
146
147 sub new { bless {%{$_[2] || {}}, error => $_[1] || 'Something happened'}, $_[0] }
148
149 sub error { "$_[0]->{error}" }
150 sub type { "$_[0]->{type}" }
151
152 sub throw {
153 my $self = shift;
154 die $self if ref $self;
155 die $self->new(@_);
156 }
157 }
158
159 1;
160
161 __END__
162
163 =pod
164
165 =encoding UTF-8
166
167 =head1 NAME
168
169 GraphQL::Client - A GraphQL client
170
171 =head1 VERSION
172
173 version 0.600
174
175 =head1 SYNOPSIS
176
177 my $graphql = GraphQL::Client->new(url => 'http://localhost:4000/graphql');
178
179 # Example: Hello world!
180
181 my $response = $graphql->execute('{hello}');
182
183 # Example: Kitchen sink
184
185 my $query = q[
186 query GetHuman {
187 human(id: $human_id) {
188 name
189 height
190 }
191 }
192 ];
193 my $variables = {
194 human_id => 1000,
195 };
196 my $operation_name = 'GetHuman';
197 my $transport_options = {
198 headers => {
199 authorization => 'Bearer s3cr3t',
200 },
201 };
202 my $response = $graphql->execute($query, $variables, $operation_name, $transport_options);
203
204 # Example: Asynchronous with Mojo::UserAgent (promisify requires Future::Mojo)
205
206 my $ua = Mojo::UserAgent->new;
207 my $graphql = GraphQL::Client->new(ua => $ua, url => 'http://localhost:4000/graphql');
208
209 my $future = $graphql->execute('{hello}');
210
211 $future->promisify->then(sub {
212 my $response = shift;
213 ...
214 });
215
216 =head1 DESCRIPTION
217
218 C<GraphQL::Client> provides a simple way to execute L<GraphQL|https://graphql.org/> queries and
219 mutations on a server.
220
221 This module is the programmatic interface. There is also a L<graphql|"CLI program">.
222
223 GraphQL servers are usually served over HTTP. The provided transport, L<GraphQL::Client::http>, lets
224 you plug in your own user agent, so this client works naturally with L<HTTP::Tiny>,
225 L<Mojo::UserAgent>, and more. You can also use L<HTTP::AnyUA> middleware.
226
227 =head1 ATTRIBUTES
228
229 =head2 url
230
231 The URL of a GraphQL endpoint, e.g. C<"http://myapiserver/graphql">.
232
233 =head2 class
234
235 The package name of a transport.
236
237 By default this is automatically determined from the protocol portion of the L</url>.
238
239 =head2 transport
240
241 The transport object.
242
243 By default this is automatically constructed based on the L</class>.
244
245 =head2 unpack
246
247 Whether or not to "unpack" the response, which enables a different style for error-handling.
248
249 Default is 0.
250
251 See L</ERROR HANDLING>.
252
253 =head1 METHODS
254
255 =head2 new
256
257 $graphql = GraphQL::Client->new(%attributes);
258
259 Construct a new client.
260
261 =head2 execute
262
263 $response = $graphql->execute($query);
264 $response = $graphql->execute($query, \%variables);
265 $response = $graphql->execute($query, \%variables, $operation_name);
266 $response = $graphql->execute($query, \%variables, $operation_name, \%transport_options);
267 $response = $graphql->execute($query, \%variables, \%transport_options);
268
269 Execute a request on a GraphQL server, and get a response.
270
271 By default, the response will either be a hashref with the following structure or a L<Future> that
272 resolves to such a hashref, depending on the transport and how it is configured.
273
274 {
275 data => {
276 field1 => {...}, # or [...]
277 ...
278 },
279 errors => [
280 { message => 'some error message blah blah blah' },
281 ...
282 ],
283 }
284
285 Note: Setting the L</unpack> attribute affects the response shape.
286
287 =head1 ERROR HANDLING
288
289 There are two different styles for handling errors.
290
291 If L</unpack> is 0 (off), every response -- whether success or failure -- is enveloped like this:
292
293 {
294 data => {...},
295 errors => [...],
296 }
297
298 where C<data> might be missing or undef if errors occurred (though not necessarily) and C<errors>
299 will be missing if the response completed without error.
300
301 It is up to you to check for errors in the response, so your code might look like this:
302
303 my $response = $graphql->execute(...);
304 if (my $errors = $response->{errors}) {
305 # handle $errors
306 }
307 else {
308 my $data = $response->{data};
309 # do something with $data
310 }
311
312 If C<unpack> is 1 (on), then L</execute> will return just the data if there were no errors,
313 otherwise it will throw an exception. So your code would instead look like this:
314
315 my $data = eval { $graphql->execute(...) };
316 if (my $error = $@) {
317 # handle errors
318 }
319 else {
320 # do something with $data
321 }
322
323 Or if you want to handle errors in a different stack frame, your code is simply this:
324
325 my $data = $graphql->execute(...);
326 # do something with $data
327
328 Both styles map to L<Future> responses intuitively. If C<unpack> is 0, the response always resolves
329 to the envelope structure. If C<unpack> is 1, successful responses will resolve to just the data and
330 errors will fail/reject.
331
332 =head1 SEE ALSO
333
334 =over 4
335
336 =item *
337
338 L<graphql> - CLI program
339
340 =item *
341
342 L<GraphQL> - Perl implementation of a GraphQL server
343
344 =item *
345
346 L<https://graphql.org/> - GraphQL project website
347
348 =back
349
350 =head1 BUGS
351
352 Please report any bugs or feature requests on the bugtracker website
353 L<https://github.com/chazmcgarvey/graphql-client/issues>
354
355 When submitting a bug or request, please include a test-file or a
356 patch to an existing test-file that illustrates the bug or desired
357 feature.
358
359 =head1 AUTHOR
360
361 Charles McGarvey <chazmcgarvey@brokenzipper.com>
362
363 =head1 COPYRIGHT AND LICENSE
364
365 This software is copyright (c) 2020 by Charles McGarvey.
366
367 This is free software; you can redistribute it and/or modify it under
368 the same terms as the Perl 5 programming language system itself.
369
370 =cut
This page took 0.056486 seconds and 3 git commands to generate.