]> Dogcows Code - chaz/graphql-client/blob - lib/GraphQL/Client.pm
rename class attribute to transport_class
[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), every response -- whether success or failure -- is enveloped like this:
276
277 {
278 data => {...},
279 errors => [...],
280 }
281
282 where C<data> might be missing or undef if errors occurred (though not necessarily) and C<errors>
283 will be missing if the response completed without error.
284
285 It is up to you to check for errors in the response, so your code might look like this:
286
287 my $response = $graphql->execute(...);
288 if (my $errors = $response->{errors}) {
289 # handle $errors
290 }
291 else {
292 my $data = $response->{data};
293 # do something with $data
294 }
295
296 If C<unpack> is 1 (on), then L</execute> will return just the data if there were no errors,
297 otherwise it will throw an exception. So your code would instead look like this:
298
299 my $data = eval { $graphql->execute(...) };
300 if (my $error = $@) {
301 # handle errors
302 }
303 else {
304 # do something with $data
305 }
306
307 Or if you want to handle errors in a different stack frame, your code is simply this:
308
309 my $data = $graphql->execute(...);
310 # do something with $data
311
312 Both styles map to L<Future> responses intuitively. If C<unpack> is 0, the response always resolves
313 to the envelope structure. If C<unpack> is 1, successful responses will resolve to just the data and
314 errors will fail/reject.
315
316 =head1 SEE ALSO
317
318 =for :list
319 * L<graphql> - CLI program
320 * L<GraphQL> - Perl implementation of a GraphQL server
321 * L<https://graphql.org/> - GraphQL project website
322
This page took 0.055885 seconds and 4 git commands to generate.