]> Dogcows Code - chaz/graphql-client/blob - lib/GraphQL/Client/http.pm
add tests and many fixes
[chaz/graphql-client] / lib / GraphQL / Client / http.pm
1 package GraphQL::Client::http;
2 # ABSTRACT: GraphQL over HTTP
3
4 use 5.010;
5 use warnings;
6 use strict;
7
8 use HTTP::AnyUA::Util qw(www_form_urlencode);
9 use HTTP::AnyUA;
10 use namespace::clean;
11
12 our $VERSION = '999.999'; # VERSION
13
14 sub _croak { require Carp; goto &Carp::croak }
15
16 sub new {
17 my $class = shift;
18 my $self = @_ % 2 == 0 ? {@_} : $_[0];
19 bless $self, $class;
20 }
21
22 sub execute {
23 my $self = shift;
24 my ($request, $options) = @_;
25
26 my $url = delete $options->{url} || $self->url;
27 my $method = delete $options->{method} || $self->method;
28
29 $request && ref($request) eq 'HASH' or _croak q{Usage: $http->execute(\%request)};
30 $request->{query} or _croak q{Request must have a query};
31 $url or _croak q{URL must be provided};
32
33 my $data = {%$request};
34
35 if ($method eq 'GET' || $method eq 'HEAD') {
36 $data->{variables} = $self->json->encode($data->{variables}) if $data->{variables};
37 my $params = www_form_urlencode($data);
38 my $sep = $url =~ /^[^#]+\?/ ? '&' : '?';
39 $url =~ s/#/${sep}${params}#/ or $url .= "${sep}${params}";
40 }
41 else {
42 my $encoded_data = $self->json->encode($data);
43 $options->{content} = $encoded_data;
44 $options->{headers}{'content-length'} = length $encoded_data;
45 $options->{headers}{'content-type'} = 'application/json';
46 }
47
48 return $self->_handle_response($self->any_ua->request($method, $url, $options));
49 }
50
51 sub _handle_response {
52 my $self = shift;
53 my ($resp) = @_;
54
55 if (eval { $resp->isa('Future') }) {
56 return $resp->followed_by(sub {
57 my $f = shift;
58
59 if (my ($exception, $category, @other) = $f->failure) {
60 if (ref $exception eq 'HASH') {
61 my $resp = $exception;
62 return Future->done($self->_handle_error($resp));
63 }
64
65 return Future->done({
66 error => $exception,
67 response => undef,
68 details => {
69 exception_details => [$category, @other],
70 },
71 });
72 }
73
74 my $resp = $f->get;
75 return Future->done($self->_handle_success($resp));
76 });
77 }
78 else {
79 return $self->_handle_error($resp) if !$resp->{success};
80 return $self->_handle_success($resp);
81 }
82 }
83
84 sub _handle_error {
85 my $self = shift;
86 my ($resp) = @_;
87
88 my $data = eval { $self->json->decode($resp->{content}) };
89 my $content = $resp->{content} // 'No content';
90 my $reason = $resp->{reason} // '';
91 my $message = "HTTP transport returned $resp->{status} ($reason): $content";
92
93 return {
94 error => $message,
95 response => $data,
96 details => {
97 http_response => $resp,
98 },
99 };
100 }
101
102 sub _handle_success {
103 my $self = shift;
104 my ($resp) = @_;
105
106 my $data = eval { $self->json->decode($resp->{content}) };
107 if (my $exception = $@) {
108 return {
109 error => "HTTP transport failed to decode response: $exception",
110 response => undef,
111 details => {
112 http_response => $resp,
113 },
114 };
115 }
116
117 return {
118 response => $data,
119 details => {
120 http_response => $resp,
121 },
122 };
123 }
124
125 sub ua {
126 my $self = shift;
127 $self->{ua} //= do {
128 require HTTP::Tiny;
129 HTTP::Tiny->new(
130 agent => $ENV{GRAPHQL_CLIENT_HTTP_USER_AGENT} // "perl-graphql-client/$VERSION",
131 );
132 };
133 }
134
135 sub any_ua {
136 my $self = shift;
137 $self->{any_ua} //= HTTP::AnyUA->new(ua => $self->ua);
138 }
139
140 sub url {
141 my $self = shift;
142 $self->{url};
143 }
144
145 sub method {
146 my $self = shift;
147 $self->{method} // 'POST';
148 }
149
150 sub json {
151 my $self = shift;
152 $self->{json} //= do {
153 require JSON::MaybeXS;
154 JSON::MaybeXS->new(utf8 => 1);
155 };
156 }
157
158 1;
159 __END__
160
161 =head1 SYNOPSIS
162
163 my $transport = GraphQL::Client::http->new(
164 url => 'http://localhost:5000/graphql',
165 method => 'POST',
166 );
167
168 my $request = {
169 query => 'query Greet($name: String) { hello(name: $name) }',
170 operationName => 'Greet',
171 variables => { name => 'Bob' },
172 };
173 my $options = {
174 headers => {
175 authorization => 'Bearer s3cr3t',
176 },
177 };
178 my $response = $client->execute($request, $options);
179
180 =head1 DESCRIPTION
181
182 You probably shouldn't use this directly. Instead use L<GraphQL::Client>.
183
184 C<GraphQL::Client::http> is a GraphQL transport for HTTP. GraphQL is not required to be transported
185 via HTTP, but this is definitely the most common way.
186
187 This also serves as a reference implementation for C<GraphQL::Client> transports.
188
189 =method new
190
191 $transport = GraphQL::Client::http->new(%attributes);
192
193 Construct a new GraphQL HTTP transport.
194
195 See L</ATTRIBUTES>.
196
197 =method execute
198
199 $response = $client->execute(\%request);
200 $response = $client->execute(\%request, \%options);
201
202 Get a response from the GraphQL server.
203
204 The C<%data> structure must have a C<query> key whose value is the query or mutation string. It may
205 optionally have a C<variables> hashref and an C<operationName> string.
206
207 The C<%options> structure is optional and may contain options passed through to the user agent. The
208 only useful options are C<headers> (which should have a hashref value) and C<method> and C<url> to
209 override the attributes of the same names.
210
211 The response will either be a hashref with the following structure or a L<Future> that resolves to
212 such a hashref:
213
214 {
215 response => { # decoded response (may be undef if an error occurred)
216 data => {...},
217 errors => [...],
218 },
219 error => 'Something happened', # may be ommitted if no error occurred
220 details => { # optional information which may aide troubleshooting
221 },
222 }
223
224 =attr ua
225
226 A user agent, such as:
227
228 =for :list
229 * instance of a L<HTTP::Tiny> (this is the default if no user agent is provided)
230 * instance of a L<Mojo::UserAgent>
231 * the string C<"AnyEvent::HTTP">
232 * and more...
233
234 See L<HTTP::AnyUA/"SUPPORTED USER AGENTS">.
235
236 =attr any_ua
237
238 The L<HTTP::AnyUA> instance. Can be used to apply middleware if desired.
239
240 =attr url
241
242 The http URL of a GraphQL endpoint, e.g. C<"http://myapiserver/graphql">.
243
244 =attr method
245
246 The HTTP method to use when querying the GraphQL server. Can be one of:
247
248 =for :list
249 * C<GET>
250 * C<POST> (default)
251
252 GraphQL servers should be able to handle both, but you can set this explicitly to one or the other
253 if you're dealing with a server that is opinionated. You can also provide a different HTTP method,
254 but anything other than C<GET> and C<POST> are less likely to work.
255
256 =attr json
257
258 The L<JSON::XS> (or compatible) object used for encoding and decoding data structures to and from
259 the GraphQL server.
260
261 Defaults to a L<JSON::MaybeXS>.
262
263 =head1 SEE ALSO
264
265 L<https://graphql.org/learn/serving-over-http/>
266
This page took 0.048595 seconds and 4 git commands to generate.