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