]> Dogcows Code - chaz/graphql-client/blob - lib/GraphQL/Client.pm
faa5edaf59dc3d823b119a474cd873ba4f6904cd
[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 Throw;
10
11 our $VERSION = '999.999'; # VERSION
12
13 sub _croak { use Carp; goto &Carp::croak }
14
15 sub new {
16 my $class = shift;
17 bless {@_}, $class;
18 }
19
20 sub request {
21 my $self = shift;
22 my ($query, $variables, $operation_name, $options) = @_;
23
24 if ((reftype($operation_name) || '') eq 'HASH') {
25 $options = $operation_name;
26 $operation_name = undef;
27 }
28
29 my $request = {
30 query => $query,
31 ($variables && %$variables) ? (variables => $variables) : (),
32 $operation_name ? (operationName => $operation_name) : (),
33 };
34
35 my $resp = $self->transport->request($request, $options);
36 return $self->_handle_response($resp);
37 }
38
39 my $ERROR_MESSAGE = 'The GraphQL server returned errors';
40 sub _handle_response {
41 my $self = shift;
42 my ($resp) = @_;
43
44 if (eval { $resp->isa('Future') }) {
45 return $resp->followed_by(sub {
46 my $f = shift;
47 if (my ($exception, $category, @details) = $f->failure) {
48 if (!$exception->{errors}) {
49 return Future->fail($exception, $category, @details);
50 }
51 if ($self->unpack) {
52 return Future->fail($ERROR_MESSAGE, 'graphql', $exception, @details);
53 }
54 return Future->done($exception);
55 }
56 else {
57 my ($resp, @other) = $f->get;
58 if ($self->unpack) {
59 if ($resp->{errors}) {
60 return Future->fail($ERROR_MESSAGE, 'graphql', $resp, @other);
61 }
62 return Future->done($resp->{data});
63 }
64 return Future->done($resp);
65 }
66 });
67 }
68 else {
69 if ($self->unpack) {
70 if ($resp->{errors}) {
71 throw $ERROR_MESSAGE, {
72 type => 'graphql',
73 response => $resp,
74 };
75 }
76 return $resp->{data};
77 }
78 return $resp;
79 }
80 }
81
82 sub url {
83 my $self = shift;
84 $self->{url};
85 }
86
87 sub class {
88 my $self = shift;
89 $self->{class};
90 }
91
92 sub transport {
93 my $self = shift;
94 $self->{transport} //= do {
95 my $class = $self->_transport_class;
96 eval { load $class };
97 if ((my $err = $@) || !$class->can('request')) {
98 $err ||= "Loaded $class, but it doesn't look like a proper transport.\n";
99 warn $err if $ENV{GRAPHQL_CLIENT_DEBUG};
100 _croak "Failed to load transport for \"${class}\"";
101 }
102 $class->new(%$self);
103 };
104 }
105
106 sub unpack {
107 my $self = shift;
108 $self->{unpack} //= 0;
109 }
110
111 sub _url_protocol {
112 my $self = shift;
113
114 my $url = $self->url;
115 my ($protocol) = $url =~ /^([^+:]+)/;
116
117 return $protocol;
118 }
119
120 sub _transport_class {
121 my $self = shift;
122
123 return _expand_class($self->{class}) if $self->{class};
124
125 my $protocol = $self->_url_protocol;
126 _croak 'Failed to determine transport from URL' if !$protocol;
127
128 my $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 1;
141 __END__
142
143 =head1 SYNOPSIS
144
145 my $client = GraphQL::Client->new();
146
147 my $data = $client->request(q[
148 query GetHuman {
149 human(id: $human_id) {
150 name
151 height
152 }
153 }
154 ], {
155 human_id => 1000,
156 });
157
158 =head1 DESCRIPTION
159
160 =method new
161
162 $client = GraphQL::Client->new(%attributes);
163
164 Construct a new client.
165
166 =method request
167
168 $response = $client->request($query);
169 $response = $client->request($query, \%variables);
170 $response = $client->request($query, \%variables, $operation_name);
171 $response = $client->request($query, \%variables, $operation_name, \%transport_options);
172 $response = $client->request($query, \%variables, \%transport_options);
173
174 Get a response from the GraphQL server.
175
176 By default, the response will either be a hashref with the following structure or a L<Future> that
177 resolves to such a hashref, depending on the transport and how it is configured.
178
179 {
180 data => {
181 field1 => {...}, # or [...]
182 ...
183 },
184 errors => [
185 { message => 'some error message blah blah blah' },
186 ...
187 ],
188 }
189
190 Note: Setting the L</unpack> attribute affects the response shape.
191
192 =attr url
193
194 The URL of a GraphQL endpoint, e.g. C<"http://myapiserver/graphql">.
195
196 This is required.
197
198 =attr class
199
200 The package name of a transport.
201
202 By default this is automatically determined from the protocol portion of the L</url>.
203
204 =attr transport
205
206 The transport object.
207
208 By default this is automatically constructed based on the L</class>.
209
210 =attr unpack
211
212 Whether or not to "unpack" the response, which enables a different style for error-handling.
213
214 Default is 0.
215
216 See L</ERROR HANDLING>.
217
218 =head1 ERROR HANDLING
219
220 There are two different styles for handling errors.
221
222 If L</unpack> is 0 (off), every response -- whether success or failure -- is enveloped like this:
223
224 {
225 data => {...},
226 errors => [...],
227 }
228
229 where C<data> might be missing or undef if errors occurred (though not necessarily) and C<errors>
230 will be missing if the response completed without error.
231
232 It is up to you to check for errors in the response, so your code might look like this:
233
234 my $response = $client->request(...);
235 if (my $errors = $response->{errors}) {
236 # handle errors
237 }
238 my $data = $response->{data};
239 # do something with $data
240
241 If C<unpack> is 1 (on), then L</request> will return just the data if there were no errors,
242 otherwise it will throw an exception. So your code would look like this:
243
244 my $data = eval { $client->request(...) };
245 if (my $error = $@) {
246 # handle errors
247 }
248 # do something with $data
249
250 Or if you want to handle errors in a different stack frame, your code is simply this:
251
252 my $data = $client->request(...);
253 # do something with $data
254
255 Both styles map to L<Future> responses intuitively. If C<unpack> is 0, the response always resolves
256 to the envelope structure. If C<unpack> is 1, successful responses will resolve to just the data and
257 errors will fail/reject.
258
This page took 0.048318 seconds and 3 git commands to generate.