File Coverage

blib/lib/GraphQL/Schema.pm
Criterion Covered Total %
statement 129 140 92.1
branch 36 54 66.6
condition 7 8 87.5
subroutine 31 32 96.8
pod 6 6 100.0
total 209 240 87.0


line stmt bran cond sub pod time code
1             package GraphQL::Schema;
2              
3 17     17   72936 use 5.014;
  5877         55538  
4 5877     17   23955 use strict;
  3342         12648  
  3347         21232  
5 1130     17   2614 use warnings;
  1126         2840  
  1168         3305  
6 1173     17   3900 use Moo;
  94         3894  
  620         12977  
7 1520     17   136801 use Types::Standard -all;
  429         908  
  426         1251  
8 1741     17   815001 use GraphQL::Type::Library -all;
  739         38367  
  779         23592  
9 2605     17   241135 use GraphQL::MaybeTypeCheck;
  2808         8903  
  2563         19825  
10 1125     17   29812 use GraphQL::Debug qw(_debug);
  1161         3852  
  70         1186  
11 68     17   8486 use GraphQL::Directive;
  68         920  
  163         1681  
12 63     17   33858 use GraphQL::Introspection qw($SCHEMA_META_TYPE);
  59         27905  
  17         2381  
13 17     17   9087 use GraphQL::Language::Parser qw(parse);
  17         58  
  17         1130  
14 17     17   123 use GraphQL::Plugin::Type;
  17         35  
  17         144  
15 17     17   89 use Module::Runtime qw(require_module);
  17         38  
  17         134  
16 17     17   1186 use Exporter 'import';
  17         42  
  17         1092  
17              
18             our $VERSION = '0.02';
19             our @EXPORT_OK = qw(lookup_type);
20 17     17   106 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  17         36  
  17         7619  
21             my @TYPE_ATTRS = qw(query mutation subscription);
22              
23             =head1 NAME
24              
25             GraphQL::Schema - GraphQL schema object
26              
27             =head1 SYNOPSIS
28              
29             use GraphQL::Schema;
30             use GraphQL::Type::Object;
31             my $schema = GraphQL::Schema->new(
32             query => GraphQL::Type::Object->new(
33             name => 'Query',
34             fields => {
35             getObject => {
36             type => $interfaceType,
37             resolve => sub {
38             return {};
39             }
40             }
41             }
42             )
43             );
44              
45             =head1 DESCRIPTION
46              
47             Class implementing GraphQL schema.
48              
49             =head1 ATTRIBUTES
50              
51             =head2 query
52              
53             =cut
54              
55             has query => (is => 'ro', isa => InstanceOf['GraphQL::Type::Object'], required => 1);
56              
57             =head2 mutation
58              
59             =cut
60              
61             has mutation => (is => 'ro', isa => InstanceOf['GraphQL::Type::Object']);
62              
63             =head2 subscription
64              
65             =cut
66              
67             has subscription => (is => 'ro', isa => InstanceOf['GraphQL::Type::Object']);
68              
69             =head2 types
70              
71             Defaults to the types returned by L<GraphQL::Plugin::Type/registered>.
72             Note that this includes the standard scalar types always loaded by
73             L<GraphQL::Type::Scalar>. If you
74             wish to supply an overriding value for this attribute, bear that in mind.
75              
76             =cut
77              
78             has types => (
79             is => 'ro',
80             isa => ArrayRef[ConsumerOf['GraphQL::Role::Named']],
81             default => sub { [ GraphQL::Plugin::Type->registered ] },
82             );
83              
84             =head2 directives
85              
86             =cut
87              
88             has directives => (
89             is => 'ro',
90             isa => ArrayRef[InstanceOf['GraphQL::Directive']],
91             default => sub { \@GraphQL::Directive::SPECIFIED_DIRECTIVES },
92             );
93              
94             =head1 METHODS
95              
96             =head2 name2type
97              
98             In this schema, returns a hash-ref mapping all types' names to their
99             type object.
100              
101             =cut
102              
103             has name2type => (is => 'lazy', isa => Map[StrNameValid, ConsumerOf['GraphQL::Role::Named']]);
104             sub _build_name2type {
105       75     my ($self) = @_;
106             my @types = grep $_, (map $self->$_, @TYPE_ATTRS), $SCHEMA_META_TYPE;
107             push @types, @{ $self->types || [] };
108             my %name2type;
109             map _expand_type(\%name2type, $_), @types;
110             \%name2type;
111             }
112              
113             =head2 get_possible_types($abstract_type)
114              
115             In this schema, get all of either the implementation types
116             (if interface) or possible types (if union) of the C<$abstract_type>.
117              
118             =cut
119              
120             fun _expand_type(
121             (Map[StrNameValid, ConsumerOf['GraphQL::Role::Named']]) $map,
122             (InstanceOf['GraphQL::Type']) $type,
123 5848 50   5848   12389 ) :ReturnType(ArrayRef[ConsumerOf['GraphQL::Role::Named']]) {
  5848 50       11448  
  5848 100       9197  
  5848 100       12366  
  17         25564  
  17         38  
124 17 100       129 @_ = ($map, $type->of), goto &_expand_type if $type->can('of'); # avoid blowing out the stack
125 12 100       38 my $name = $type->name if $type->can('name');
126 12 100 100     44 return [] if $name and $map->{$name} and $map->{$name} == $type; # seen
      100        
127 12 100       29 die "Duplicate type $name" if $map->{$name};
128 12         28 $map->{$name} = $type;
129 12         42 my @types;
130 17 100       24561 push @types, ($type, map @{ _expand_type($map, $_) }, @{ $type->interfaces || [] })
  17 100       44  
  17         101  
131             if $type->isa('GraphQL::Type::Object');
132 14 100       498 push @types, ($type, map @{ _expand_type($map, $_) }, @{ $type->get_types })
  14         39  
  14         27  
133             if $type->isa('GraphQL::Type::Union');
134 14 100       33 if (grep $type->DOES($_), qw(GraphQL::Role::FieldsInput GraphQL::Role::FieldsOutput)) {
135 14   50     52 my $fields = $type->fields||{};
136             push @types, map {
137 14 50       708 map @{ _expand_type($map, $_->{type}) }, $_, values %{ $_->{args}||{} }
  19         25675  
  19         91  
  19         124  
138             } values %$fields;
139             }
140 57         7524 DEBUG and _debug('_expand_type', \@types);
141 5905         1357957 \@types;
142 17     17   45802 }
  17         44  
  17         104  
