X-Git-Url: https://git.dogcows.com/gitweb?a=blobdiff_plain;f=lib%2FGraphQL%2FClient%2Fhttp.pm;h=77a089f2e319b5f3627ac31c1a9ceac3407fede2;hb=1a966ff0db27d934fce36e2bd43ea6aace0a7555;hp=5b9b634f8fa53630576ae087debbf168a2d462aa;hpb=697f37637e11830cac0913e18aa7ebd41e6af508;p=chaz%2Fgraphql-client diff --git a/lib/GraphQL/Client/http.pm b/lib/GraphQL/Client/http.pm index 5b9b634..77a089f 100644 --- a/lib/GraphQL/Client/http.pm +++ b/lib/GraphQL/Client/http.pm @@ -1,33 +1,42 @@ package GraphQL::Client::http; # ABSTRACT: GraphQL over HTTP +use 5.010; use warnings; use strict; use HTTP::AnyUA::Util qw(www_form_urlencode); use HTTP::AnyUA; +use namespace::clean; our $VERSION = '999.999'; # VERSION +sub _croak { require Carp; goto &Carp::croak } + sub new { my $class = shift; - bless {@_}, $class; + my $self = @_ % 2 == 0 ? {@_} : $_[0]; + bless $self, $class; } -sub request { +sub execute { my $self = shift; my ($request, $options) = @_; - my $url = $options->{url} || $self->url; - my $method = $options->{method} || $self->method; + my $url = delete $options->{url} || $self->url; + my $method = delete $options->{method} || $self->method; + + $request && ref($request) eq 'HASH' or _croak q{Usage: $http->execute(\%request)}; + $request->{query} or _croak q{Request must have a query}; + $url or _croak q{URL must be provided}; my $data = {%$request}; if ($method eq 'GET' || $method eq 'HEAD') { $data->{variables} = $self->json->encode($data->{variables}) if $data->{variables}; my $params = www_form_urlencode($data); - my $sep = $url =~ /\?/ ? '&' : '?'; - $url .= "${sep}${params}"; + my $sep = $url =~ /^[^#]+\?/ ? '&' : '?'; + $url =~ s/#/${sep}${params}#/ or $url .= "${sep}${params}"; } else { my $encoded_data = $self->json->encode($data); @@ -36,41 +45,83 @@ sub request { $options->{headers}{'content-type'} = 'application/json'; } - return $self->_handle_response($self->_any_ua->request($method, $url, $options)); + return $self->_handle_response($self->any_ua->request($method, $url, $options)); } sub _handle_response { my $self = shift; my ($resp) = @_; - my $handle_error = sub { - my $resp = shift; + if (eval { $resp->isa('Future') }) { + return $resp->followed_by(sub { + my $f = shift; + + if (my ($exception, $category, @other) = $f->failure) { + if (ref $exception eq 'HASH') { + my $resp = $exception; + return Future->done($self->_handle_error($resp)); + } + + return Future->done({ + error => $exception, + response => undef, + details => { + exception_details => [$category, @other], + }, + }); + } + + my $resp = $f->get; + return Future->done($self->_handle_success($resp)); + }); + } + else { + return $self->_handle_error($resp) if !$resp->{success}; + return $self->_handle_success($resp); + } +} - return { - errors => [ - { - message => "HTTP transport returned $resp->{status}: $resp->{content}", - x_transport_response => $resp, - }, - ], - }; - }; - my $handle_response = sub { - my $resp = shift; +sub _handle_error { + my $self = shift; + my ($resp) = @_; + + my $data = eval { $self->json->decode($resp->{content}) }; + my $content = $resp->{content} // 'No content'; + my $reason = $resp->{reason} // ''; + my $message = "HTTP transport returned $resp->{status} ($reason): $content"; + + chomp $message; - return $handle_error->($resp) if !$resp->{success}; - return $self->json->decode($resp->{content}); + return { + error => $message, + response => $data, + details => { + http_response => $resp, + }, }; +} - if ($self->_any_ua->response_is_future) { - return $resp->transform( - done => $handle_response, - fail => $handle_error, - ); - } - else { - return $handle_response->($resp); +sub _handle_success { + my $self = shift; + my ($resp) = @_; + + my $data = eval { $self->json->decode($resp->{content}) }; + if (my $exception = $@) { + return { + error => "HTTP transport failed to decode response: $exception", + response => undef, + details => { + http_response => $resp, + }, + }; } + + return { + response => $data, + details => { + http_response => $resp, + }, + }; } sub ua { @@ -78,11 +129,16 @@ sub ua { $self->{ua} //= do { require HTTP::Tiny; HTTP::Tiny->new( - agent => "perl-graphql-client/$VERSION", + agent => $ENV{GRAPHQL_CLIENT_HTTP_USER_AGENT} // "perl-graphql-client/$VERSION", ); }; } +sub any_ua { + my $self = shift; + $self->{any_ua} //= HTTP::AnyUA->new(ua => $self->ua); +} + sub url { my $self = shift; $self->{url}; @@ -101,11 +157,6 @@ sub json { }; } -sub _any_ua { - my $self = shift; - $self->{_any_ua} //= HTTP::AnyUA->new(ua => $self->ua); -} - 1; __END__ @@ -116,7 +167,17 @@ __END__ method => 'POST', ); - my $data = $client->request($query, $variables, $operation_name, $options); + my $request = { + query => 'query Greet($name: String) { hello(name: $name) }', + operationName => 'Greet', + variables => { name => 'Bob' }, + }; + my $options = { + headers => { + authorization => 'Bearer s3cr3t', + }, + }; + my $response = $transport->execute($request, $options); =head1 DESCRIPTION @@ -125,7 +186,7 @@ You probably shouldn't use this directly. Instead use L. C is a GraphQL transport for HTTP. GraphQL is not required to be transported via HTTP, but this is definitely the most common way. -This also serves as a reference implementation for future GraphQL transports. +This also serves as a reference implementation for C transports. =method new @@ -133,23 +194,33 @@ This also serves as a reference implementation for future GraphQL transports. Construct a new GraphQL HTTP transport. -=method request +See L. - $response = $client->request(\%data, \%options); +=method execute + + $response = $client->execute(\%request); + $response = $client->execute(\%request, \%options); Get a response from the GraphQL server. -The C<%data> structure must have a C key whose value is the query or mutation string. It may -optionally have a C hashref an an C string. +The C<%request> structure must have a C key whose value is the query or mutation string. It +may optionally have a C hashref and an C string. -The C<%options> structure contains options passed through to the user agent. +The C<%options> structure is optional and may contain options passed through to the user agent. The +only useful options are C (which should have a hashref value) and C and C to +override the attributes of the same names. The response will either be a hashref with the following structure or a L that resolves to such a hashref: { - data => {...}, - errors => [...], + response => { # decoded response (may be undef if an error occurred) + data => {...}, + errors => [...], + }, + error => 'Something happened', # omitted if no error occurred + details => { # optional information which may aide troubleshooting + }, } =attr ua @@ -164,6 +235,14 @@ A user agent, such as: See L. +=attr any_ua + +The L instance. Can be used to apply middleware if desired. + +=attr url + +The http URL of a GraphQL endpoint, e.g. C<"http://myapiserver/graphql">. + =attr method The HTTP method to use when querying the GraphQL server. Can be one of: @@ -172,6 +251,10 @@ The HTTP method to use when querying the GraphQL server. Can be one of: * C * C (default) +GraphQL servers should be able to handle both, but you can set this explicitly to one or the other +if you're dealing with a server that is opinionated. You can also provide a different HTTP method, +but anything other than C and C are less likely to work. + =attr json The L (or compatible) object used for encoding and decoding data structures to and from