File Coverage

blib/lib/Plack/App/GraphQL.pm
Criterion Covered Total %
statement 71 92 77.1
branch 8 24 33.3
condition 11 30 36.6
subroutine 29 37 78.3
pod 1 23 4.3
total 120 206 58.2


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