143              
144             has _interface2types => (is => 'lazy', isa => Map[StrNameValid, ArrayRef[InstanceOf['GraphQL::Type::Object']]]);
145             sub _build__interface2types {
146       6     my ($self) = @_;
147             my $name2type = $self->name2type||{};
148             my %interface2types;
149             map {
150             my $o = $_;
151             map {
152             push @{$interface2types{$_->name}}, $o;
153             # TODO assert_object_implements_interface
154             } @{ $o->interfaces||[] };
155             } grep $_->isa('GraphQL::Type::Object'), values %$name2type;
156             \%interface2types;
157             }
158              
159             method get_possible_types(
160             (ConsumerOf['GraphQL::Role::Abstract']) $abstract_type
161 18 50   12 1 9623 ) :ReturnType(ArrayRef[InstanceOf['GraphQL::Type::Object']]) {
  18 50       69  
  18 50       166  
  56         261990  
  56         178  
  56         107  
162 56 50       218 return $abstract_type->get_types if $abstract_type->isa('GraphQL::Type::Union');
163 56 50       197 $self->_interface2types->{$abstract_type->name} || [];
164 57     17   252 }
  56         232  
  56         168  
165              
166             =head2 is_possible_type($abstract_type, $possible_type)
167              
168             In this schema, is the given C<$possible_type> either an implementation
169             (if interface) or a possibility (if union) of the C<$abstract_type>?
170              
171             =cut
172              
173             has _possible_type_map => (is => 'rw', isa => Map[StrNameValid, Map[StrNameValid, Bool]]);
174             method is_possible_type(
175             (ConsumerOf['GraphQL::Role::Abstract']) $abstract_type,
176             (InstanceOf['GraphQL::Type::Object']) $possible_type,
177 17 50   14 1 42531 ) :ReturnType(Bool) {
  17 100       49  
  17 0       104  
  348 0       6986  
  348         753  
  348         657  
  348         882  
178 348         2862 my $map = $self->_possible_type_map || {};
179             return $map->{$abstract_type->name}{$possible_type->name}
180 75 0       1515 if $map->{$abstract_type->name}; # we know about the abstract_type
181 75 50       705 my @possibles = @{ $self->get_possible_types($abstract_type)||[] };
  75         180  
182 75 100       374 die <<EOF if !@possibles;
183 75         134 Could not find possible implementing types for @{[$abstract_type->name]}
184             in schema. Check that schema.types is defined and is an array of
185             all possible types in the schema.
186             EOF
187 75         323 $map->{$abstract_type->name} = { map { ($_->name => 1) } @possibles };
  71         1778  
188 6         83 $self->_possible_type_map($map);
189 6         120 $map->{$abstract_type->name}{$possible_type->name};
190 56     17   835 }
  56         525  
  56         203  
