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