]> Dogcows Code - chaz/graphql-client/blob - lib/GraphQL/Client.pm
Release GraphQL-Client 0.605
[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 = '999.999'; # 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 __END__
162
163 =head1 SYNOPSIS
164
165 my $graphql = GraphQL::Client->new(url => 'http://localhost:4000/graphql');
166
167 # Example: Hello world!
168
169 my $response = $graphql->execute('{hello}');
170
171 # Example: Kitchen sink
172
173 my $query = q[
174 query GetHuman {
175 human(id: $human_id) {
176 name
177 height
178 }
179 }
180 ];
181 my $variables = {
182 human_id => 1000,
183 };
184 my $operation_name = 'GetHuman';
185 my $transport_options = {
186 headers => {
187 authorization => 'Bearer s3cr3t',
188 },
189 };
190 my $response = $graphql->execute($query, $variables, $operation_name, $transport_options);
191
192 # Example: Asynchronous with Mojo::UserAgent (promisify requires Future::Mojo)
193
194 my $ua = Mojo::UserAgent->new;
195 my $graphql = GraphQL::Client->new(ua => $ua, url => 'http://localhost:4000/graphql');
196
197 my $future = $graphql->execute('{hello}');
198
199 $future->promisify->then(sub {
200 my $response = shift;
201 ...
202 });
203
204 =head1 DESCRIPTION
205
206 C<GraphQL::Client> provides a simple way to execute L<GraphQL|https://graphql.org/> queries and
207 mutations on a server.
208
209 This module is the programmatic interface. There is also a L<"CLI program"|graphql>.
210
211 GraphQL servers are usually served over HTTP. The provided transport, L<GraphQL::Client::http>, lets
212 you plug in your own user agent, so this client works naturally with L<HTTP::Tiny>,
213 L<Mojo::UserAgent>, and more. You can also use L<HTTP::AnyUA> middleware.
214
215 =method new
216
217 $graphql = GraphQL::Client->new(%attributes);
218
219 Construct a new client.
220
221 =method execute
222
223 $response = $graphql->execute($query);
224 $response = $graphql->execute($query, \%variables);
225 $response = $graphql->execute($query, \%variables, $operation_name);
226 $response = $graphql->execute($query, \%variables, $operation_name, \%transport_options);
227 $response = $graphql->execute($query, \%variables, \%transport_options);
228
229 Execute a request on a GraphQL server, and get a response.
230
231 By default, the response will either be a hashref with the following structure or a L<Future> that
232 resolves to such a hashref, depending on the transport and how it is configured.
233
234 {
235 data => {
236 field1 => {...}, # or [...]
237 ...
238 },
239 errors => [
240 { message => 'some error message blah blah blah' },
241 ...
242 ],
243 }
244
245 Note: Setting the L</unpack> attribute affects the response shape.
246
247 =attr url
248
249 The URL of a GraphQL endpoint, e.g. C<"http://myapiserver/graphql">.
250
251 =attr unpack
252
253 Whether or not to "unpack" the response, which enables a different style for error-handling.
254
255 Default is 0.
256
257 See L</ERROR HANDLING>.
258
259 =attr transport_class
260
261 The package name of a transport.
262
263 This is optional if the correct transport can be correctly determined from the L</url>.
264
265 =attr transport
266
267 The transport object.
268
269 By default this is automatically constructed based on L</transport_class> or L</url>.
270
271 =head1 ERROR HANDLING
272
273 There are two different styles for handling errors.
274
275 If L</unpack> is 0 (off, the default), every response -- whether success or failure -- is enveloped
276 like this:
277
278 {
279 data => {...},
280 errors => [...],
281 }
282
283 where C<data> might be missing or undef if errors occurred (though not necessarily) and C<errors>
284 will be missing if the response completed without error.
285
286 It is up to you to check for errors in the response, so your code might look like this:
287
288 my $response = $graphql->execute(...);
289 if (my $errors = $response->{errors}) {
290 # handle $errors
291 }
292 else {
293 my $data = $response->{data};
294 # do something with $data
295 }
296
297 If C<unpack> is 1 (on), then L</execute> will return just the data if there were no errors,
298 otherwise it will throw an exception. So your code would instead look like this:
299
300 my $data = eval { $graphql->execute(...) };
301 if (my $error = $@) {
302 my $resp = $error->{response};
303 # handle errors
304 }
305 else {
306 # do something with $data
307 }
308
309 Or if you want to handle errors in a different stack frame, your code is simply this:
310
311 my $data = $graphql->execute(...);
312 # do something with $data
313
314 Both styles map to L<Future> responses intuitively. If C<unpack> is 0, the response always resolves
315 to the envelope structure. If C<unpack> is 1, successful responses will resolve to just the data and
316 errors will fail/reject.
317
318 =head1 SEE ALSO
319
320 =for :list
321 * L<graphql> - CLI program
322 * L<GraphQL> - Perl implementation of a GraphQL server
323 * L<https://graphql.org/> - GraphQL project website
324
This page took 0.052745 seconds and 4 git commands to generate.