File Coverage

blib/lib/Plack/App/GraphQL.pm
Criterion Covered Total %
statement 72 93 77.4
branch 8 24 33.3
condition 12 32 37.5
subroutine 30 38 78.9
pod 1 24 4.1
total 123 211 58.2


line stmt bran cond sub pod time code
1             package Plack::App::GraphQL;
2              
3 1     1   3194 use Plack::Util;
  1         19509  
  1         28  
4 1     1   1421 use Plack::Request;
  1         98743  
  1         30  
5 1     1   1382 use GraphQL::Execution;
  1         2204765  
  1         58  
6 1     1   5061 use Safe::Isa;
  1         666  
  1         124  
7 1     1   7 use Moo;
  1         2  
  1         8  
8              
9             extends 'Plack::Component';
10              
11             our $VERSION = '0.003';
12              
13             has convert => (
14             is => 'ro',
15             isa => sub { ref($_[0]) ? 1:0 },
16             predicate => 'has_convert',
17             coerce => sub {
18             if(ref($_[0]) eq 'ARRAY') {
19             my ($class_proto, @args) = @{$_[0]};
20             return normalize_convert_class($class_proto)->to_graphql(@args);
21             } else {
22             return $_[0]; # assume its a hashref already.
23             }
24             },
25             );
26              
27             sub normalize_convert_class {
28 0     0 0 0 my $class_proto = shift;
29 0 0       0 my $class = $class_proto =~m/^\+(.+)$/ ?
30             $1 : "GraphQL::Plugin::Convert::$class_proto";
31 0         0 return Plack::Util::load_class($class);
32             }
33              
34             has schema => (
35             is => 'ro',
36             lazy => 1,
37             required => 1,
38             builder => '_build_schema',
39             coerce => sub {
40             my $schema_proto = shift;
41             return ( ref($schema_proto) =~m/GraphQL::Schema/) ?
42             $schema_proto :
43             coerce_schema($schema_proto);
44             }
45             );
46              
47             sub coerce_schema {
48             my $source = Plack::Util::is_real_fh($_[0]) ?
49 1 50   1 0 4 do { local $/ = undef; <$_[0]> } :
  0         0  
  0         0  
50             $_[0];
51 1         31 return Plack::Util::load_class("GraphQL::Schema")
52             ->from_doc($source);
53             }
54              
55             sub _build_schema {
56 0     0   0 my $self = shift;
57             return $self->has_convert ?
58             $self->convert->{schema} :
59 0 0       0 undef;
60             }
61              
62             has root_value => (
63             is => 'ro',
64             required => 1,
65             lazy => 1,
66             builder => '_build_root_value',
67             );
68              
69             sub _build_root_value {
70 0     0   0 my $self = shift;
71             return $self->has_convert ?
72             $self->convert->{root_value} :
73 0 0       0 undef;
74             }
75              
76             has resolver => (
77             is => 'ro',
78             required => 0,
79             lazy => 1,
80             builder => '_build_resolver',
81             );
82              
83             sub _build_resolver {
84 1     1   29 my $self = shift;
85             return $self->has_convert ?
86             $self->convert->{resolver} :
87 1 50       10 undef;
88             }
89              
90             has promise_code => (
91             is => 'ro',
92             required => 0,
93             lazy => 1,
94             ); # TODO waiting on GraphQL
95              
96             has endpoint => (
97             is => 'ro',
98             required => 1,
99             builder => 'DEFAULT_ENDPOINT',
100             );
101              
102 1   50 1 0 92 sub DEFAULT_ENDPOINT { our $DEFAULT_ENDPOINT ||= '/' }
103              
104             has context_class => (
105             is => 'ro',
106             required => 1,
107             builder => 'DEFAULT_CONTEXT_CLASS',
108             coerce => sub { Plack::Util::load_class($_[0]) },
109             );
110              
111 1   50 1 0 4279 sub DEFAULT_CONTEXT_CLASS { our $DEFAULT_CONTEXT_CLASS ||= 'Plack::App::GraphQL::Context' }
112              
113             has ui_template_class => (
114             is => 'ro',
115             required => 1,
116             init_arg => undef,
117             builder => 'DEFAULT_UI_TEMPLATE_CLASS',
118             coerce => sub { Plack::Util::load_class(shift) },
119             );
120              
121 1   50 1 0 59748 sub DEFAULT_UI_TEMPLATE_CLASS { our $DEFAULT_UI_TEMPLATE_CLASS ||= 'Plack::App::GraphQL::UITemplate' }
122              
123             has ui_template => (
124             is => 'ro',
125             required => 1,
126             init_arg => undef,
127             builder => '_build_ui_template',
128             lazy => 1,
129             );
130              
131             sub _build_ui_template {
132 0     0   0 my $self = shift;
133 0         0 $self->ui_template_class->new(json_encoder => $self->json_encoder, graphiql_version => $self->graphiql_version);
134             }
135              
136             has graphiql => (
137             is => 'ro',
138             required => 1,
139             builder => 'DEFAULT_GRAPHIQL',
140             );
141              
142 1   50 1 0 126 sub DEFAULT_GRAPHIQL { our $DEFAULT_GRAPHIQL ||= 0 }
143              
144             has graphiql_version => (
145             is => 'ro',
146             required => 1,
147             builder => 'DEFAULT_GRAPHIQL_VERSION',
148             );
149              
150 1   50 1 0 98 sub DEFAULT_GRAPHIQL_VERSION { our $DEFAULT_GRAPHIQL_VERSION ||= 'latest' }
151              
152             has json_encoder => (
153             is => 'ro',
154             required => 1,
155             handles => {
156             json_encode => 'encode',
157             json_decode => 'decode',
158             },
159             builder => '_build_json_encoder',
160             );
161              
162             our $DEFAULT_JSON_CLASS = 'JSON::MaybeXS';
163             sub _build_json_encoder {
164 1   33 1   12 return our $JSON_ENCODER ||= Plack::Util::load_class($DEFAULT_JSON_CLASS)
165             ->new
166             ->utf8
167             ->allow_nonref;
168             }
169              
170             has exceptions_class => (
171             is => 'ro',
172             required => 1,
173             builder => 'DEFAULT_EXCEPTIONS_CLASS',
174             coerce => sub { Plack::Util::load_class(shift) },
175             );
176              
177             our $DEFAULT_EXCEPTIONS_CLASS = 'Plack::App::GraphQL::Exceptions';
178 1     1 0 12 sub DEFAULT_EXCEPTIONS_CLASS { $DEFAULT_EXCEPTIONS_CLASS }
179              
180             has exceptions => (
181             is => 'ro',
182             required => 1,
183             init_arg => undef,
184             lazy => 1,
185             handles => [qw(respond_415 respond_404 respond_400)],
186             builder => '_build_exceptions',
187             );
188              
189             sub _build_exceptions {
190 0     0   0 my $self = shift;
191 0         0 return $self->exceptions_class->new(psgi_app=>$self);
192             }
193              
194             sub call {
195 1     1 1 24173 my ($self, $env) = @_;
196 1         12 my $req = Plack::Request->new($env);
197 1 50       14 return $self->respond($req) if $self->matches_endpoint($req);
198 0         0 return $self->respond_404($req);
199             }
200              
201             sub matches_endpoint {
202 1     1 0 3 my ($self, $req) = @_;
203 1 50       4 return $req->env->{PATH_INFO} eq $self->endpoint ? 1:0;
204             }
205              
206             sub respond {
207 1     1 0 16 my ($self, $req) = @_;
208 1 50   0   16 return sub { $self->respond_graphiql($req, @_) } if $self->accepts_graphiql($req);
  0         0  
209 1 50   1   5 return sub { $self->respond_graphql($req, @_) } if $self->accepts_graphql($req);
  1         29  
210 0         0 return $self->respond_415($req);
211             }
212              
213             sub accepts_graphiql {
214 1     1 0 6 my ($self, $req) = @_;
215             return 1 if $self->graphiql
216             and (($req->env->{HTTP_ACCEPT}||'') =~ /^text\/html\b/)
217 1 0 0     19 and (!defined($req->body_parameters->{'raw'}));
      33        
      33        
218 1         4 return 0;
219             }
220              
221             sub accepts_graphql {
222 1     1 0 3 my ($self, $req) = @_;
223 1 50 50     4 return (($req->env->{HTTP_ACCEPT}||'') =~ /^application\/json\b/) ? 1:0;
224             }
225              
226             sub respond_graphiql {
227 0     0 0 0 my ($self, $req, $responder) = @_;
228 0         0 my $body = $self->ui_template->process($req);
229 0         0 return $responder->(
230             [
231             200,
232             [
233             'Content-Type' => 'text/html',
234             'Content-Length' => Plack::Util::content_length([$body]),
235             ],
236             [$body],
237             ]
238             );
239             }
240              
241             sub respond_graphql {
242 1     1 0 3 my ($self, $req, $responder) = @_;
243 1         4 my ($results, $context) = $self->prepare_results($req);
244 1         4 my $writer = $self->prepare_writer($context, $responder);
245              
246             # This is ugly; its just a placeholder until GraphQL allows Futures.
247 1 50       35 if(ref($results)=~m/Future/) {
248             $results->on_done(sub {
249 0     0   0 return $self->write_results($context, shift, $writer);
250 0         0 }); # needs on_fail...
251             } else {
252 1         4 return $self->write_results($context, $results, $writer)
253             }
254             }
255              
256             sub prepare_writer {
257 1     1 0 3 my ($self, $context, $responder) = @_;
258 1         3 my @headers = $self->prepare_headers($context);
259 1         5 return my $writer = $responder->([200, \@headers]);
260             }
261              
262             sub prepare_headers {
263 1     1 0 3 my ($self, $context) = @_;
264 1         4 return my @headers = ('Content-Type' => 'application/json');
265             }
266              
267             sub write_results {
268 1     1 0 3 my ($self, $context, $results, $writer) = @_;
269 1         18 my $body = $self->json_encode($results);
270 1         37 $writer->write($body);
271 1         23 $writer->close;
272             }
273              
274             sub prepare_results {
275 1     1 0 2 my ($self, $req) = @_;
276 1         4 my $data = $self->prepare_body($req);
277 1         5 my $context = $self->prepare_context($req, $data);
278 1         1366 my $root_value = $self->prepare_root_value($context);
279 1         48 my $results = $self->execute(
280             $self->schema,
281             $data,
282             $root_value,
283             $context,
284             $self->resolver,
285             $self->promise_code,
286             );
287 1         13603 return ($results, $context);
288             }
289              
290             sub prepare_body {
291 1     1 0 3 my ($self, $req) = @_;
292             my $json_body = eval {
293             $self->json_decode($req->raw_body());
294 1   33     2 } || do {
295             $self->respond_400($req);
296             };
297 1         475 return $json_body;
298             }
299              
300             sub prepare_context {
301 1     1 0 3 my ($self, $req) = @_;
302 1   33     5 return my $context = $req->env->{'plack.graphql.context'} ||= $self->build_context($req);
303             }
304              
305             sub build_context {
306 1     1 0 9 my ($self, $req, $data) = @_;
307 1         4 my $context_class = $self->context_class;
308             return $context_class->new(
309             request => $req,
310             data => $data,
311             app => $self,
312 1         4 log => $req->env->{'psgix.logger'},
313             );
314             }
315              
316             sub prepare_root_value {
317 1     1 0 3 my ($self, $context) = @_;
318 1   33     5 return my $root_value = $context->req->env->{'plack.graphql.root_value'} ||= $self->root_value;
319             }
320              
321             sub execute {
322 1     1 0 2 my ($self, $schema, $data, $root_value, $context, $resolver, $promise_code) = @_;
323             return my $results = GraphQL::Execution::execute(
324             $schema,
325             $data->{query},
326             $root_value,
327             $context,
328             $data->{variables},
329             $data->{operationName},
330 1         21 $resolver,
331             $promise_code,
332             );
333             }
334              
335             1;
336              
337             =head1 NAME
338            
339             Plack::App::GraphQL - Serve GraphQL from Plack / PSGI
340              
341             =begin markdown
342              
343              
344             https://travis-ci.org/jjn1056/Plack-App-GraphQL
345              
346             # PROJECT STATUS
347              
348             [![Build Status](https://travis-ci.org/jjn1056/Plack-App-GraphQL.svg?branch=master)](https://travis-ci.org/jjn1056/Plack-App-GraphQL)
349             [![CPAN version](https://badge.fury.io/pl/Plack-App-GraphQL.svg)](https://metacpan.org/pod/Plack-App-GraphQL)
350              
351             =end markdown
352            
353             =head1 SYNOPSIS
354            
355             use Plack::App::GraphQL;
356              
357             my $schema = q|
358             type Query {
359             hello: String
360             }
361             |;
362              
363             my %root_value = (
364             hello => 'Hello World!',
365             );
366              
367             my $app = Plack::App::GraphQL
368             ->new(schema => $schema, root_value => \%root_value)
369             ->to_app;
370              
371             Or mount under a given URL:
372              
373             use Plack::Builder;
374             use Plack::App::GraphQL;
375              
376             # $schema and %root_value as above
377              
378             my $app = Plack::App::GraphQL
379             ->new(schema => $schema, root_value => \%root_value)
380             ->to_app;
381              
382             builder {
383             mount "/graphql" => $app;
384             };
385              
386             You can also use the 'endpoint' configuration option to set a root path to match.
387             This is the most simple option if you application is not serving other endpoints
388             or applications (See documentation below).
389              
390             =head1 DESCRIPTION
391            
392             Serve L<GraphQL> with L<Plack>.
393              
394             Please note this is an early access / minimal documentation release. You should already
395             be familiar with L<GraphQL>. There's some examples in C</examples> but few real test
396             cases. If you are not comfortable using this based on reading the source code and
397             can't accept the possibility that the underlying code might change (although I expect
398             the configuration options are pretty set now) then you shouldn't use this. I recommend
399             looking at official plugins for Dancer and Mojolicious: L<Dancer2::Plugin::GraphQL>,
400             L<Mojolicious::Plugin::GraphQL> instead (or you can send me patches :) ).
401              
402             This currently doesn't support an asychronous responses until updates are made in
403             core L<GraphQL>.
404              
405             =head1 CONFIGURATION
406            
407             This L<Plack> applications supports the following configuration arguments:
408              
409             =head2 schema
410              
411             The L<GraphQL::Schema>. Canonically this should be an instance of L<GraphQL::Schema>
412             but if you pass a string or a filehandle, we will assume that it is a parse-able
413             graphql SDL document that we can build a schema object from. Makes for easy demos.
414              
415             =head2 root_value
416              
417             An object, hashref or coderef that field resolvers can use to look up requests. Generally
418             the method or hash keys will match the query or mutation keys. See the examples for
419             more.
420              
421             You can override this at runtime (with for example a bit of middleware) by using the
422             'plack.graphql.root_value' key in the PSGI $env. This may or my not be considered a
423             good practice :) Some examples suggest always using the $context for stuff like this
424             while other examples seem to think its a good idea. I choose to rather enable this
425             ability and let you decide what is right for your application.
426              
427             =head2 resolver
428              
429             Used to change how field resolvers work. See L<GraphQL> (or ignore this since its likely
430             something you really don't need for normal work.
431              
432             =head2 convert
433              
434             This takes a sub class of L<GraphQL::Plugin::Convert>, such as L<GraphQL::Plugin::Convert::DBIC>.
435             Providing this will automatically provide L</schema>, L</root_value> and L</resolver>.
436              
437             You can shortcut the value of this with a '+' and we will assume the default namespace. For
438             example '+DBIC' is the same as 'GraphQL::Plugin::Convert::DBIC'.
439              
440             =head2 endpoint
441              
442             The URI path part that is associated with the graphql API endpoint. Often this is set to
443             'graphql'. The default is '/'. You might prefer to use a custom or alternative router
444             (for example L<Plack::Builder>).
445              
446             =head2 context_class
447              
448             Default is L<Plack::App::GraphQL::Context>. This is an object that is passed as the 'context'
449             argument to your field resolvers. You might wish to subclass this to add additional useful
450             methods such as simple access to a user object (if you you authentication for example).
451              
452             =head2 graphiql_version
453              
454             String that defaults to 'latest'. This allows you to specify the graphiql version to use.
455              
456             B<NOTE> Setting this does not enable GraphiQL.
457              
458             =head2 graphiql
459              
460             Boolean that defaults to FALSE. Turn this on to enable the HTML Interactive GraphQL query
461             screen. Useful for leaning and debugging but you probably want it off in production.
462              
463             B<NOTE> If you want to use this you should also install L<Template::Tiny> which is needed. We
464             don't make L<Template::Tiny> a dependency here so that you are not forced to install it where
465             you don't want the interactive screens (such as production).
466              
467             =head2 json_encoder
468              
469             Lets you specify the instance of the class used for JSON encoding / decoding. The default is an
470             instance of L<JSON::MaybeXS> so you will want to be sure install a fast JSON de/encoder in production,
471             such as L<Cpanel::JSON::XS> (it will default to a pure Perl one which might not need your speed
472             requirements).
473              
474             =head2 exceptions_class
475              
476             Class that provides the exception responses. Override the default (L<Plack::App::GraphQL::Exceptions>)
477             if you want complete control over how your errors look.
478              
479             =head1 METHODS
480              
481             TBD
482            
483             =head1 AUTHOR
484            
485             John Napiorkowski <jnapiork@cpan.org>
486              
487             =head1 SEE ALSO
488            
489             L<GraphQL>, L<Plack>
490              
491             =head1 COPYRIGHT
492              
493             Copyright (c) 2019 by "AUTHOR" as listed above.
494              
495             =head1 LICENSE
496              
497             This library is free software and may be distributed under the same terms as perl itself.
498            
499             =cut