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