File Coverage

blib/lib/Plack/App/GraphQL.pm
Criterion Covered Total %
statement 68 89 76.4
branch 8 24 33.3
condition 10 27 37.0
subroutine 28 36 77.7
pod 1 22 4.5
total 115 198 58.0


line stmt bran cond sub pod time code
1             package Plack::App::GraphQL;
2              
3 1     1   1565 use Plack::Util;
  1         10528  
  1         26  
4 1     1   904 use Plack::Request;
  1         66084  
  1         33  
5 1     1   1160 use GraphQL::Execution;
  1         1959255  
  1         57  
6 1     1   921 use Safe::Isa;
  1         540  
  1         122  
7 1     1   7 use Moo;
  1         2  
  1         9  
8              
9             extends 'Plack::Component';
10              
11             our $VERSION = '0.001';
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 5 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   52 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             );
95              
96             has endpoint => (
97             is => 'ro',
98             required => 1,
99             builder => 'DEFAULT_ENDPOINT',
100             );
101              
102 1   50 1 0 87 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 4217 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 57854 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 64 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   17 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 12 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 20784 my ($self, $env) = @_;
188 1         9 my $req = Plack::Request->new($env);
189 1 50       15 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       9 return $req->env->{PATH_INFO} eq $self->endpoint ? 1:0;
196             }
197              
198             sub respond {
199 1     1 0 19 my ($self, $req) = @_;
200 1 50   0   5 return sub { $self->respond_graphiql($req, @_) } if $self->accepts_graphiql($req);
  0         0  
201 1 50   1   5 return sub { $self->respond_graphql($req, @_) } if $self->accepts_graphql($req);
  1         30  
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     8 and (!defined($req->body_parameters->{'raw'}));
      33        
      33        
210 1         4 return 0;
211             }
212              
213             sub accepts_graphql {
214 1     1 0 2 my ($self, $req) = @_;
215 1 50 50     3 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 3 my ($self, $req, $responder) = @_;
235 1         5 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       60 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         5 my @headers = $self->prepare_headers($context);
251 1         5 return my $writer = $responder->([200, \@headers]);
252             }
253              
254             sub prepare_headers {
255 1     1 0 3 my ($self, $context) = @_;
256 1         5 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         22 my $body = $self->json_encode($results);
262 1         44 $writer->write($body);
263 1         25 $writer->close;
264             }
265              
266             sub prepare_results {
267 1     1 0 3 my ($self, $req) = @_;
268 1         4 my $data = $self->prepare_body($req);
269 1         5 my $context = $self->prepare_context($req, $data);
270 1         1448 my $results = $self->execute(
271             $self->schema,
272             $data,
273             $self->root_value,
274             $context,
275             $self->resolver,
276             $self->promise_code,
277             );
278 1         13649 return ($results, $context);
279             }
280              
281             sub prepare_body {
282 1     1 0 3 my ($self, $req) = @_;
283             my $json_body = eval {
284             $self->json_decode($req->raw_body());
285 1   33     2 } || do {
286             $self->respond_400($req);
287             };
288 1         505 return $json_body;
289             }
290              
291             sub prepare_context {
292 1     1 0 3 my ($self, $req) = @_;
293 1   33     3 return my $context = $req->env->{'plack.graphql.context'} ||= $self->build_context($req);
294             }
295              
296             sub build_context {
297 1     1 0 10 my ($self, $req, $data) = @_;
298 1         4 my $context_class = $self->context_class;
299             return $context_class->new(
300             request => $req,
301             data => $data,
302             app => $self,
303 1         5 log => $req->env->{'psgix.logger'},
304             );
305             }
306              
307             sub execute {
308 1     1 0 5 my ($self, $schema, $data, $root_value, $context, $resolver, $promise_code) = @_;
309             return my $results = GraphQL::Execution::execute(
310             $schema,
311             $data->{query},
312             $root_value,
313             $context,
314             $data->{variables},
315             $data->{operationName},
316 1         22 $resolver,
317             $promise_code,
318             );
319             }
320              
321             1;
322              
323             =head1 NAME
324            
325             Plack::App::GraphQL - Serve GraphQL from Plack / PSGI
326            
327             =head1 SYNOPSIS
328            
329             use Plack::App::GraphQL;
330              
331             my $schema = q|
332             type Query {
333             hello: String
334             }
335             |;
336              
337             my %root_value = (
338             hello => 'Hello World!',
339             );
340              
341             my $app = Plack::App::GraphQL
342             ->new(schema => $schema, root_value => \%root_value)
343             ->to_app;
344              
345             Or mount under a given URL:
346              
347             use Plack::Builder;
348             use Plack::App::GraphQL;
349              
350             # $schema and %root_value as above
351              
352             my $app = Plack::App::GraphQL
353             ->new(schema => $schema, root_value => \%root_value)
354             ->to_app;
355              
356             builder {
357             mount "/graphql" => $app;
358             };
359              
360             You can also use the 'endpoint' configuration option to set a root path to match.
361             This is the most simple option if you application is not serving other endpoints
362             or applications (See documentation below).
363              
364             =head1 DESCRIPTION
365            
366             Serve L<GraphQL> with L<Plack>.
367              
368             Please note this is an early access / minimal documentation release. You should already
369             be familiar with L<GraphQL>. There's some examples in C</examples> but few real test
370             cases. If you are not comfortable using this based on reading the source code and
371             can't accept the possibility that the underlying code might change (although I expect
372             the configuration options are pretty set now) then you shouldn't use this. I recommend
373             looking at official plugins for Dancer and Mojolicious: L<Dancer2::Plugin::GraphQL>,
374             L<Mojolicious::Plugin::GraphQL> instead (or you can send me patches :) ).
375              
376             This currently doesn't support an asychronous responses until updates are made in
377             core L<GraphQL>.
378              
379             =head1 CONFIGURATION
380            
381             This L<Plack> applications supports the following configuration arguments:
382              
383             =head2 schema
384              
385             The L<GraphQL::Schema>. Canonically this should be an instance of L<GraphQL::Schema>
386             but if you pass a string or a filehandle, we will assume that it is a parse-able
387             graphql SDL document that we can build a schema object from. Makes for easy demos.
388              
389             =head2 root_value
390              
391             An object, hashref or coderef that field resolvers can use to look up requests. Generally
392             the method or hash keys will match the query or mutation keys. See the examples for
393             more.
394              
395             =head2 resolver
396              
397             Used to change how field resolvers work. See L<GraphQL> (or ignore this since its likely
398             something you really don't need for normal work.
399              
400             =head2 convert
401              
402             This takes a sub class of L<GraphQL::Plugin::Convert>, such as L<GraphQL::Plugin::Convert::DBIC>.
403             Providing this will automatically provide L</schema>, L</root_value> and L</resolver>.
404              
405             You can shortcut the value of this with a '+' and we will assume the default namespace. For
406             example '+DBIC' is the same as 'GraphQL::Plugin::Convert::DBIC'.
407              
408             =head2 endpoint
409              
410             The URI path part that is associated with the graphql API endpoint. Often this is set to
411             'graphql'. The default is '/'. You might prefer to use a custom or alternative router
412             (for example L<Plack::Builder>).
413              
414             =head2 context_class
415              
416             Default is L<Plack::App::GraphQL::Context>. This is an object that is passed as the 'context'
417             argument to your field resolvers. You might wish to subclass this to add additional useful
418             methods such as simple access to a user object (if you you authentication for example).
419              
420             =head2 graphiql
421              
422             Boolean that defaults to FALSE. Turn this on to enable the HTML Interactive GraphQL query
423             screen. Useful for leaning and debugging but you probably want it off in production.
424              
425             B<NOTE> If you want to use this you should also install L<Template::Tiny> which is needed. We
426             don't make L<Template::Tiny> a dependency here so that you are not forced to install it where
427             you don't want the interactive screens (such as production).
428              
429             =head2 json_encoder
430              
431             Lets you specify the instance of the class used for JSON encoding / decoding. The default is an
432             instance of L<JSON::MaybeXS> so you will want to be sure install a fast JSON de/encoder in production,
433             such as L<Cpanel::JSON::XS> (it will default to a pure Perl one which might not need your speed
434             requirements).
435              
436             =head2 exceptions_class
437              
438             Class that provides the exception responses. Override the default (L<Plack::App::GraphQL::Exceptions>)
439             if you want complete control over how your errors look.
440              
441             =head1 METHODS
442              
443             TBD
444            
445             =head1 AUTHOR
446            
447             John Napiorkowski <jnapiork@cpan.org>
448              
449             =head1 SEE ALSO
450            
451             L<GraphQL>, L<Plack>
452              
453             =head1 COPYRIGHT
454              
455             Copyright (c) 2019 by "AUTHOR" as listed above.
456              
457             =head1 LICENSE
458              
459             This library is free software and may be distributed under the same terms as perl itself.
460            
461             =cut