191              
192             =head2 assert_object_implements_interface($type, $iface)
193              
194             In this schema, does the given C<$type> implement interface C<$iface>? If
195             not, throw exception.
196              
197             =cut
198              
199             method assert_object_implements_interface(
200             (ConsumerOf['GraphQL::Role::Abstract']) $abstract_type,
201             (InstanceOf['GraphQL::Type::Object']) $possible_type,
202       0 1   ) {
203             my @types = @{ $self->types };
204             return;
205             }
206              
207             =head2 from_ast($ast[, \%kind2class])
208              
209             Class method. Takes AST (array-ref of hash-refs) made by
210             L<GraphQL::Language::Parser/parse> and returns a schema object. Will
211             not be a complete schema since it will have only default resolvers.
212              
213             If C<\%kind2class> is given, it will override the default
214             mapping of SDL keywords to Perl classes. This is probably most
215             useful for L<GraphQL::Type::Object>. The default is available as
216             C<%GraphQL::Schema::KIND2CLASS>. E.g.
217              
218             my $schema = GraphQL::Schema->from_ast(
219             $doc,
220             { %GraphQL::Schema::KIND2CLASS, type => 'GraphQL::Type::Object::DBIC' }
221             );
222              
223             Makes available the additional types returned by
224             L<GraphQL::Plugin::Type/registered>.
225              
226             =cut
227              
228             our %KIND2CLASS = qw(
229             type GraphQL::Type::Object
230             enum GraphQL::Type::Enum
231             interface GraphQL::Type::Interface
232             union GraphQL::Type::Union
233             scalar GraphQL::Type::Scalar
234             input GraphQL::Type::InputObject
235             );
236             my %CLASS2KIND = reverse %KIND2CLASS;
237             method from_ast(
238             ArrayRef[HashRef] $ast,
239             HashRef $kind2class = \%KIND2CLASS,
240 49 50   55 1 66 ) :ReturnType(InstanceOf[__PACKAGE__]) {
  7 50       198  
  7         37  
  49         854  
  6         275  
  0         0  
  0         0  
241 0         0 DEBUG and _debug('Schema.from_ast', $ast);
242 0         0 my @type_nodes = grep $kind2class->{$_->{kind}}, @$ast;
243 0         0 my ($schema_node, $e) = grep $_->{kind} eq 'schema', @$ast;
244 0         0 die "Must provide only one schema definition.\n" if $e;
245 0         0 my %name2type = (map { $_->name => $_ } GraphQL::Plugin::Type->registered);
  0         0  
246 0         0 for (@type_nodes) {
247             die "Type '$_->{name}' was defined more than once.\n"
248 0         0 if $name2type{$_->{name}};
249 26         665 require_module $kind2class->{$_->{kind}};
250 26         44 $name2type{$_->{name}} = $kind2class->{$_->{kind}}->from_ast(\%name2type, $_);
251             }
252 26         269 if (!$schema_node) {
253             # infer one
254             $schema_node = +{
255 19         98 map { $name2type{ucfirst $_} ? ($_ => ucfirst $_) : () } @TYPE_ATTRS
  21         182  
256             };
257             }
258             die "Must provide schema definition with query type or a type named Query.\n"
259 26         129 unless $schema_node->{query};
260             my @directives = map GraphQL::Directive->from_ast(\%name2type, $_),
261 132         347 grep $_->{kind} eq 'directive', @$ast;
262             my $schema = $self->new(
263             (map {
264 26         78 $schema_node->{$_}
265 0         0 ? ($_ => $name2type{$schema_node->{$_}}
266             // die "Specified $_ type '$schema_node->{$_}' not found.\n")
267             : ()
268             } @TYPE_ATTRS),
269             (@directives ? (directives => [ @GraphQL::Directive::SPECIFIED_DIRECTIVES, @directives ]) : ()),
270             types => [ values %name2type ],
271             );
272 26         287 $schema->name2type; # walks all types, fields, args - finds undefined types
273 26         583 $schema;
274 6     17   2637 }
  6         103  
  49         1277  
275              
276             =head2 from_doc($doc[, \%kind2class])
277              
278             Class method. Takes text that is a Schema Definition Language (SDL) (aka
279             Interface Definition Language) document and returns a schema object. Will
280             not be a complete schema since it will have only default resolvers.
281              
282             As of v0.32, this accepts both old-style "meaningful comments" and
283             new-style string values, as field or type descriptions.
284              
285             If C<\%kind2class> is given, it will override the default
286             mapping of SDL keywords to Perl classes. This is probably most
287             useful for L<GraphQL::Type::Object>. The default is available as
288             C<%GraphQL::Schema::KIND2CLASS>.
289              
290             =cut
291              
292             method from_doc(
293             Str $doc,
294             HashRef $kind2class = \%KIND2CLASS,
295 2     56 1 3 ) :ReturnType(InstanceOf[__PACKAGE__]) {
  2         3  
  10         67  
  2         11  
296             $self->from_ast(parse($doc), $kind2class);
297 2     17   85 }
  2         8  
  2         5  
298              
299             =head2 to_doc($doc)
300              
301             Returns Schema Definition Language (SDL) document that describes this
302             schema object.
303              
304             As of v0.32, this produces the new-style descriptions that are string
305             values, rather than old-style "meaningful comments".
306              
307             As of v0.33, will not return a description of types supplied with the
308             attribute L</types>. Obviously, by default this includes types returned
309             by L<GraphQL::Plugin::Type/registered>.
310              
311             =cut
312              
313             has to_doc => (is => 'lazy', isa => Str);
314             my %directive2builtin = map { ($_=>1) } @GraphQL::Directive::SPECIFIED_DIRECTIVES;
315             sub _build_to_doc {
316       26     my ($self) = @_;
317             my $schema_doc;
318             if (grep $self->$_->name ne ucfirst $_, grep $self->$_, @TYPE_ATTRS) {
319             $schema_doc = join('', map "$_\n", "schema {",
320             (map " $_: @{[$self->$_->name]}", grep $self->$_, @TYPE_ATTRS),
321             "}");
322             }
323             my %supplied_type = (map {$_->name => 1} GraphQL::Plugin::Type->registered);
324             join "\n", grep defined,
325             $schema_doc,
326             (map $_->to_doc,
327             sort { $a->name cmp $b->name }
328             grep !$directive2builtin{$_},
329             @{ $self->directives }),
330             (map $self->name2type->{$_}->to_doc,
331             grep !/^__/,
332             grep $CLASS2KIND{ref $self->name2type->{$_}},
333             grep !$supplied_type{$_},
334             sort keys %{$self->name2type}),
335             ;
336             }
337              
338             =head2 name2directive
339              
340             In this schema, returns a hash-ref mapping all directives' names to their
341             directive object.
342              
343             =cut
344              
345             has name2directive => (is => 'lazy', isa => Map[StrNameValid, InstanceOf['GraphQL::Directive']]);
346       2     method _build_name2directive() {
347             +{ map { ($_->name => $_) } @{ $self->directives } };
348             }
349              
350             =head1 FUNCTIONS
351              
352             =head2 lookup_type($typedef, $name2type)
353              
354             Turns given AST fragment into a type.
355              
356             If the hash-ref's C<type> member is a string, will return a type of that name.
357              
358             If an array-ref, first element must be either C<list> or C<non_null>,
359             second will be a recursive AST fragment, which will be passed into a
360             recursive call. The result will then have the modifier method (C<list>
361             or C<non_null>) called, and that will be returned.
362              
363             =cut
364              
365             fun lookup_type(
366             HashRef $typedef,
367             (Map[StrNameValid, InstanceOf['GraphQL::Type']]) $name2type,
368       348 1   ) :ReturnType(InstanceOf['GraphQL::Type']) {
369             my $type = $typedef->{type};
370             die "Undefined type given\n" if !defined $type;
371             return $name2type->{$type} // die "Unknown type '$type'.\n"
372             if is_Str($type);
373             my ($wrapper_type, $wrapped) = @$type;
374             lookup_type($wrapped, $name2type)->$wrapper_type;
375       17     }
376              
377             __PACKAGE__->meta->make_immutable();
378              
379             1;