├── t
└── graphql
│ ├── validator
│ ├── context.t
│ └── rules
│ │ ├── unique_variable_names.t
│ │ ├── variables_are_input_types.t
│ │ ├── known_fragment_names.t
│ │ ├── known_type_names.t
│ │ ├── unique_input_field_names.t
│ │ ├── lone_anonymous_operation.t
│ │ ├── unique_fragment_names.t
│ │ ├── unique_operation_names.t
│ │ ├── fragment_on_composite_types.t
│ │ ├── unique_directives_per_location.t
│ │ ├── scalar_leafs.t
│ │ ├── unique_argument_names.t
│ │ ├── no_unused_fragments.t
│ │ ├── known_argument_names.t
│ │ ├── default_values_of_correct_type.t
│ │ ├── no_unused_variables.t
│ │ ├── known_directives.t
│ │ ├── possible_nonnull_arguments.t
│ │ └── possible_fragment_spreads.t
│ ├── language
│ ├── kitchen-sink.graphql
│ └── printer.t
│ ├── type
│ └── schema.t
│ ├── validator.t
│ ├── execute-resolve.t
│ ├── execute-mutations.t
│ ├── execute-nonnull.t
│ ├── execute-directives.t
│ ├── execute-lists.t
│ └── execute-schema.t
├── lib
└── GraphQL
│ ├── Nullish.pm
│ ├── Language
│ ├── Source.pm
│ ├── Location.pm
│ ├── Token.pm
│ └── Kinds.pm
│ ├── Type
│ ├── List.pm
│ ├── NonNull.pm
│ ├── Interface.pm
│ ├── Union.pm
│ ├── Scalar.pm
│ ├── Object.pm
│ ├── InputObject.pm
│ ├── Enum.pm
│ └── Directive.pm
│ ├── Validator
│ ├── Rule
│ │ ├── KnownFragmentNames.pm
│ │ ├── UniqueFragmentNames.pm
│ │ ├── UniqueVariableNames.pm
│ │ ├── LoneAnonymousOperation.pm
│ │ ├── VariablesAreInputTypes.pm
│ │ ├── UniqueArgumentNames.pm
│ │ ├── UniqueOperationNames.pm
│ │ ├── UniqueInputFieldNames.pm
│ │ ├── ArgumentsOfCorrectType.pm
│ │ ├── UniqueDirectivesPerLocation.pm
│ │ ├── KnownTypeNames.pm
│ │ ├── NoUnusedFragments.pm
│ │ ├── NoUndefinedVariables.pm
│ │ ├── ScalarLeafs.pm
│ │ ├── NoUnusedVariables.pm
│ │ ├── FragmentsOnCompositeTypes.pm
│ │ ├── DefaultValuesOfCorrectType.pm
│ │ ├── PossibleFragmentSpreads.pm
│ │ ├── NoFragmentCycles.pm
│ │ ├── VariablesInAllowedPosition.pm
│ │ ├── ProvidedNonNullArguments.pm
│ │ ├── KnownArgumentNames.pm
│ │ ├── KnownDirectives.pm
│ │ └── FieldsOnCorrectType.pm
│ └── Context.pm
│ ├── Util
│ └── TypeComparators.pm
│ ├── Error.pm
│ └── Validator.pm
├── .gitignore
├── cpanfile
├── dist.ini
├── Build.PL
└── README.md
/t/graphql/validator/context.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More skip_all => 'not implemented yet';
6 |
7 | done_testing;
8 |
--------------------------------------------------------------------------------
/lib/GraphQL/Nullish.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Nullish;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use constant {
7 | NULLISH => {},
8 | };
9 |
10 | use Exporter qw/import/;
11 |
12 | our @EXPORT_OK = (qw/
13 | NULLISH
14 | /);
15 |
16 |
17 |
18 | 1;
19 |
20 | __END__
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /blib/
2 | /.build/
3 | _build/
4 | cover_db/
5 | inc/
6 | Build
7 | !Build/
8 | Build.bat
9 | .last_cover_stats
10 | /Makefile
11 | /Makefile.old
12 | /MANIFEST.bak
13 | /META.yml
14 | /META.json
15 | /MYMETA.*
16 | nytprof.out
17 | /pm_to_blib
18 | *.o
19 | *.bs
20 | /_eumm/
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/cpanfile:
--------------------------------------------------------------------------------
1 | # vim: ft=perl
2 | requires 'perl' => '5.010000';
3 |
4 | requires 'Data::Dumper' => '2.131';
5 | requires 'JSON' => '2.90';
6 | requires 'List::Util' => '1.26';
7 |
8 | on test => sub {
9 | requires 'Test::Deep' => '1.127';
10 | requires 'Test::More' => '1.302085';
11 | };
12 |
13 | on develop => sub {
14 | requires 'Data::Printer' => '0.39';
15 | };
16 |
--------------------------------------------------------------------------------
/lib/GraphQL/Language/Source.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Language::Source;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | sub new {
7 | my ($class, %args) = @_;
8 | bless {
9 | body => $args{body},
10 | name => $args{name} || 'GraphQL',
11 | }, $class;
12 | }
13 |
14 | sub body { shift->{body} }
15 | sub name { shift->{name} }
16 |
17 | 1;
18 |
19 | __END__
20 |
--------------------------------------------------------------------------------
/lib/GraphQL/Language/Location.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Language::Location;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use Exporter qw/import/;
7 |
8 | our @EXPORT_OK = (qw/get_location/);
9 |
10 | sub get_location {
11 | my ($source, $position) = @_;
12 |
13 | my $body = $source->body;
14 | my $line = 1;
15 | my $column = $position + 1;
16 |
17 | while (($body =~ /(\r\n|[\n\r])/g) && $-[0] < $position) {
18 | $line++;
19 | $column = $position + 1 - ($-[0] + length($1));
20 | }
21 |
22 | return { line => $line, column => $column };
23 | }
24 |
25 | 1;
26 |
27 | __END__
28 |
--------------------------------------------------------------------------------
/dist.ini:
--------------------------------------------------------------------------------
1 | [NameFromDirectory]
2 |
3 | [Git::GatherDir]
4 | exclude_filename = Build.PL
5 | exclude_filename = META.json
6 | exclude_filename = LICENSE
7 | exclude_filename = README.md
8 |
9 | [CopyFilesFromBuild]
10 | copy = META.json
11 | copy = LICENSE
12 | copy = Build.PL
13 |
14 | [VersionFromModule]
15 | [LicenseFromModule]
16 | override_author = 1
17 |
18 | [Git::Check]
19 | allow_dirty = dist.ini
20 | allow_dirty = Changes
21 | allow_dirty = META.json
22 | allow_dirty = README.md
23 | allow_dirty = Build.PL
24 |
25 | ; Make Github center and front
26 | [GithubMeta]
27 | issues = 1
28 | [ReadmeAnyFromPod]
29 | type = markdown
30 | filename = README.md
31 | location = root
32 |
33 | [MetaNoIndex]
34 | directory = t
35 | directory = examples
36 |
37 | [Prereqs::FromCPANfile]
38 | [ModuleBuild]
39 | [MetaJSON]
40 |
41 | [PodSyntaxTests]
42 | [License]
43 | [ExtraTests]
44 | [ExecDir]
45 | dir = script
46 | [Manifest]
47 | [ManifestSkip]
48 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/List.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::List;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Util::Type qw/is_type/;
7 |
8 | sub of_type { shift->{of_type} }
9 |
10 | sub new {
11 | my ($class, $type) = @_;
12 |
13 | die "Can only create List of a GraphQLType but got: $type"
14 | unless is_type($type);
15 |
16 | my $self = bless { of_type => $type }, $class;
17 | return $self;
18 | }
19 |
20 | sub to_string {
21 | my $self = shift;
22 | return '[' . $self->of_type->to_string . ']';
23 | }
24 |
25 | sub to_json { shift->to_string }
26 | sub inspect { shift->to_string }
27 |
28 | 1;
29 |
30 | __END__
31 |
32 | List Modifier
33 |
34 | A list is a kind of type marker, a wrapping type which points to another
35 | type. Lists are often created within the context of defining the fields of
36 | an object type.
37 |
38 | Example:
39 |
40 | const PersonType = new GraphQLObjectType({
41 | name: 'Person',
42 | fields: () => ({
43 | parents: { type: new GraphQLList(Person) },
44 | children: { type: new GraphQLList(Person) },
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/KnownFragmentNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::KnownFragmentNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 |
8 | sub unknown_fragment_message {
9 | my $frag_name = shift;
10 | return qq`Unknown fragment "$frag_name".`;
11 | }
12 |
13 | # Known fragment names
14 | #
15 | # A GraphQL document is only valid if all `...Fragment` fragment spreads refer
16 | # to fragments defined in the same document.
17 | sub validate {
18 | my ($self, $context) = @_;
19 | return {
20 | FragmentSpread => sub {
21 | my (undef, $node) = @_;
22 |
23 | my $frag_name = $node->{name}{value};
24 | my $frag = $context->get_fragment($frag_name);
25 |
26 | unless ($frag) {
27 | $context->report_error(
28 | GraphQLError(
29 | unknown_fragment_message($frag_name),
30 | [$node->{name}]
31 | )
32 | );
33 | }
34 |
35 | return; # void
36 | }
37 | };
38 | }
39 |
40 |
41 | 1;
42 |
43 | __END__
44 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/unique_variable_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub duplicate_variable {
15 | my ($name, $l1, $c1, $l2, $c2) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::UniqueVariableNames::duplicate_variable_message($name),
18 | locations => [{ line => $l1, column => $c1 }, { line => $l2, column => $c2 }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'unique variable names' => sub {
24 | expect_passes_rule('UniqueVariableNames', '
25 | query A($x: Int, $y: String) { __typename }
26 | query B($x: String, $y: Int) { __typename }
27 | ');
28 | };
29 |
30 | subtest 'duplicate variable names' => sub {
31 | expect_fails_rule('UniqueVariableNames', '
32 | query A($x: Int, $x: Int, $x: String) { __typename }
33 | query B($x: String, $x: Int) { __typename }
34 | query C($x: Int, $x: Int) { __typename }
35 | ', [
36 | duplicate_variable('x', 2, 16, 2, 25),
37 | duplicate_variable('x', 2, 16, 2, 34),
38 | duplicate_variable('x', 3, 16, 3, 28),
39 | duplicate_variable('x', 4, 16, 4, 25)
40 | ]);
41 | };
42 |
43 | done_testing;
44 |
--------------------------------------------------------------------------------
/t/graphql/language/kitchen-sink.graphql:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2015, Facebook, Inc.
2 | # All rights reserved.
3 | #
4 | # This source code is licensed under the BSD-style license found in the
5 | # LICENSE file in the root directory of this source tree. An additional grant
6 | # of patent rights can be found in the PATENTS file in the same directory.
7 |
8 | query queryName($foo: ComplexType, $site: Site = MOBILE) {
9 | whoever123is: node(id: [123, 456]) {
10 | id ,
11 | ... on User @defer {
12 | field2 {
13 | id ,
14 | alias: field1(first:10, after:$foo,) @include(if: $foo) {
15 | id,
16 | ...frag
17 | }
18 | }
19 | }
20 | ... @skip(unless: $foo) {
21 | id
22 | }
23 | ... {
24 | id
25 | }
26 | }
27 | }
28 |
29 | mutation likeStory {
30 | like(story: 123) @defer {
31 | story {
32 | id
33 | }
34 | }
35 | }
36 |
37 | subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
38 | storyLikeSubscribe(input: $input) {
39 | story {
40 | likers {
41 | count
42 | }
43 | likeSentence {
44 | text
45 | }
46 | }
47 | }
48 | }
49 |
50 | fragment frag on Friend {
51 | foo(size: $size, bar: $b, obj: {key: "value"})
52 | }
53 |
54 | {
55 | unnamed(truthy: true, falsey: false, nullish: null),
56 | query
57 | }
58 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/variables_are_input_types.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | subtest 'input types are valid' => sub {
15 | expect_passes_rule('VariablesAreInputTypes', '
16 | query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) {
17 | field(a: $a, b: $b, c: $c)
18 | }
19 | ');
20 | };
21 |
22 | subtest 'output types are invalid' => sub {
23 | expect_fails_rule('VariablesAreInputTypes', '
24 | query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) {
25 | field(a: $a, b: $b, c: $c)
26 | }
27 | ', [
28 | { locations => [ { line => 2, column => 21 } ],
29 | message => GraphQL::Validator::Rule::VariablesAreInputTypes::non_input_type_on_var_message('a', 'Dog'),
30 | path => undef },
31 | { locations => [ { line => 2, column => 30 } ],
32 | message => GraphQL::Validator::Rule::VariablesAreInputTypes::non_input_type_on_var_message('b', '[[CatOrDog!]]!'),
33 | path => undef },
34 | { locations => [ { line => 2, column => 50 } ],
35 | message => GraphQL::Validator::Rule::VariablesAreInputTypes::non_input_type_on_var_message('c', 'Pet'),
36 | path => undef },
37 | ]);
38 | };
39 |
40 | done_testing;
41 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/UniqueFragmentNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::UniqueFragmentNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Visitor qw/FALSE/;
8 |
9 | sub duplicate_fragment_name_message {
10 | my $frag_name = shift;
11 | return qq`There can be only one fragment named "$frag_name".`;
12 | }
13 |
14 | # Unique fragment names
15 | #
16 | # A GraphQL document is only valid if all defined fragments have unique names.
17 | sub validate {
18 | my ($self, $context) = @_;
19 | my %known_fragment_names;
20 |
21 | return {
22 | OperationDefinition => sub { FALSE },
23 | FragmentDefinition => sub {
24 | my (undef, $node) = @_;
25 | my $fragment_name = $node->{name}{value};
26 |
27 | if ($known_fragment_names{ $fragment_name }) {
28 | $context->report_error(
29 | GraphQLError(
30 | duplicate_fragment_name_message($fragment_name),
31 | [$known_fragment_names{ $fragment_name }, $node->{name}]
32 | )
33 | );
34 | }
35 | else {
36 | $known_fragment_names{ $fragment_name } = $node->{name};
37 | }
38 |
39 | return FALSE;
40 | },
41 | };
42 | }
43 |
44 | 1;
45 |
46 | __END__
47 |
--------------------------------------------------------------------------------
/Build.PL:
--------------------------------------------------------------------------------
1 |
2 | # This file was automatically generated by Dist::Zilla::Plugin::ModuleBuild v6.009.
3 | use strict;
4 | use warnings;
5 |
6 | use Module::Build 0.28;
7 |
8 |
9 | my %module_build_args = (
10 | "build_requires" => {
11 | "Module::Build" => "0.28"
12 | },
13 | "configure_requires" => {
14 | "Module::Build" => "0.28"
15 | },
16 | "dist_abstract" => "A Perl port of the reference implementation of L.",
17 | "dist_author" => [
18 | "Artur Khabibullin - rtkh\@cpan.org"
19 | ],
20 | "dist_name" => "GraphQL",
21 | "dist_version" => "0.01",
22 | "license" => "perl",
23 | "module_name" => "GraphQL",
24 | "recursive_test_files" => 1,
25 | "requires" => {
26 | "Data::Dumper" => "2.131",
27 | "JSON" => "2.90",
28 | "List::Util" => "1.26",
29 | "perl" => "5.010000"
30 | },
31 | "test_requires" => {
32 | "Test::Deep" => "1.127",
33 | "Test::More" => "1.302085"
34 | }
35 | );
36 |
37 |
38 | my %fallback_build_requires = (
39 | "Module::Build" => "0.28",
40 | "Test::Deep" => "1.127",
41 | "Test::More" => "1.302085"
42 | );
43 |
44 |
45 | unless ( eval { Module::Build->VERSION(0.4004) } ) {
46 | delete $module_build_args{test_requires};
47 | $module_build_args{build_requires} = \%fallback_build_requires;
48 | }
49 |
50 | my $build = Module::Build->new(%module_build_args);
51 |
52 |
53 | $build->create_build_script;
54 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/NonNull.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::NonNull;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Util::Type qw/is_type/;
7 |
8 | sub of_type { shift->{of_type} }
9 |
10 | sub new {
11 | my ($class, $type) = @_;
12 |
13 | die "Can only create NonNull of a Nullable GraphQLType but got: ${ \$type->to_string }.\n"
14 | if !is_type($type) || $type->isa('GraphQL::Type::NonNull');
15 |
16 | my $self = bless { of_type => $type }, $class;
17 | return $self;
18 | }
19 |
20 | sub to_string {
21 | my $self = shift;
22 | return $self->of_type->to_string . '!';
23 | }
24 |
25 | sub to_json { shift->to_string }
26 | sub inspect { shift->to_string }
27 |
28 | 1;
29 |
30 | __END__
31 | * Non-Null Modifier
32 | *
33 | * A non-null is a kind of type marker, a wrapping type which points to another
34 | * type. Non-null types enforce that their values are never null and can ensure
35 | * an error is raised if this ever occurs during a request. It is useful for
36 | * fields which you can make a strong guarantee on non-nullability, for example
37 | * usually the id field of a database row will never be null.
38 | *
39 | * Example:
40 | *
41 | * const RowType = new GraphQLObjectType({
42 | * name: 'Row',
43 | * fields: () => ({
44 | * id: { type: new GraphQLNonNull(GraphQLString) },
45 | * })
46 | * })
47 | *
48 | * Note: the enforcement of non-nullability occurs within the executor.
49 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/UniqueVariableNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::UniqueVariableNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 |
8 | sub duplicate_variable_message {
9 | my $variable_name = shift;
10 | return qq`There can be only one variable named "$variable_name".`;
11 | }
12 |
13 | # Unique variable names
14 | #
15 | # A GraphQL operation is only valid if all its variables are uniquely named.
16 | sub validate {
17 | my ($self, $context) = @_;
18 | my %known_variable_names;
19 |
20 | return {
21 | OperationDefinition => sub {
22 | %known_variable_names = ();
23 | return; # void
24 | },
25 | VariableDefinition => sub {
26 | my (undef, $node) = @_;
27 | my $variable_name = $node->{variable}{name}{value};
28 |
29 | if ($known_variable_names{ $variable_name }) {
30 | $context->report_error(
31 | GraphQLError(
32 | duplicate_variable_message($variable_name),
33 | [$known_variable_names{ $variable_name }, $node->{variable}{name}]
34 | )
35 | );
36 | }
37 | else {
38 | $known_variable_names{ $variable_name } = $node->{variable}{name};
39 | }
40 |
41 | return; # void
42 | },
43 | };
44 | }
45 |
46 | 1;
47 |
48 | __END__
49 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/LoneAnonymousOperation.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::LoneAnonymousOperation;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Kinds qw/Kind/;
8 | use GraphQL::Language::Parser;
9 |
10 | sub anon_operation_not_alone_message {
11 | return 'This anonymous operation must be the only defined operation.';
12 | }
13 |
14 | # Lone anonymous operation
15 | #
16 | # A GraphQL document is only valid if when it contains an anonymous operation
17 | # (the query short-hand) that it contains only that one operation definition.
18 | sub validate {
19 | my ($self, $context) = @_;
20 | my $operation_count = 0;
21 |
22 | return {
23 | Document => sub {
24 | my (undef, $node) = @_;
25 | $operation_count =
26 | scalar grep { $_->{kind} eq Kind->OPERATION_DEFINITION }
27 | @{ $node->{definitions} };
28 |
29 | return; # void
30 | },
31 | OperationDefinition => sub {
32 | my (undef, $node) = @_;
33 |
34 | if (!$node->{name} && $operation_count > 1) {
35 | $context->report_error(
36 | GraphQLError(
37 | anon_operation_not_alone_message,
38 | [$node]
39 | )
40 | );
41 | }
42 |
43 | return; # void
44 | },
45 | };
46 | }
47 |
48 | 1;
49 |
50 | __END__
51 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/VariablesAreInputTypes.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::VariablesAreInputTypes;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Printer qw/print_doc/;
8 | use GraphQL::Util qw/type_from_ast/;
9 | use GraphQL::Util::Type qw/is_input_type/;
10 |
11 | sub non_input_type_on_var_message {
12 | my ($variable_name, $type_name) = @_;
13 | return qq`Variable "\$$variable_name" cannot be non-input type "$type_name".`;
14 | }
15 |
16 | # Variables are input types
17 | #
18 | # A GraphQL operation is only valid if all the variables it defines are of
19 | # input types (scalar, enum, or input object).
20 | sub validate {
21 | my ($self, $context) = @_;
22 | return {
23 | VariableDefinition => sub {
24 | my (undef, $node) = @_;
25 | my $type = type_from_ast($context->get_schema, $node->{type});
26 |
27 | # If the variable type is not an input type, return an error.
28 | if ($type && !is_input_type($type)) {
29 | my $variable_name = $node->{variable}{name}{value};
30 | $context->report_error(
31 | GraphQLError(
32 | non_input_type_on_var_message($variable_name, print_doc($node->{type})),
33 | [$node->{type}]
34 | )
35 | );
36 | }
37 |
38 | return; # void
39 | }
40 | };
41 | }
42 |
43 | 1;
44 |
45 | __END__
46 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/UniqueArgumentNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::UniqueArgumentNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Visitor qw/FALSE/;
8 |
9 | sub duplicate_arg_message {
10 | my $arg_name = shift;
11 | return qq`There can be only one argument named "$arg_name".`;
12 | }
13 |
14 | # Unique argument names
15 | #
16 | # A GraphQL field or directive is only valid if all supplied arguments are
17 | # uniquely named.
18 | sub validate {
19 | my ($self, $context) = @_;
20 | my %known_arg_names;
21 | return {
22 | Field => sub {
23 | %known_arg_names = ();
24 | return; # void
25 | },
26 | Directive => sub {
27 | %known_arg_names = ();
28 | return; # void
29 | },
30 | Argument => sub {
31 | my (undef, $node) = @_;
32 | my $arg_name = $node->{name}{value};
33 |
34 | if ($known_arg_names{ $arg_name }) {
35 | $context->report_error(
36 | GraphQLError(
37 | duplicate_arg_message($arg_name),
38 | [$known_arg_names{ $arg_name }, $node->{name}]
39 | )
40 | );
41 | }
42 | else {
43 | $known_arg_names{ $arg_name } = $node->{name};
44 | }
45 |
46 | return FALSE;
47 | },
48 | };
49 | }
50 |
51 | 1;
52 |
53 | __END__
54 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/UniqueOperationNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::UniqueOperationNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Visitor qw/FALSE/;
8 |
9 | sub duplicate_operation_name_message {
10 | my $operation_name = shift;
11 | return qq`There can be only one operation named "${operation_name}".`;
12 | }
13 |
14 | # Unique operation names
15 | #
16 | # A GraphQL document is only valid if all defined operations have unique names.
17 | sub validate {
18 | my ($self, $context) = @_;
19 | my %known_operation_names;
20 |
21 | return {
22 | OperationDefinition => sub {
23 | my (undef, $node) = @_;
24 | my $operation_name = $node->{name};
25 |
26 | if ($operation_name) {
27 | if ($known_operation_names{ $operation_name->{value} }) {
28 | $context->report_error(
29 | GraphQLError(
30 | duplicate_operation_name_message($operation_name->{value}),
31 | [$known_operation_names{ $operation_name->{value} }, $operation_name]
32 | )
33 | );
34 | }
35 | else {
36 | $known_operation_names{ $operation_name->{value} } = $operation_name;
37 | }
38 | }
39 |
40 | return FALSE;
41 | },
42 | FragmentDefinition => sub { FALSE },
43 | };
44 | }
45 |
46 | 1;
47 |
48 | __END__
49 |
--------------------------------------------------------------------------------
/t/graphql/type/schema.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use GraphQL qw/:types/;
8 |
9 | my $ImplementingType;
10 |
11 | my $InterfaceType = GraphQLInterfaceType(
12 | name => 'Interface',
13 | fields => { fieldName => { type => GraphQLString } },
14 | resolve_type => sub {
15 | return $ImplementingType;
16 | },
17 | );
18 |
19 | $ImplementingType = GraphQLObjectType(
20 | name => 'Object',
21 | interfaces => [ $InterfaceType ],
22 | fields => { fieldName => { type => GraphQLString, resolve => sub { '' } } },
23 | );
24 |
25 | my $Schema = GraphQLSchema(
26 | query => GraphQLObjectType(
27 | name => 'Query',
28 | fields => {
29 | getObject => {
30 | type => $InterfaceType,
31 | resolve => sub {
32 | return {};
33 | }
34 | }
35 | }
36 | ),
37 | );
38 |
39 | subtest 'Type System: Schema' => sub {
40 | subtest 'Getting possible types' => sub {
41 | subtest 'throws human-reable error if schema.types is not defined' => sub {
42 | eval {
43 | $Schema->is_possible_type($InterfaceType, $ImplementingType);
44 | };
45 | my $e = $@;
46 | is $e, "Could not find possible implementing types for Interface in schema. "
47 | . "Check that schema.types is defined and is an array of all possible "
48 | . "types in the schema.\n";
49 | };
50 | };
51 | };
52 |
53 | done_testing;
54 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/Interface.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::Interface;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Util qw/assert_valid_name/;
7 | use GraphQL::Util::Type qw/resolve_thunk define_field_map/;
8 |
9 | sub name { shift->{name} }
10 | sub resolve_type { shift->{resolve_type} }
11 |
12 | sub new {
13 | my ($class, %config) = @_;
14 |
15 | assert_valid_name($config{name});
16 |
17 | if ($config{resolve_type}) {
18 | die qq`$config{name} must provide "resolve_type" as a function.`
19 | if ref($config{resolve_type}) ne 'CODE';
20 | }
21 |
22 | my $self = bless {
23 | name => $config{name},
24 | description => $config{description} || '',
25 | resolve_type => $config{resolve_type},
26 |
27 | _type_config => \%config,
28 | _fields => undef,
29 | }, $class;
30 |
31 | return $self;
32 | }
33 |
34 | sub get_fields {
35 | my $self = shift;
36 | return $self->{_fields}
37 | || ($self->{_fields} = define_field_map($self, $self->{_type_config}{fields}));
38 | }
39 |
40 | sub to_string { shift->name }
41 |
42 | sub to_json { shift->to_string }
43 | sub inspect { shift->to_string }
44 |
45 | 1;
46 |
47 | __END__
48 |
49 | Interface Type Definition
50 |
51 | When a field can return one of a heterogeneous set of types, a Interface type
52 | is used to describe what types are possible, what fields are in common across
53 | all types, as well as a function to determine which type is actually used
54 | when the field is resolved.
55 |
56 | Example:
57 |
58 | const EntityType = new GraphQLInterfaceType({
59 | name: 'Entity',
60 | fields: {
61 | name: { type: GraphQLString }
62 | }
63 | });
64 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/UniqueInputFieldNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::UniqueInputFieldNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Language::Visitor qw/FALSE/;
7 | use GraphQL::Error qw/GraphQLError/;
8 |
9 | sub duplicate_input_field_message {
10 | my $field_name = shift;
11 | return qq`There can be only one input field named "$field_name".`;
12 | }
13 |
14 | # Unique input field names
15 | #
16 | # A GraphQL input object value is only valid if all supplied fields are
17 | # uniquely named.
18 | sub validate {
19 | my ($self, $context) = @_;
20 |
21 | my @known_name_stack;
22 | my %known_names;
23 |
24 | return {
25 | ObjectValue => {
26 | enter => sub {
27 | push @known_name_stack, \%known_names;
28 | %known_names = ();
29 | return; # void
30 | },
31 | leave => sub {
32 | %known_names = %{ pop @known_name_stack };
33 | return; # void
34 | }
35 | },
36 | ObjectField => sub {
37 | my (undef, $node) = @_;
38 | my $field_name = $node->{name}{value};
39 |
40 | if ($known_names{ $field_name }) {
41 | $context->report_error(
42 | GraphQLError(
43 | duplicate_input_field_message($field_name),
44 | [$known_names{ $field_name }, $node->{name}]
45 | )
46 | );
47 | }
48 | else {
49 | $known_names{ $field_name } = $node->{name};
50 | }
51 |
52 | return FALSE;
53 | },
54 | };
55 | }
56 |
57 | 1;
58 |
59 | __END__
60 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/known_fragment_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub undef_frag {
15 | my ($frag_name, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::KnownFragmentNames::unknown_fragment_message($frag_name),
18 | locations => [ { line => $line, column => $column } ],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'known fragment names are valid' => sub {
24 | expect_passes_rule('KnownFragmentNames', '
25 | {
26 | human(id: 4) {
27 | ...HumanFields1
28 | ... on Human {
29 | ...HumanFields2
30 | }
31 | ... {
32 | name
33 | }
34 | }
35 | }
36 | fragment HumanFields1 on Human {
37 | name
38 | ...HumanFields3
39 | }
40 | fragment HumanFields2 on Human {
41 | name
42 | }
43 | fragment HumanFields3 on Human {
44 | name
45 | }
46 | ');
47 | };
48 |
49 | subtest 'unknown fragment names are invalid' => sub {
50 | expect_fails_rule('KnownFragmentNames', '
51 | {
52 | human(id: 4) {
53 | ...UnknownFragment1
54 | ... on Human {
55 | ...UnknownFragment2
56 | }
57 | }
58 | }
59 | fragment HumanFields on Human {
60 | name
61 | ...UnknownFragment3
62 | }
63 | ', [
64 | undef_frag('UnknownFragment1', 4, 14),
65 | undef_frag('UnknownFragment2', 6, 16),
66 | undef_frag('UnknownFragment3', 12, 12)
67 | ]);
68 | };
69 |
70 | done_testing;
71 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/ArgumentsOfCorrectType.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::ArgumentsOfCorrectType;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Printer qw/print_doc/;
8 | use GraphQL::Language::Visitor qw/FALSE/;
9 | use GraphQL::Util qw/is_valid_literal_value/;
10 |
11 | sub bad_value_message {
12 | my ($arg_name, $type, $value, $verbose_errors) = @_;
13 | my $message = $verbose_errors ? "\n" . join("\n", @$verbose_errors) : '';
14 | return qq`Argument "$arg_name" has invalid value $value.$message`;
15 | }
16 |
17 | # Argument values of correct type
18 | #
19 | # A GraphQL document is only valid if all field argument literal values are
20 | # of the type expected by their position.
21 | sub validate {
22 | my ($self, $context) = @_;
23 | return {
24 | Argument => sub {
25 | my (undef, $node) = @_;
26 | my $arg_def = $context->get_argument;
27 |
28 | if ($arg_def) {
29 | my $errors =
30 | is_valid_literal_value($arg_def->{type}, $node->{value});
31 | if ($errors && @$errors) {
32 | $context->report_error(
33 | GraphQLError(
34 | bad_value_message(
35 | $node->{name}{value},
36 | $arg_def->{type},
37 | print_doc($node->{value}),
38 | $errors
39 | ),
40 | [$node->{value}]
41 | )
42 | );
43 | }
44 | }
45 |
46 | return FALSE;
47 | },
48 | };
49 | }
50 |
51 | 1;
52 |
53 | __END__
54 |
--------------------------------------------------------------------------------
/t/graphql/validator.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/..";
9 | use harness qw/
10 | $test_schema
11 | /;
12 |
13 | use GraphQL::Language::Parser qw/parse/;
14 | use GraphQL::TypeInfo;
15 | use GraphQL::Validator qw/validate SPECIFIED_RULES/;
16 |
17 | sub expect_valid {
18 | my ($schema, $query_string) = @_;
19 | my $errors = validate($schema, parse($query_string));
20 | is_deeply $errors, [], 'Should validate';
21 | }
22 |
23 | subtest 'validates queries' => sub {
24 | expect_valid($test_schema, '
25 | query {
26 | catOrDog {
27 | ... on Cat {
28 | furColor
29 | }
30 | ... on Dog {
31 | isHousetrained
32 | }
33 | }
34 | }
35 | ');
36 | };
37 |
38 | # NOTE: experimental
39 | subtest 'validates using a custom TypeInfo' => sub {
40 | # This TypeInfo will never return a valid field.
41 | my $type_info = GraphQL::TypeInfo->new($test_schema, sub { 0 });
42 |
43 | my $ast = parse('
44 | query {
45 | catOrDog {
46 | ... on Cat {
47 | furColor
48 | }
49 | ... on Dog {
50 | isHousetrained
51 | }
52 | }
53 | }
54 | ');
55 |
56 | my $errors = validate($test_schema, $ast, SPECIFIED_RULES, $type_info);
57 | my @error_messages = map { $_->{message} } @$errors;
58 |
59 | is_deeply \@error_messages, [
60 | 'Cannot query field "catOrDog" on type "QueryRoot". Did you mean "catOrDog"?',
61 | 'Cannot query field "furColor" on type "Cat". Did you mean "furColor"?',
62 | 'Cannot query field "isHousetrained" on type "Dog". Did you mean "isHousetrained"?',
63 | ];
64 | };
65 |
66 | done_testing;
67 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/UniqueDirectivesPerLocation.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::UniqueDirectivesPerLocation;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 |
8 | sub duplicate_directive_message {
9 | my $directive_name = shift;
10 | return qq`The directive "$directive_name" can only be used once at `
11 | . qq`this location.`;
12 | }
13 |
14 | # Unique directive names per location
15 | #
16 | # A GraphQL document is only valid if all directives at a given location
17 | # are uniquely named.
18 | sub validate {
19 | my ($self, $context) = @_;
20 | return {
21 | # Many different AST nodes may contain directives. Rather than listing
22 | # them all, just listen for entering any node, and check to see if it
23 | # defines any directives.
24 | enter => sub {
25 | my (undef, $node) = @_;
26 |
27 | if ($node->{directives}) {
28 | my %known_directives;
29 | for my $directive (@{ $node->{directives} }) {
30 | my $directive_name = $directive->{name}{value};
31 | if ($known_directives{ $directive_name }) {
32 | $context->report_error(
33 | GraphQLError(
34 | duplicate_directive_message($directive_name),
35 | [$known_directives{ $directive_name }, $directive]
36 | )
37 | );
38 | }
39 | else {
40 | $known_directives{ $directive_name } = $directive;
41 | }
42 | }
43 | }
44 |
45 | return; # void
46 | },
47 | };
48 | }
49 |
50 | 1;
51 |
52 | __END__
53 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/Union.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::Union;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Util qw/assert_valid_name/;
7 | use GraphQL::Util::Type qw/define_types/;
8 |
9 | sub name { shift->{name} }
10 | sub resolve_type { shift->{resolve_type} }
11 |
12 | sub new {
13 | my ($class, %config) = @_;
14 |
15 | assert_valid_name($config{name});
16 |
17 | if ($config{resolve_type}) {
18 | die qq`$config{name} must provide "resolve_type" as a function.`
19 | if ref($config{resolve_type}) ne 'CODE';
20 | }
21 |
22 | my $self = bless {
23 | name => $config{name},
24 | description => $config{description} || '',
25 | resolve_type => $config{resolve_type},
26 |
27 | _type_config => \%config,
28 | _types => undef,
29 | _possible_type_names => undef,
30 | }, $class;
31 |
32 | return $self;
33 | }
34 |
35 | sub get_types {
36 | my $self = shift;
37 | return $self->{_types}
38 | || ($self->{_types} = define_types($self, $self->{_type_config}{types}));
39 | }
40 |
41 | sub to_string { shift->name }
42 |
43 | sub to_json { shift->to_string }
44 | sub inspect { shift->to_string }
45 |
46 | 1;
47 |
48 | __END__
49 |
50 | Union Type Definition
51 |
52 | When a field can return one of a heterogeneous set of types, a Union type
53 | is used to describe what types are possible as well as providing a function
54 | to determine which type is actually used when the field is resolved.
55 |
56 | Example:
57 |
58 | const PetType = new GraphQLUnionType({
59 | name: 'Pet',
60 | types: [ DogType, CatType ],
61 | resolveType(value) {
62 | if (value instanceof Dog) {
63 | return DogType;
64 | }
65 | if (value instanceof Cat) {
66 | return CatType;
67 | }
68 | }
69 | });
70 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/KnownTypeNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::KnownTypeNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Util qw/
8 | stringify_type
9 | quoted_or_list
10 | suggestion_list
11 | /;
12 |
13 | sub unknown_type_message {
14 | my ($type, $suggested_types) = @_;
15 | my $message = qq`Unknown type "${ stringify_type($type) }".`;
16 |
17 | if ($suggested_types && @$suggested_types) {
18 | $message .= ' Did you mean ' . quoted_or_list($suggested_types) . '?';
19 | }
20 |
21 | return $message;
22 | }
23 |
24 | # Known type names
25 | #
26 | # A GraphQL document is only valid if referenced types (specifically
27 | # variable definitions and fragment conditions) are defined by the type schema.
28 | sub validate {
29 | my ($self, $context) = @_;
30 | return {
31 | # TODO: when validating IDL, re-enable these. Experimental version does not
32 | # add unreferenced types, resulting in false-positive errors. Squelched
33 | # errors for now.
34 | ObjectTypeDefinition => sub { return },
35 | InterfaceTypeDefinition => sub { return },
36 | UnionTypeDefinition => sub { return },
37 | InputObjectTypeDefinition => sub { return },
38 | NamedType => sub {
39 | my (undef, $node) = @_;
40 |
41 | my $schema = $context->get_schema;
42 | my $type_name = $node->{name}{value};
43 | my $type = $schema->get_type($type_name);
44 |
45 | unless ($type) {
46 | $context->report_error(
47 | GraphQLError(
48 | unknown_type_message(
49 | $type_name,
50 | suggestion_list($type_name, [keys %{ $schema->get_type_map }]),
51 | ),
52 | [$node]
53 | )
54 | );
55 | }
56 |
57 | return;
58 | },
59 | };
60 | }
61 |
62 | 1;
63 |
64 | __END__
65 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/NoUnusedFragments.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::NoUnusedFragments;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Language::Visitor qw/FALSE/;
7 | use GraphQL::Error qw/GraphQLError/;
8 |
9 | sub unused_frag_message {
10 | my $frag_name = shift;
11 | return qq`Fragment "$frag_name" is never used.`;
12 | }
13 |
14 | # No unused fragments
15 | #
16 | # A GraphQL document is only valid if all fragment definitions are spread
17 | # within operations, or spread within other fragments spread within operations.
18 | sub validate {
19 | my ($self, $context) = @_;
20 | my (@operation_defs, @fragment_defs);
21 |
22 | return {
23 | OperationDefinition => sub {
24 | my (undef, $node) = @_;
25 | push @operation_defs, $node;
26 | return FALSE;
27 | },
28 | FragmentDefinition => sub {
29 | my (undef, $node) = @_;
30 | push @fragment_defs, $node;
31 | return FALSE;
32 | },
33 | Document => {
34 | leave => sub {
35 | my %fragment_name_used;
36 | for my $operation (@operation_defs) {
37 | my $frags = $context->get_recursively_referenced_fragments($operation);
38 | for my $frag (@$frags) {
39 | $fragment_name_used{ $frag->{name}{value} } = 1;
40 | }
41 | }
42 |
43 | for my $frag_def (@fragment_defs) {
44 | my $frag_name = $frag_def->{name}{value};
45 | unless ($fragment_name_used{ $frag_name }) {
46 | $context->report_error(
47 | GraphQLError(
48 | unused_frag_message($frag_name),
49 | [$frag_def]
50 | )
51 | );
52 | }
53 | }
54 |
55 | return; # void
56 | },
57 | },
58 | };
59 | }
60 |
61 | 1;
62 |
63 | __END__
64 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/known_type_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub unknown_type {
15 | my ($type_name, $suggested_types, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::KnownTypeNames::unknown_type_message($type_name, $suggested_types),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'known type names are valid' => sub {
24 | expect_passes_rule('KnownTypeNames', '
25 | query Foo($var: String, $required: [String!]!) {
26 | user(id: 4) {
27 | pets { ... on Pet { name }, ...PetFields, ... { name } }
28 | }
29 | }
30 | fragment PetFields on Pet {
31 | name
32 | }
33 | ');
34 | };
35 |
36 | subtest 'unknown type names are invalid' => sub {
37 | expect_fails_rule('KnownTypeNames', '
38 | query Foo($var: JumbledUpLetters) {
39 | user(id: 4) {
40 | name
41 | pets { ... on Badger { name }, ...PetFields }
42 | }
43 | }
44 | fragment PetFields on Peettt {
45 | name
46 | }
47 | ', [
48 | unknown_type('JumbledUpLetters', [], 2, 23),
49 | unknown_type('Badger', [], 5, 25),
50 | unknown_type('Peettt', [ 'Pet' ], 8, 29)
51 | ]);
52 | };
53 |
54 | subtest 'ignores type definitions' => sub {
55 | plan skip_all => 'FAILS';
56 |
57 | expect_fails_rule('KnownTypeNames', '
58 | type NotInTheSchema {
59 | field: FooBar
60 | }
61 | interface FooBar {
62 | field: NotInTheSchema
63 | }
64 | union U = A | B
65 | input Blob {
66 | field: UnknownType
67 | }
68 | query Foo($var: NotInTheSchema) {
69 | user(id: $var) {
70 | id
71 | }
72 | }
73 | ', [
74 | unknown_type('NotInTheSchema', [], 12, 23),
75 | ]);
76 | };
77 |
78 | done_testing;
79 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/NoUndefinedVariables.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::NoUndefinedVariables;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 |
8 | sub undefined_var_message {
9 | my ($var_name, $op_name) = @_;
10 | return $op_name
11 | ? qq`Variable "\$$var_name" is not defined by operation "$op_name".`
12 | : qq`Variable "\$$var_name" is not defined.`;
13 | }
14 |
15 | # No undefined variables
16 | #
17 | # A GraphQL operation is only valid if all variables encountered, both directly
18 | # and via fragment spreads, are defined by that operation.
19 | sub validate {
20 | my ($self, $context) = @_;
21 | my %variable_name_defined;
22 |
23 | return {
24 | OperationDefinition => {
25 | enter => sub {
26 | %variable_name_defined = ();
27 | return;
28 | },
29 | leave => sub {
30 | my (undef, $operation) = @_;
31 | my $usages = $context->get_recursive_variable_usages($operation);
32 |
33 | for my $u (@$usages) {
34 | my $node = $u->{node};
35 | my $var_name = $node->{name}{value};
36 |
37 | if (!$variable_name_defined{ $var_name }) {
38 | $context->report_error(
39 | GraphQLError(
40 | undefined_var_message(
41 | $var_name,
42 | $operation->{name} && $operation->{name}{value}
43 | ),
44 | [$node, $operation]
45 | )
46 | );
47 | }
48 | }
49 |
50 | return; # void
51 | },
52 | },
53 | VariableDefinition => sub {
54 | my (undef, $node) = @_;
55 | $variable_name_defined{ $node->{variable}{name}{value} } = 1;
56 | return; # void
57 | },
58 | };
59 | }
60 |
61 | 1;
62 |
63 | __END__
64 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/ScalarLeafs.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::ScalarLeafs;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Util qw/stringify_type/;
8 | use GraphQL::Util::Type qw/
9 | get_named_type
10 | is_leaf_type
11 | /;
12 |
13 | sub no_subselection_allowed_message {
14 | my ($field_name, $type) = @_;
15 | return qq`Field "$field_name" must not have a selection since `
16 | . qq`type "${ stringify_type($type) }" has no subfields.`;
17 | }
18 |
19 | sub required_subselection_message {
20 | my ($field_name, $type) = @_;
21 | return qq`Field "$field_name" of type "${ stringify_type($type) }" must have a `
22 | . qq`selection of subfields. Did you mean "$field_name { ... }"?`;
23 | }
24 |
25 | # Scalar leafs
26 | #
27 | # A graph_qL document is valid only if all leaf fields (fields without
28 | # sub selections) are of scalar or enum types.
29 | sub validate {
30 | my ($self, $context) = @_;
31 |
32 | return {
33 | Field => sub {
34 | my (undef, $node) = @_;
35 | my $type = $context->get_type;
36 |
37 | if ($type) {
38 | if (is_leaf_type(get_named_type($type))) {
39 | if ($node->{selection_set}) {
40 | $context->report_error(
41 | GraphQLError(
42 | no_subselection_allowed_message($node->{name}{value}, $type),
43 | [$node->{selection_set}]
44 | )
45 | );
46 | }
47 | }
48 | elsif (!$node->{selection_set}) {
49 | $context->report_error(
50 | GraphQLError(
51 | required_subselection_message($node->{name}{value}, $type),
52 | [$node]
53 | )
54 | );
55 | }
56 | }
57 |
58 | return; # void
59 | },
60 | };
61 | }
62 |
63 | 1;
64 |
65 | __END__
66 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/unique_input_field_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub duplicate_field {
15 | my ($name, $l1, $c1, $l2, $c2) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::UniqueInputFieldNames::duplicate_input_field_message($name),
18 | locations => [{ line => $l1, column => $c1 }, { line => $l2, column => $c2 }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'input object with fields' => sub {
24 | expect_passes_rule('UniqueInputFieldNames', '
25 | {
26 | field(arg: { f: true })
27 | }
28 | ');
29 | };
30 |
31 | subtest 'same input object within two args' => sub {
32 | expect_passes_rule('UniqueInputFieldNames', '
33 | {
34 | field(arg1: { f: true }, arg2: { f: true })
35 | }
36 | ');
37 | };
38 |
39 | subtest 'multiple input object fields' => sub {
40 | expect_passes_rule('UniqueInputFieldNames', '
41 | {
42 | field(arg: { f1: "value", f2: "value", f3: "value" })
43 | }
44 | ');
45 | };
46 |
47 | subtest 'allows for nested input objects with similar fields' => sub {
48 | expect_passes_rule('UniqueInputFieldNames', '
49 | {
50 | field(arg: {
51 | deep: {
52 | deep: {
53 | id: 1
54 | }
55 | id: 1
56 | }
57 | id: 1
58 | })
59 | }
60 | ');
61 | };
62 |
63 | subtest 'duplicate input object fields' => sub {
64 | expect_fails_rule('UniqueInputFieldNames', '
65 | {
66 | field(arg: { f1: "value", f1: "value" })
67 | }
68 | ', [
69 | duplicate_field('f1', 3, 22, 3, 35)
70 | ]);
71 | };
72 |
73 | subtest 'many duplicate input object fields' => sub {
74 | expect_fails_rule('UniqueInputFieldNames', '
75 | {
76 | field(arg: { f1: "value", f1: "value", f1: "value" })
77 | }
78 | ', [
79 | duplicate_field('f1', 3, 22, 3, 35),
80 | duplicate_field('f1', 3, 22, 3, 48)
81 | ]);
82 | };
83 |
84 | done_testing;
85 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/NoUnusedVariables.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::NoUnusedVariables;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 |
8 | sub unused_variable_message {
9 | my ($var_name, $op_name) = @_;
10 | return $op_name
11 | ? qq`Variable "\$$var_name" is never used in operation "$op_name".`
12 | : qq`Variable "\$$var_name" is never used.`;
13 | }
14 |
15 | # No unused variables
16 | #
17 | # A GraphQL operation is only valid if all variables defined by an operation
18 | # are used, either directly or within a spread fragment.
19 | sub validate {
20 | my ($self, $context) = @_;
21 | my @variable_defs;
22 |
23 | return {
24 | OperationDefinition => {
25 | enter => sub {
26 | @variable_defs = ();
27 | return; # void
28 | },
29 | leave => sub {
30 | my (undef, $operation) = @_;
31 |
32 |
33 | my %variable_name_used;
34 | my $usages = $context->get_recursive_variable_usages($operation);
35 | my $op_name = $operation->{name} ? $operation->{name}{value} : undef;
36 |
37 | for my $u (@$usages) {
38 | my $node = $u->{node};
39 | $variable_name_used{ $node->{name}{value} } = 1;
40 | }
41 |
42 | for my $variable_def (@variable_defs) {
43 | my $variable_name = $variable_def->{variable}{name}{value};
44 |
45 | unless ($variable_name_used{ $variable_name }) {
46 | $context->report_error(
47 | GraphQLError(
48 | unused_variable_message($variable_name, $op_name),
49 | [$variable_def]
50 | )
51 | );
52 | }
53 | }
54 |
55 | return; # void
56 | },
57 | },
58 | VariableDefinition => sub {
59 | my (undef, $def) = @_;
60 | push @variable_defs, $def;
61 | return; # void
62 | },
63 | };
64 | }
65 |
66 | 1;
67 |
68 | __END__
69 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/Scalar.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::Scalar;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | sub name { shift->{name} }
7 | sub description { shift->{description} }
8 |
9 | sub new {
10 | my ($class, %config) = @_;
11 |
12 | die qq`$config{name} must provide "serialize" function. If this custom Scalar `
13 | . qq`is also used as an input type, ensure "parse_value" and "parse_literal" `
14 | . qq`funcitions are also provided.\n`
15 | if ref($config{serialize}) ne 'CODE';
16 |
17 | if ($config{parse_value} || $config{parse_literal}) {
18 | die qq`$config{name} must provide both "parse_value" and "parse_literal" functions`
19 | if ref($config{parse_value}) ne 'CODE' || ref($config{parse_literal}) ne 'CODE';
20 | }
21 |
22 | my $self = bless {
23 | name => $config{name},
24 | description => $config{description} || '',
25 | _scalar_config => \%config,
26 | }, $class;
27 |
28 | return $self;
29 | }
30 |
31 | # Serializes an internal value to include a response.
32 | sub serialize {
33 | my ($self, $value) = @_;
34 | my $serializer = $self->{_scalar_config}{serialize};
35 | return $serializer->($value);
36 | }
37 |
38 | # Parses an externally provided value to use as an input.
39 | sub parse_value {
40 | my ($self, $value) = @_;
41 | my $parser = $self->{_scalar_config}{parse_value};
42 | return $parser ? $parser->($value) : undef;
43 | }
44 |
45 | # Parses an externally provided literal value to use as an input.
46 | sub parse_literal {
47 | my ($self, $value_node) = @_;
48 | my $parser = $self->{_scalar_config}{parse_literal};
49 | return $parser ? $parser->($value_node) : undef;
50 | }
51 |
52 | sub to_string { shift->name }
53 |
54 | sub to_json { shift->to_string }
55 | sub inspect { shift->to_string }
56 |
57 | 1;
58 |
59 | __END__
60 |
61 | Scalar Type Definition
62 |
63 | The leaf values of any request and input values to arguments are
64 | Scalars (or Enums) and are defined with a name and a series of functions
65 | used to parse input from ast or variables and to ensure validity.
66 |
67 | Example:
68 |
69 | const OddType = new GraphQLScalarType({
70 | name: 'Odd',
71 | serialize(value) {
72 | return value % 2 === 1 ? value : null;
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/lone_anonymous_operation.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub anon_not_alone {
15 | my ($line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::LoneAnonymousOperation::anon_operation_not_alone_message(),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'no operations' => sub {
24 | expect_passes_rule('LoneAnonymousOperation', '
25 | fragment fragA on Type {
26 | field
27 | }
28 | ');
29 | };
30 |
31 | subtest 'one anon operation' => sub {
32 | expect_passes_rule('LoneAnonymousOperation', '
33 | {
34 | field
35 | }
36 | ');
37 | };
38 |
39 | subtest 'multiple named operations' => sub {
40 | expect_passes_rule('LoneAnonymousOperation', '
41 | query Foo {
42 | field
43 | }
44 |
45 | query Bar {
46 | field
47 | }
48 | ');
49 | };
50 |
51 | subtest 'anon operation with fragment' => sub {
52 | expect_passes_rule('LoneAnonymousOperation', '
53 | {
54 | ...Foo
55 | }
56 | fragment Foo on Type {
57 | field
58 | }
59 | ');
60 | };
61 |
62 | subtest 'multiple anon operations' => sub {
63 | expect_fails_rule('LoneAnonymousOperation', '
64 | {
65 | fieldA
66 | }
67 | {
68 | fieldB
69 | }
70 | ', [
71 | anon_not_alone(2, 7),
72 | anon_not_alone(5, 7)
73 | ]);
74 | };
75 |
76 | subtest 'anon operation with a mutation' => sub {
77 | expect_fails_rule('LoneAnonymousOperation', '
78 | {
79 | fieldA
80 | }
81 | mutation Foo {
82 | fieldB
83 | }
84 | ', [
85 | anon_not_alone(2, 7)
86 | ]);
87 | };
88 |
89 | subtest 'anon operation with a subscription' => sub {
90 | expect_fails_rule('LoneAnonymousOperation', '
91 | {
92 | fieldA
93 | }
94 | subscription Foo {
95 | fieldB
96 | }
97 | ', [
98 | anon_not_alone(2, 7)
99 | ]);
100 | };
101 |
102 | done_testing;
103 |
--------------------------------------------------------------------------------
/lib/GraphQL/Util/TypeComparators.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Util::TypeComparators;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use List::Util qw/reduce/;
7 | use Exporter qw/import/;
8 |
9 | use GraphQL::Util::Type qw/
10 | is_abstract_type
11 | /;
12 |
13 | our @EXPORT_OK = (qw/
14 | is_equal_type
15 | do_types_overlap
16 | /);
17 |
18 | sub is_equal_type {
19 | my ($type_a, $type_b) = @_;
20 |
21 | # Equivalent types are equal.
22 | if ($type_a->to_string eq $type_b->to_string) {
23 | return 1;
24 | }
25 |
26 | # If either type is non-null, the other must also be non-null.
27 | if ($type_a->isa('GraphQL::Type::NonNull') && $type_b->isa('GraphQL::Type::NonNull')) {
28 | return is_equal_type($type_a->of_type, $type_b->of_type);
29 | }
30 |
31 | # If either type is a list, the other must also be a list.
32 | if ($type_a->isa('GraphQL::Type::List') && $type_b->isa('GraphQL::Type::List')) {
33 | return is_equal_type($type_a->of_type, $type_b->of_type);
34 | }
35 |
36 | # Otherwise the types are not equal.
37 | return;
38 | }
39 |
40 | # Provided two composite types, determine if they "overlap". Two composite
41 | # types overlap when the Sets of possible concrete types for each intersect.
42 | #
43 | # This is often used to determine if a fragment of a given type could possibly
44 | # be visited in a context of another type.
45 | #
46 | # This function is commutative.
47 | sub do_types_overlap {
48 | my ($schema, $type_a, $type_b) = @_;
49 |
50 | # Equivalent types overlap
51 | if ($type_a == $type_b) {
52 | return 1;
53 | }
54 |
55 | if (is_abstract_type($type_a)) {
56 | if (is_abstract_type($type_b)) {
57 | # If both types are abstract, then determine if there is any intersection
58 | # between possible concrete types of each.
59 | return reduce { $a || $schema->is_possible_type($type_b, $b) }
60 | 0, @{ $schema->get_possible_types($type_a) };
61 | }
62 |
63 | # Determine if the latter type is a possible concrete type of the former.
64 | return $schema->is_possible_type($type_a, $type_b);
65 | }
66 |
67 | if (is_abstract_type($type_b)) {
68 | # Determine if the former type is a possible concrete type of the latter.
69 | return $schema->is_possible_type($type_b, $type_a);
70 | }
71 |
72 | # Otherwise the types do not overlap.
73 | return;
74 | }
75 |
76 | 1;
77 |
78 | __END__
79 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/Object.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::Object;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Util qw/assert_valid_name/;
7 | use GraphQL::Util::Type qw/define_field_map define_interfaces/;
8 |
9 | sub name { shift->{name} }
10 | sub is_type_of { shift->{is_type_of} }
11 |
12 | sub new {
13 | my ($class, %config) = @_;
14 |
15 | assert_valid_name($config{name}, $config{is_introspection});
16 |
17 | if ($config{is_type_of}) {
18 | die qq`$config{name} must provide "is_type_of" as a function.`
19 | if ref($config{is_type_of}) ne 'CODE';
20 | }
21 |
22 | my $self = bless {
23 | name => $config{name},
24 | description => $config{description} || '',
25 | is_type_of => $config{is_type_of},
26 |
27 | _type_config => \%config,
28 | _fields => undef,
29 | _interfaces => undef,
30 | }, $class;
31 |
32 | return $self;
33 | }
34 |
35 | sub get_fields {
36 | my $self = shift;
37 | return $self->{_fields}
38 | || ($self->{_fields} = define_field_map($self, $self->{_type_config}{fields}));
39 | }
40 |
41 | sub get_interfaces {
42 | my $self = shift;
43 | return $self->{_interfaces}
44 | || ($self->{_interfaces} = define_interfaces($self, $self->{_type_config}{interfaces}));
45 | }
46 |
47 | sub to_string { shift->name }
48 |
49 | sub to_json { shift->to_string }
50 | sub inspect { shift->to_string }
51 |
52 | 1;
53 |
54 | __END__
55 |
56 | Object Type Definition
57 |
58 | Almost all of the GraphQL types you define will be object types. Object types
59 | have a name, but most importantly describe their fields.
60 |
61 | Example:
62 |
63 | const AddressType = new GraphQLObjectType({
64 | name: 'Address',
65 | fields: {
66 | street: { type: GraphQLString },
67 | number: { type: GraphQLInt },
68 | formatted: {
69 | type: GraphQLString,
70 | resolve(obj) {
71 | return obj.number + ' ' + obj.street
72 | }
73 | }
74 | }
75 | });
76 |
77 | When two types need to refer to each other, or a type needs to refer to
78 | itself in a field, you can use a function expression (aka a closure or a
79 | thunk) to supply the fields lazily.
80 |
81 | Example:
82 |
83 | const PersonType = new GraphQLObjectType({
84 | name: 'Person',
85 | fields: () => ({
86 | name: { type: GraphQLString },
87 | bestFriend: { type: PersonType },
88 | })
89 | });
90 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/FragmentsOnCompositeTypes.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::FragmentsOnCompositeTypes;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Printer qw/print_doc/;
8 | use GraphQL::Util qw/
9 | stringify_type
10 | type_from_ast
11 | /;
12 | use GraphQL::Util::Type qw/is_composite_type/;
13 |
14 | sub inline_fragment_on_non_composite_error_message {
15 | my $type = shift;
16 | return qq`Fragment cannot condition on non composite type "${ stringify_type($type) }".`;
17 | }
18 |
19 | sub fragment_on_non_composite_error_message {
20 | my ($frag_name, $type) = @_;
21 | return qq`Fragment "$frag_name" cannot condition on non composite `
22 | . qq`type "${ stringify_type($type) }".`;
23 | }
24 |
25 | # Fragments on composite type
26 | #
27 | # Fragments use a type condition to determine if they apply, since fragments
28 | # can only be spread into a composite type (object, interface, or union), the
29 | # type condition must also be a composite type.
30 | sub validate {
31 | my ($self, $context) = @_;
32 | return {
33 | InlineFragment => sub {
34 | my (undef, $node) = @_;
35 |
36 | if ($node->{type_condition}) {
37 | my $type = type_from_ast($context->get_schema, $node->{type_condition});
38 | if ($type && !is_composite_type($type)) {
39 | $context->report_error(
40 | GraphQLError(
41 | inline_fragment_on_non_composite_error_message(
42 | print_doc($node->{type_condition}),
43 | ),
44 | [$node->{type_condition}]
45 | )
46 | )
47 | }
48 | }
49 |
50 | return; # void;
51 | },
52 | FragmentDefinition => sub {
53 | my (undef, $node) = @_;
54 |
55 | my $type = type_from_ast($context->get_schema, $node->{type_condition});
56 | if ($type && !is_composite_type($type)) {
57 | $context->report_error(
58 | GraphQLError(
59 | fragment_on_non_composite_error_message(
60 | $node->{name}{value},
61 | print_doc($node->{type_condition})
62 | ),
63 | [$node->{type_condition}]
64 | )
65 | );
66 | }
67 |
68 | return; # void;
69 | },
70 | };
71 | }
72 |
73 | 1;
74 |
75 | __END__
76 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/DefaultValuesOfCorrectType.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::DefaultValuesOfCorrectType;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Printer qw/print_doc/;
8 | use GraphQL::Language::Visitor qw/FALSE/;
9 | use GraphQL::Util qw/
10 | stringify_type
11 | is_valid_literal_value
12 | /;
13 |
14 | sub default_for_non_null_arg_message {
15 | my ($var_name, $type, $guess_type) = @_;
16 | return qq`Variable "\$$var_name" of type "${ stringify_type($type) }" is required and `
17 | . qq`will not use the default value. `
18 | . qq`Perhaps you meant to use type "${ stringify_type($guess_type) }".`;
19 | }
20 |
21 | sub bad_value_for_default_arg_message {
22 | my ($var_name, $type, $value, $verbose_errors) = @_;
23 | my $message = $verbose_errors ? "\n" . join("\n", @$verbose_errors) : '';
24 | return qq`Variable "\$$var_name" of type "${ stringify_type($type) }" has invalid `
25 | . qq`default value $value.$message`;
26 | }
27 |
28 | # Variable default values of correct type
29 | #
30 | # A GraphQL document is only valid if all variable default values are of the
31 | # type expected by their definition.
32 | sub validate {
33 | my ($self, $context) = @_;
34 | return {
35 | VariableDefinition => sub {
36 | my (undef, $node) = @_;
37 |
38 | my $name = $node->{variable}{name}{value};
39 | my $default_value = $node->{default_value};
40 | my $type = $context->get_input_type;
41 |
42 | if ($type->isa('GraphQL::Type::NonNull') && $default_value) {
43 | $context->report_error(
44 | GraphQLError(
45 | default_for_non_null_arg_message($name, $type, $type->of_type),
46 | [$default_value]
47 | )
48 | );
49 | }
50 |
51 | if ($type && $default_value) {
52 | my $errors = is_valid_literal_value($type, $default_value);
53 | if ($errors && @$errors) {
54 | $context->report_error(
55 | GraphQLError(
56 | bad_value_for_default_arg_message(
57 | $name, $type, print_doc($default_value), $errors
58 | ),
59 | [$default_value]
60 | )
61 | );
62 | }
63 | }
64 |
65 | return FALSE
66 | },
67 | SelectionSet => sub { FALSE },
68 | FragmentDefinition => sub { FALSE },
69 | };
70 | }
71 |
72 | 1;
73 |
74 | __END__
75 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/unique_fragment_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub duplicate_frag {
15 | my ($frag_name, $l1, $c1, $l2, $c2) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::UniqueFragmentNames::duplicate_fragment_name_message($frag_name),
18 | locations => [{ line => $l1, column => $c1 }, { line => $l2, column => $c2 }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'no fragments' => sub {
24 | expect_passes_rule('UniqueFragmentNames', '
25 | {
26 | field
27 | }
28 | ');
29 | };
30 |
31 | subtest 'one fragment' => sub {
32 | expect_passes_rule('UniqueFragmentNames', '
33 | {
34 | ...fragA
35 | }
36 |
37 | fragment fragA on Type {
38 | field
39 | }
40 | ');
41 | };
42 |
43 | subtest 'many fragments' => sub {
44 | expect_passes_rule('UniqueFragmentNames', '
45 | {
46 | ...fragA
47 | ...fragB
48 | ...fragC
49 | }
50 | fragment fragA on Type {
51 | fieldA
52 | }
53 | fragment fragB on Type {
54 | fieldB
55 | }
56 | fragment fragC on Type {
57 | fieldC
58 | }
59 | ');
60 | };
61 |
62 | subtest 'inline fragments are always unique' => sub {
63 | expect_passes_rule('UniqueFragmentNames', '
64 | {
65 | ...on Type {
66 | fieldA
67 | }
68 | ...on Type {
69 | fieldB
70 | }
71 | }
72 | ');
73 | };
74 |
75 | subtest 'fragment and operation named the same' => sub {
76 | expect_passes_rule('UniqueFragmentNames', '
77 | query Foo {
78 | ...Foo
79 | }
80 | fragment Foo on Type {
81 | field
82 | }
83 | ');
84 | };
85 |
86 | subtest 'fragments named the same' => sub {
87 | expect_fails_rule('UniqueFragmentNames', '
88 | {
89 | ...fragA
90 | }
91 | fragment fragA on Type {
92 | fieldA
93 | }
94 | fragment fragA on Type {
95 | fieldB
96 | }
97 | ', [
98 | duplicate_frag('fragA', 5, 16, 8, 16)
99 | ]);
100 | };
101 |
102 | subtest 'fragments named the same without being referenced' => sub {
103 | expect_fails_rule('UniqueFragmentNames', '
104 | fragment fragA on Type {
105 | fieldA
106 | }
107 | fragment fragA on Type {
108 | fieldB
109 | }
110 | ', [
111 | duplicate_frag('fragA', 2, 16, 5, 16)
112 | ]);
113 | };
114 |
115 | done_testing;
116 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/InputObject.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::InputObject;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Util::Type qw/is_input_type is_plain_obj resolve_thunk/;
7 | use GraphQL::Util qw/assert_valid_name/;
8 |
9 | sub name { shift->{name} }
10 |
11 | sub new {
12 | my ($class, %config) = @_;
13 |
14 | assert_valid_name($config{name});
15 |
16 | my $self = bless {
17 | name => $config{name},
18 | description => $config{description},
19 |
20 | _type_config => \%config,
21 | _fields => undef,
22 | }, $class;
23 |
24 | return $self;
25 | }
26 |
27 | sub get_fields {
28 | my $self = shift;
29 | return $self->{_fields}
30 | || ($self->{_fields} = $self->_define_field_map);
31 | }
32 |
33 | sub _define_field_map {
34 | my $self = shift;
35 |
36 | my $field_map = resolve_thunk($self->{_type_config}{fields});
37 | die qq`$self->{name} fields must be an object with field names as keys or a `
38 | . qq`function which returns such an object\n` unless is_plain_obj($field_map);
39 |
40 | my @field_names = keys %$field_map;
41 | die qq`$self->{name} fields must be an object with names as keys or a `
42 | . qq`function which return such an object\n` unless scalar(@field_names);
43 |
44 | my %result_field_map;
45 | for my $field_name (@field_names) {
46 | assert_valid_name($field_name);
47 |
48 | my $field = {
49 | # description - can be omitted
50 | default_value => undef,
51 | %{ $field_map->{$field_name} },
52 | name => $field_name,
53 | };
54 |
55 | die qq`$self->{name}.$field_name field type must be Input Type but `
56 | . qq`got: $field->{type}\n` unless is_input_type($field->{type});
57 |
58 | die qq`$self->{name}.$field_name field has a resolve property, but `
59 | . qq`Input Types cannot define resolvers.\n` if $field->{resolve};
60 |
61 | $result_field_map{$field_name} = $field;
62 | }
63 |
64 | return \%result_field_map;
65 | }
66 |
67 | sub to_string { shift->name }
68 |
69 | sub to_json { shift->to_string }
70 | sub inspect { shift->to_string }
71 |
72 | 1;
73 |
74 | __END__
75 |
76 | Input Object Type Definition
77 |
78 | An input object defines a structured collection of fields which may be
79 | supplied to a field argument.
80 |
81 | Using `NonNull` will ensure that a value must be provided by the query
82 |
83 | Example:
84 |
85 | const GeoPoint = new GraphQLInputObjectType({
86 | name: 'GeoPoint',
87 | fields: {
88 | lat: { type: new GraphQLNonNull(GraphQLFloat) },
89 | lon: { type: new GraphQLNonNull(GraphQLFloat) },
90 | alt: { type: GraphQLFloat, defaultValue: 0 },
91 | }
92 | });
93 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/PossibleFragmentSpreads.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::PossibleFragmentSpreads;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Util qw/
8 | stringify_type
9 | type_from_ast
10 | /;
11 | use GraphQL::Util::TypeComparators qw/do_types_overlap/;
12 |
13 | use DDP;
14 |
15 | sub type_incompatible_spread_message {
16 | my ($frag_name, $parent_type, $frag_type) = @_;
17 | return qq`Fragment "${frag_name}" cannot be spread here as objects of `
18 | . qq`type "${ stringify_type($parent_type) }" can never be of type "${ stringify_type($frag_type) }".`;
19 | }
20 |
21 | sub type_incompatible_anon_spread_message {
22 | my ($parent_type, $frag_type) = @_;
23 | return qq`Fragment cannot be spread here as objects of `
24 | . qq`type "${ stringify_type($parent_type) }" can never be of type "${ stringify_type($frag_type) }".`;
25 | }
26 |
27 | # Possible fragment spread
28 | #
29 | # A fragment spread is only valid if the type condition could ever possibly
30 | # be true: if there is a non-empty intersection of the possible parent types,
31 | # and possible types which pass the type condition->
32 | sub validate {
33 | my ($self, $context) = @_;
34 | return {
35 | InlineFragment => sub {
36 | my (undef, $node) = @_;
37 |
38 | my $frag_type = $context->get_type;
39 | my $parent_type = $context->get_parent_type;
40 |
41 | if ( $frag_type
42 | && $parent_type
43 | && !do_types_overlap($context->get_schema, $frag_type, $parent_type))
44 | {
45 | $context->report_error(
46 | GraphQLError(
47 | type_incompatible_anon_spread_message($parent_type, $frag_type),
48 | [$node]
49 | )
50 | );
51 | }
52 |
53 | return; # void
54 | },
55 | FragmentSpread => sub {
56 | my (undef, $node) = @_;
57 |
58 | my $frag_name = $node->{name}{value};
59 | my $frag_type = get_fragment_type($context, $frag_name);
60 | my $parent_type = $context->get_parent_type;
61 |
62 | if ( $frag_type
63 | && $parent_type
64 | && !do_types_overlap($context->get_schema, $frag_type, $parent_type))
65 | {
66 | $context->report_error(
67 | GraphQLError(
68 | type_incompatible_spread_message($frag_name, $parent_type, $frag_type),
69 | [$node]
70 | )
71 | );
72 | }
73 |
74 | return; # void
75 | },
76 | };
77 | }
78 |
79 | sub get_fragment_type {
80 | my ($context, $name) = @_;
81 | my $frag = $context->get_fragment($name);
82 | return $frag && type_from_ast($context->get_schema, $frag->{type_condition});
83 | }
84 |
85 | 1;
86 |
87 | __END__
88 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/unique_operation_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub duplicate_op {
15 | my ($op_name, $l1, $c1, $l2, $c2) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::UniqueOperationNames::duplicate_operation_name_message($op_name),
18 | locations => [{ line => $l1, column => $c1 }, { line => $l2, column => $c2 }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'no operations' => sub {
24 | expect_passes_rule('UniqueOperationNames', '
25 | fragment fragA on Type {
26 | field
27 | }
28 | ');
29 | };
30 |
31 | subtest 'one anon operation' => sub {
32 | expect_passes_rule('UniqueOperationNames', '
33 | {
34 | field
35 | }
36 | ');
37 | };
38 |
39 | subtest 'one named operation' => sub {
40 | expect_passes_rule('UniqueOperationNames', '
41 | query Foo {
42 | field
43 | }
44 | ');
45 | };
46 |
47 | subtest 'multiple operations' => sub {
48 | expect_passes_rule('UniqueOperationNames', '
49 | query Foo {
50 | field
51 | }
52 |
53 | query Bar {
54 | field
55 | }
56 | ');
57 | };
58 |
59 | subtest 'multiple operations of different types' => sub {
60 | expect_passes_rule('UniqueOperationNames', '
61 | query Foo {
62 | field
63 | }
64 |
65 | mutation Bar {
66 | field
67 | }
68 |
69 | subscription Baz {
70 | field
71 | }
72 | ');
73 | };
74 |
75 | subtest 'fragment and operation named the same' => sub {
76 | expect_passes_rule('UniqueOperationNames', '
77 | query Foo {
78 | ...Foo
79 | }
80 | fragment Foo on Type {
81 | field
82 | }
83 | ');
84 | };
85 |
86 | subtest 'multiple operations of same name' => sub {
87 | expect_fails_rule('UniqueOperationNames', '
88 | query Foo {
89 | fieldA
90 | }
91 | query Foo {
92 | fieldB
93 | }
94 | ', [
95 | duplicate_op('Foo', 2, 13, 5, 13)
96 | ]);
97 | };
98 |
99 | subtest 'multiple ops of same name of different types (mutation)' => sub {
100 | expect_fails_rule('UniqueOperationNames', '
101 | query Foo {
102 | fieldA
103 | }
104 | mutation Foo {
105 | fieldB
106 | }
107 | ', [
108 | duplicate_op('Foo', 2, 13, 5, 16)
109 | ]);
110 | };
111 |
112 | subtest 'multiple ops of same name of different types (subscription)' => sub {
113 | expect_fails_rule('UniqueOperationNames', '
114 | query Foo {
115 | fieldA
116 | }
117 | subscription Foo {
118 | fieldB
119 | }
120 | ', [
121 | duplicate_op('Foo', 2, 13, 5, 20)
122 | ]);
123 | };
124 |
125 | done_testing;
126 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/fragment_on_composite_types.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub error {
15 | my ($frag_name, $type_name, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::FragmentsOnCompositeTypes::fragment_on_non_composite_error_message($frag_name, $type_name),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'object is valid fragment type' => sub {
24 | expect_passes_rule('FragmentsOnCompositeTypes', '
25 | fragment validFragment on Dog {
26 | barks
27 | }
28 | ');
29 | };
30 |
31 | subtest 'interface is valid fragment type' => sub {
32 | expect_passes_rule('FragmentsOnCompositeTypes', '
33 | fragment validFragment on Pet {
34 | name
35 | }
36 | ');
37 | };
38 |
39 | subtest 'object is valid inline fragment type' => sub {
40 | expect_passes_rule('FragmentsOnCompositeTypes', '
41 | fragment validFragment on Pet {
42 | ... on Dog {
43 | barks
44 | }
45 | }
46 | ');
47 | };
48 |
49 | subtest 'inline fragment without type is valid' => sub {
50 | expect_passes_rule('FragmentsOnCompositeTypes', '
51 | fragment validFragment on Pet {
52 | ... {
53 | name
54 | }
55 | }
56 | ');
57 | };
58 |
59 | subtest 'union is valid fragment type' => sub {
60 | expect_passes_rule('FragmentsOnCompositeTypes', '
61 | fragment validFragment on CatOrDog {
62 | __typename
63 | }
64 | ');
65 | };
66 |
67 | subtest 'scalar is invalid fragment type' => sub {
68 | expect_fails_rule('FragmentsOnCompositeTypes', '
69 | fragment scalarFragment on Boolean {
70 | bad
71 | }
72 | ', [error('scalarFragment', 'Boolean', 2, 34)]);
73 | };
74 |
75 | subtest 'enum is invalid fragment type' => sub {
76 | expect_fails_rule('FragmentsOnCompositeTypes', '
77 | fragment scalarFragment on FurColor {
78 | bad
79 | }
80 | ', [error('scalarFragment', 'FurColor', 2, 34)]);
81 | };
82 |
83 | subtest 'input object is invalid fragment type' => sub {
84 | expect_fails_rule('FragmentsOnCompositeTypes', '
85 | fragment inputFragment on ComplexInput {
86 | stringField
87 | }
88 | ', [error('inputFragment', 'ComplexInput', 2, 33)]);
89 | };
90 |
91 | subtest 'scalar is invalid inline fragment type' => sub {
92 | expect_fails_rule('FragmentsOnCompositeTypes', '
93 | fragment invalidFragment on Pet {
94 | ... on String {
95 | barks
96 | }
97 | }
98 | ',
99 | [{ message => GraphQL::Validator::Rule::FragmentsOnCompositeTypes::inline_fragment_on_non_composite_error_message('String'),
100 | locations => [ { line => 3, column => 16 } ],
101 | path => undef, }]);
102 | };
103 |
104 | done_testing;
105 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/NoFragmentCycles.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::NoFragmentCycles;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Language::Visitor qw/FALSE/;
7 | use GraphQL::Error qw/GraphQLError/;
8 |
9 | sub cycle_error_message {
10 | my ($frag_name, $spread_names) = @_;
11 | my $via = scalar @$spread_names ? ' via ' . join(', ', @$spread_names) : '';
12 | return qq`Cannot spread fragment "$frag_name" within itself$via.`;
13 | }
14 |
15 | sub validate {
16 | my ($self, $context) = @_;
17 |
18 | # Tracks already visited fragments to maintain O(N) and to ensure that
19 | # cycles are not redundantly reported.
20 | my %visited_frags;
21 |
22 | # Array of AST nodes used to produce meaningful errors
23 | my @spread_path;
24 |
25 | # Position in the spread path
26 | my %spread_path_index_by_name;
27 |
28 | # This does a straight-forward DFS to find cycles.
29 | # It does not terminate when a cycle was found but continues to explore
30 | # the graph to find all possible cycles.
31 | my $detect_cycle_recursive;
32 | $detect_cycle_recursive = sub {
33 | my $frag = shift;
34 | my $frag_name = $frag->{name}{value};
35 |
36 | $visited_frags{ $frag_name } = 1;
37 |
38 | my $spread_nodes = $context->get_fragment_spreads($frag->{selection_set});
39 | unless (@$spread_nodes) {
40 | return;
41 | }
42 |
43 | $spread_path_index_by_name{ $frag_name } = scalar @spread_path;
44 |
45 | for my $spread_node (@$spread_nodes) {
46 | my $spread_name = $spread_node->{name}{value};
47 | my $cycle_index = $spread_path_index_by_name{ $spread_name };
48 |
49 | if (!defined($cycle_index)) {
50 | push @spread_path, $spread_node;
51 |
52 | if (!$visited_frags{ $spread_name }) {
53 | my $spread_fragment = $context->get_fragment($spread_name);
54 | if ($spread_fragment) {
55 | $detect_cycle_recursive->($spread_fragment);
56 | }
57 | }
58 |
59 | pop @spread_path;
60 | }
61 | else {
62 | my @cycle_path = splice @spread_path, $cycle_index;
63 | $context->report_error(
64 | GraphQLError(
65 | cycle_error_message(
66 | $spread_name,
67 | [map { $_->{name}{value} } @cycle_path]
68 | ),
69 | [@cycle_path, $spread_node]
70 | )
71 | );
72 | }
73 | }
74 |
75 | $spread_path_index_by_name{ $frag_name } = undef;
76 | };
77 |
78 | return {
79 | OperationDefinition => sub { return },
80 | FragmentDefinition => sub {
81 | my (undef, $node) = @_;
82 |
83 | if (!$visited_frags{ $node->{name}{value} }) {
84 | $detect_cycle_recursive->($node);
85 | }
86 |
87 | return FALSE;
88 | },
89 | };
90 | }
91 |
92 | 1;
93 |
94 | __END__
95 |
--------------------------------------------------------------------------------
/t/graphql/execute-resolve.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 | use Test::Deep;
7 | use JSON qw/encode_json/;
8 |
9 | use GraphQL qw/graphql :types/;
10 | use GraphQL::Language::Parser qw/parse/;
11 | use GraphQL::Execute qw/execute/;
12 |
13 | sub testSchema {
14 | my $testField = shift;
15 | return GraphQLSchema(
16 | query => GraphQLObjectType(
17 | name => 'Query',
18 | fields => {
19 | test => $testField,
20 | },
21 | ),
22 | );
23 | }
24 |
25 | subtest 'default function accesses properties' => sub {
26 | my $schema = testSchema({ type => GraphQLString });
27 |
28 | my $source = {
29 | test => 'testValue'
30 | };
31 |
32 | is_deeply graphql($schema, '{ test }', $source), {
33 | data => {
34 | test => 'testValue'
35 | }
36 | };
37 | };
38 |
39 | subtest 'default function calls methods' => sub {
40 | my $schema = testSchema({ type => GraphQLString });
41 |
42 | my $source;
43 | $source = {
44 | _secret => 'secretValue',
45 | test => sub { $source->{_secret} },
46 | };
47 |
48 | is_deeply graphql($schema, '{ test }', $source), {
49 | data => {
50 | test => 'secretValue'
51 | }
52 | };
53 | };
54 |
55 | subtest 'default function passes args and context' => sub {
56 | my $schema = testSchema({
57 | type => GraphQLInt,
58 | args => {
59 | addend1 => { type => GraphQLInt },
60 | },
61 | });
62 |
63 | {
64 | package Adder;
65 |
66 | sub new {
67 | my ($class, $num) = @_;
68 | bless { _num => $num }, $class;
69 | }
70 |
71 | sub test {
72 | my ($self, $args, $context) = @_;
73 | return $self->{_num} + $args->{addend1} + $context->{addend2};
74 | }
75 | }
76 | my $source = Adder->new(700);
77 |
78 | is_deeply graphql($schema, '{ test(addend1: 80) }', $source, { addend2 => 9 }),
79 | { data => { test => 789 } };
80 | };
81 |
82 | subtest 'uses provided resolve function' => sub {
83 | my $schema = testSchema({
84 | type => GraphQLString,
85 | args => {
86 | aStr => { type => GraphQLString },
87 | aInt => { type => GraphQLInt },
88 | },
89 | resolve => sub {
90 | my ($source, $args) = @_;
91 | encode_json([$source, $args]);
92 | }
93 | });
94 |
95 | is_deeply graphql($schema, '{ test }'), { data => { test => '[null,{}]' } };
96 |
97 | is_deeply graphql($schema, '{ test }', 'Source!'), {
98 | data => { test => '["Source!",{}]' }
99 | };
100 |
101 | is_deeply graphql($schema, '{ test(aStr: "String!") }', 'Source!'), {
102 | data => { test => '["Source!",{"aStr":"String!"}]' }
103 | };
104 |
105 | is_deeply graphql($schema, '{ test(aInt: -123, aStr: "String!") }', 'Source!'), {
106 | data => { test => '["Source!",{"aStr":"String!","aInt":-123}]' }
107 | };
108 | };
109 |
110 | done_testing;
111 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/Enum.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::Enum;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Language::Kinds qw/ENUM/;
7 | use GraphQL::Util qw/assert_valid_name/;
8 | use GraphQL::Util::Type qw/define_enum_values/;
9 |
10 | sub name { shift->{name} }
11 |
12 | sub new {
13 | my ($class, %config) = @_;
14 |
15 | assert_valid_name($config{name}, $config{is_introspection});
16 |
17 | my $self = bless {
18 | name => $config{name},
19 | description => $config{description},
20 |
21 | _enum_config => \%config,
22 | _values => undef,
23 | _value_lookup => undef,
24 | _name_lookup => undef,
25 | }, $class;
26 |
27 | # NOTE: RANDOM ORDER OF VALUES
28 | # NOTE: random order; makes an array from a hash
29 | $self->{_values} = define_enum_values($self, $config{values});
30 |
31 | return $self;
32 | }
33 |
34 | sub get_values { shift->{_values} }
35 |
36 | sub get_value {
37 | my ($self, $name) = @_;
38 | return $self->_get_name_lookup->{$name};
39 | }
40 |
41 | sub serialize {
42 | my ($self, $value) = @_;
43 | my $enum_value = $self->_get_value_lookup->{$value};
44 | return $enum_value ? $enum_value->{name} : undef; # null
45 | }
46 |
47 | sub parse_value {
48 | my ($self, $value) = @_;
49 | if (!ref($value)) { # === 'string'
50 | my $enum_value = $self->_get_name_lookup->{$value};
51 | if ($enum_value) {
52 | return $enum_value->{value};
53 | }
54 | }
55 | return;
56 | }
57 |
58 | sub parse_literal {
59 | my ($self, $value_node) = @_;
60 | if ($value_node->{kind} eq ENUM) {
61 | my $enum_value = $self->_get_name_lookup->{ $value_node->{value} };
62 | if ($enum_value) {
63 | return $enum_value->{value};
64 | }
65 | }
66 | return;
67 | }
68 |
69 | sub _get_value_lookup {
70 | my $self = shift;
71 | if (!$self->{_value_lookup}) {
72 | my %lookup = map { $_->{value} => $_ } @{ $self->get_values };
73 | $self->{_value_lookup} = \%lookup;
74 | }
75 | return $self->{_value_lookup};
76 | }
77 |
78 | sub _get_name_lookup {
79 | my $self = shift;
80 | if (!$self->{_name_lookup}) {
81 | my %lookup = map { $_->{name} => $_ } @{ $self->get_values };
82 | $self->{_name_lookup} = \%lookup;
83 | }
84 | return $self->{_name_lookup};
85 | }
86 |
87 | sub to_string { shift->name }
88 |
89 | sub to_json { shift->to_string }
90 | sub inspect { shift->to_string }
91 |
92 | 1;
93 |
94 | __END__
95 |
96 | Enum Type Definition
97 |
98 | Some leaf values of requests and input values are Enums. GraphQL serializes
99 | Enum values as strings, however internally Enums can be represented by any
100 | kind of type, often integers.
101 |
102 | Example:
103 |
104 | const RGBType = new GraphQLEnumType({
105 | name: 'RGB',
106 | values: {
107 | RED: { value: 0 },
108 | GREEN: { value: 1 },
109 | BLUE: { value: 2 }
110 | }
111 | });
112 |
113 | Note: If a value is not provided in a definition, the name of the enum value
114 | will be used as its internal value.
115 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/VariablesInAllowedPosition.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::VariablesInAllowedPosition;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Type qw/GraphQLNonNull/;
8 | use GraphQL::Util qw/
9 | stringify_type
10 | type_from_ast
11 | /;
12 | use GraphQL::Util::Type qw/is_type_subtype_of/;
13 |
14 | sub bad_var_pos_message {
15 | my ($var_name, $var_type, $expected_type) = @_;
16 | return qq`Variable "\$$var_name" of type "${ stringify_type($var_type) }" used in `
17 | . qq`position expecting type "${ stringify_type($expected_type) }".`;
18 | }
19 |
20 | # Variables passed to field arguments conform to type
21 | sub validate {
22 | my ($self, $context) = @_;
23 | my %var_def_map;
24 |
25 | return {
26 | OperationDefinition => {
27 | enter => sub {
28 | %var_def_map = ();
29 | return; # void
30 | },
31 | leave => sub {
32 | my (undef, $operation) = @_;
33 | my $usages = $context->get_recursive_variable_usages($operation);
34 |
35 | for my $usage (@$usages) {
36 | my $node = $usage->{node};
37 | my $type = $usage->{type};
38 |
39 | my $var_name = $node->{name}{value};
40 | my $var_def = $var_def_map{ $var_name };
41 | if ($var_def && $type) {
42 | # A var type is allowed if it is the same or more strict (e.g. is
43 | # a subtype of) than the expected type. It can be more strict if
44 | # the variable type is non-null when the expected type is nullable.
45 | # If both are list types, the variable item type can be more strict
46 | # than the expected item type (contravariant).
47 | my $schema = $context->get_schema;
48 | my $var_type = type_from_ast($schema, $var_def->{type});
49 | if ($var_type &&
50 | !is_type_subtype_of($schema, effective_type($var_type, $var_def), $type))
51 | {
52 | $context->report_error(
53 | GraphQLError(
54 | bad_var_pos_message($var_name, $var_type, $type),
55 | [$var_def, $node]
56 | )
57 | );
58 | }
59 | }
60 | };
61 |
62 | return; # void
63 | }
64 | },
65 | VariableDefinition => sub {
66 | my (undef, $node) = @_;
67 | $var_def_map{ $node->{variable}{name}{value} } = $node;
68 | return; # void
69 | },
70 | };
71 | }
72 |
73 | # If a variable definition has a default value, it's effectively non-null.
74 | sub effective_type {
75 | my ($var_type, $var_def) = @_;
76 | return !$var_def->{default_value} || $var_type->isa('GraphQL::Type::NonNull')
77 | ? $var_type
78 | : GraphQLNonNull($var_type);
79 | }
80 |
81 | 1;
82 |
83 | __END__
84 |
--------------------------------------------------------------------------------
/t/graphql/language/printer.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use FindBin '$Bin';
6 | # use Test::Deep;
7 | use Test::More;
8 | # use Storable qw/dclone/;
9 |
10 | use GraphQL::Language::Parser qw/parse/;
11 | use GraphQL::Language::Printer qw/print_doc/;
12 |
13 | open my $fh, '<:encoding(UTF-8)', "$Bin/kitchen-sink.graphql" or BAIL_OUT($!);
14 | my $kitchen_sink = join '', <$fh>;
15 | close $fh;
16 |
17 | # subtest 'does not alter ast' => sub {
18 | # my $ast = parse($kitchen_sink);
19 | # my $ast_before = dclone($ast);
20 | # print_doc($ast);
21 | # cmp_deeply $ast, $ast_before;
22 | # };
23 |
24 | subtest 'prints minimal ast' => sub {
25 | my $ast = { kind => 'Field', name => { kind => 'Name', value => 'foo' } };
26 | is print_doc($ast), 'foo';
27 | };
28 |
29 | subtest 'produces helpful error messages' => sub {
30 | my $bad_ast1 = { random => 'Data' };
31 | eval { print_doc($bad_ast1) };
32 | is $@, "Invalid AST Node: {'random' => 'Data'}\n";
33 | };
34 |
35 | subtest 'correctly prints non-query operations without name' => sub {
36 | my $query_ast_shorthanded = parse('query { id, name }');
37 | is print_doc($query_ast_shorthanded), <<'EOS';
38 | {
39 | id
40 | name
41 | }
42 | EOS
43 |
44 | my $mutation_ast = parse('mutation { id, name }');
45 | is print_doc($mutation_ast), <<'EOS';
46 | mutation {
47 | id
48 | name
49 | }
50 | EOS
51 |
52 | my $query_ast_with_artifacts = parse(
53 | 'query ($foo: TestType) @testDirective { id, name }'
54 | );
55 | is print_doc($query_ast_with_artifacts), <<'EOS';
56 | query ($foo: TestType) @testDirective {
57 | id
58 | name
59 | }
60 | EOS
61 |
62 | my $mutation_ast_with_artifacts = parse(
63 | 'mutation ($foo: TestType) @testDirective { id, name }'
64 | );
65 | is print_doc($mutation_ast_with_artifacts), <<'EOS';
66 | mutation ($foo: TestType) @testDirective {
67 | id
68 | name
69 | }
70 | EOS
71 | };
72 |
73 | subtest 'prints kitchen sink' => sub {
74 | my $ast = parse($kitchen_sink);
75 | my $printed = print_doc($ast);
76 | is $printed, <<'EOS'
77 | query queryName($foo: ComplexType, $site: Site = MOBILE) {
78 | whoever123is: node(id: [123, 456]) {
79 | id
80 | ... on User @defer {
81 | field2 {
82 | id
83 | alias: field1(first: 10, after: $foo) @include(if: $foo) {
84 | id
85 | ...frag
86 | }
87 | }
88 | }
89 | ... @skip(unless: $foo) {
90 | id
91 | }
92 | ... {
93 | id
94 | }
95 | }
96 | }
97 |
98 | mutation likeStory {
99 | like(story: 123) @defer {
100 | story {
101 | id
102 | }
103 | }
104 | }
105 |
106 | subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
107 | storyLikeSubscribe(input: $input) {
108 | story {
109 | likers {
110 | count
111 | }
112 | likeSentence {
113 | text
114 | }
115 | }
116 | }
117 | }
118 |
119 | fragment frag on Friend {
120 | foo(size: $size, bar: $b, obj: {key: "value"})
121 | }
122 |
123 | {
124 | unnamed(truthy: true, falsey: false, nullish: null)
125 | query
126 | }
127 | EOS
128 | };
129 |
130 | done_testing;
131 |
--------------------------------------------------------------------------------
/lib/GraphQL/Language/Token.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Language::Token;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use constant {
7 | SOF => '',
8 | EOF => '',
9 | BANG => '!',
10 | DOLLAR => '$',
11 | PAREN_L => '(',
12 | PAREN_R => ')',
13 | SPREAD => '...',
14 | COLON => ':',
15 | EQUALS => '=',
16 | AT => '@',
17 | BRACKET_L => '[',
18 | BRACKET_R => ']',
19 | BRACE_L => '{',
20 | PIPE => '|',
21 | BRACE_R => '}',
22 | NAME => 'Name',
23 | INT => 'Int',
24 | FLOAT => 'Float',
25 | STRING => 'String',
26 | COMMENT => 'Comment',
27 | };
28 |
29 | use Exporter qw/import/;
30 |
31 | our @EXPORT_OK = (qw/
32 | TokenKind
33 |
34 | SOF
35 | EOF
36 | BANG
37 | DOLLAR
38 | PAREN_L
39 | PAREN_R
40 | SPREAD
41 | COLON
42 | EQUALS
43 | AT
44 | BRACKET_L
45 | BRACKET_R
46 | BRACE_L
47 | PIPE
48 | BRACE_R
49 | NAME
50 | INT
51 | FLOAT
52 | STRING
53 | COMMENT
54 | /);
55 |
56 | our %EXPORT_TAGS = (
57 | kinds => [qw/
58 | SOF
59 | EOF
60 | BANG
61 | DOLLAR
62 | PAREN_L
63 | PAREN_R
64 | SPREAD
65 | COLON
66 | EQUALS
67 | AT
68 | BRACKET_L
69 | BRACKET_R
70 | BRACE_L
71 | PIPE
72 | BRACE_R
73 | NAME
74 | INT
75 | FLOAT
76 | STRING
77 | COMMENT
78 | /],
79 | );
80 |
81 | sub TokenKind { __PACKAGE__ }
82 |
83 | sub new {
84 | my ($class, %args) = @_;
85 | return bless {
86 | # The kind of Token.
87 | kind => undef,
88 |
89 | # The character offset at which this Node begins.
90 | start => undef,
91 |
92 | # The character offset at which this Node ends.
93 | end => undef,
94 |
95 | # The 1-indexed line number on which this Token appears.
96 | line => undef,
97 |
98 | # The 1-indexed column number at which this Token begins.
99 | column => undef,
100 |
101 | # For non-punctuation tokens, represents the interpreted value of the token.
102 | value => undef,
103 |
104 | # Tokens exist as nodes in a double-linked-list amongst all tokens
105 | # including ignored tokens. is always the first node and
106 | # the last.
107 | prev => undef,
108 | next => undef,
109 |
110 | %args,
111 | }, $class;
112 | }
113 |
114 | sub kind { shift->{kind} }
115 |
116 | sub start { shift->{start} }
117 | sub end { shift->{end} }
118 |
119 | sub line { shift->{line} }
120 | sub column { shift->{column} }
121 |
122 | sub value { shift->{value} }
123 |
124 | sub prev { shift->{prev} }
125 | sub next { shift->{next} }
126 |
127 | sub inspect {
128 | my $self = shift;
129 | return {
130 | kind => $self->kind,
131 | value => $self->value,
132 | line => $self->line,
133 | column => $self->column,
134 | };
135 | }
136 |
137 | sub desc {
138 | my $self = shift;
139 | return $self->value ? qq`$self->{kind} "$self->{value}"` : $self->kind;
140 | }
141 |
142 | 1;
143 |
144 | __END__
145 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/unique_directives_per_location.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub duplicate_directive {
15 | my ($directiveName, $l1, $c1, $l2, $c2) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::UniqueDirectivesPerLocation::duplicate_directive_message($directiveName),
18 | locations => [{ line => $l1, column => $c1 }, { line => $l2, column => $c2 }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'no directives' => sub {
24 | expect_passes_rule('UniqueDirectivesPerLocation', '
25 | fragment Test on Type {
26 | field
27 | }
28 | ');
29 | };
30 |
31 | subtest 'unique directives in different locations' => sub {
32 | expect_passes_rule('UniqueDirectivesPerLocation', '
33 | fragment Test on Type @directiveA {
34 | field @directiveB
35 | }
36 | ');
37 | };
38 |
39 | subtest 'unique directives in same locations' => sub {
40 | expect_passes_rule('UniqueDirectivesPerLocation', '
41 | fragment Test on Type @directiveA @directiveB {
42 | field @directiveA @directiveB
43 | }
44 | ');
45 | };
46 |
47 | subtest 'same directives in different locations' => sub {
48 | expect_passes_rule('UniqueDirectivesPerLocation', '
49 | fragment Test on Type @directiveA {
50 | field @directiveA
51 | }
52 | ');
53 | };
54 |
55 | subtest 'same directives in similar locations' => sub {
56 | expect_passes_rule('UniqueDirectivesPerLocation', '
57 | fragment Test on Type {
58 | field @directive
59 | field @directive
60 | }
61 | ');
62 | };
63 |
64 | subtest 'duplicate directives in one location' => sub {
65 | expect_fails_rule('UniqueDirectivesPerLocation', '
66 | fragment Test on Type {
67 | field @directive @directive
68 | }
69 | ', [
70 | duplicate_directive('directive', 3, 15, 3, 26)
71 | ]);
72 | };
73 |
74 | subtest 'many duplicate directives in one location' => sub {
75 | expect_fails_rule('UniqueDirectivesPerLocation', '
76 | fragment Test on Type {
77 | field @directive @directive @directive
78 | }
79 | ', [
80 | duplicate_directive('directive', 3, 15, 3, 26),
81 | duplicate_directive('directive', 3, 15, 3, 37)
82 | ]);
83 | };
84 |
85 | subtest 'different duplicate directives in one location' => sub {
86 | expect_fails_rule('UniqueDirectivesPerLocation', '
87 | fragment Test on Type {
88 | field @directiveA @directiveB @directiveA @directiveB
89 | }
90 | ', [
91 | duplicate_directive('directiveA', 3, 15, 3, 39),
92 | duplicate_directive('directiveB', 3, 27, 3, 51)
93 | ]);
94 | };
95 |
96 | subtest 'duplicate directives in many locations' => sub {
97 | expect_fails_rule('UniqueDirectivesPerLocation', '
98 | fragment Test on Type @directive @directive {
99 | field @directive @directive
100 | }
101 | ', [
102 | duplicate_directive('directive', 2, 29, 2, 40),
103 | duplicate_directive('directive', 3, 15, 3, 26)
104 | ]);
105 | };
106 |
107 | done_testing;
108 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/scalar_leafs.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub no_scalar_subselection {
15 | my ($field, $type, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::ScalarLeafs::no_subselection_allowed_message($field, $type),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | sub missing_obj_subselection {
24 | my ($field, $type, $line, $column) = @_;
25 | return {
26 | message => GraphQL::Validator::Rule::ScalarLeafs::required_subselection_message($field, $type),
27 | locations => [{ line => $line, column => $column }],
28 | path => undef,
29 | };
30 | }
31 |
32 | subtest 'valid scalar selection' => sub {
33 | expect_passes_rule('ScalarLeafs', '
34 | fragment scalarSelection on Dog {
35 | barks
36 | }
37 | ');
38 | };
39 |
40 | subtest 'object type missing selection' => sub {
41 | expect_fails_rule('ScalarLeafs', '
42 | query directQueryOnObjectWithoutSubFields {
43 | human
44 | }
45 | ', [ missing_obj_subselection('human', 'Human', 3, 9) ]);
46 | };
47 |
48 | subtest 'interface type missing selection' => sub {
49 | expect_fails_rule('ScalarLeafs', '
50 | {
51 | human { pets }
52 | }
53 | ', [ missing_obj_subselection('pets', '[Pet]', 3, 17) ]);
54 | };
55 |
56 | subtest 'valid scalar selection with args' => sub {
57 | expect_passes_rule('ScalarLeafs', '
58 | fragment scalarSelectionWithArgs on Dog {
59 | doesKnowCommand(dogCommand: SIT)
60 | }
61 | ');
62 | };
63 |
64 | subtest 'scalar selection not allowed on Boolean' => sub {
65 | expect_fails_rule('ScalarLeafs', '
66 | fragment scalarSelectionsNotAllowedOnBoolean on Dog {
67 | barks { sinceWhen }
68 | }
69 | ',
70 | [ no_scalar_subselection('barks', 'Boolean', 3, 15) ] );
71 | };
72 |
73 | subtest 'scalar selection not allowed on Enum' => sub {
74 | expect_fails_rule('ScalarLeafs', '
75 | fragment scalarSelectionsNotAllowedOnEnum on Cat {
76 | furColor { inHexdec }
77 | }
78 | ',
79 | [ no_scalar_subselection('furColor', 'FurColor', 3, 18) ] );
80 | };
81 |
82 | subtest 'scalar selection not allowed with args' => sub {
83 | expect_fails_rule('ScalarLeafs', '
84 | fragment scalarSelectionsNotAllowedWithArgs on Dog {
85 | doesKnowCommand(dogCommand: SIT) { sinceWhen }
86 | }
87 | ',
88 | [ no_scalar_subselection('doesKnowCommand', 'Boolean', 3, 42) ] );
89 | };
90 |
91 | subtest 'Scalar selection not allowed with directives' => sub {
92 | expect_fails_rule('ScalarLeafs', '
93 | fragment scalarSelectionsNotAllowedWithDirectives on Dog {
94 | name @include(if: true) { isAlsoHumanName }
95 | }
96 | ',
97 | [ no_scalar_subselection('name', 'String', 3, 33) ] );
98 | };
99 |
100 | subtest 'Scalar selection not allowed with directives and args' => sub {
101 | expect_fails_rule('ScalarLeafs', '
102 | fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog {
103 | doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen }
104 | }
105 | ',
106 | [ no_scalar_subselection('doesKnowCommand', 'Boolean', 3, 61) ] );
107 | };
108 |
109 | done_testing;
110 |
--------------------------------------------------------------------------------
/lib/GraphQL/Type/Directive.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Type::Directive;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use constant {
7 | # Operations
8 | QUERY => 'QUERY',
9 | MUTATION => 'MUTATION',
10 | SUBSCRIPTION => 'SUBSCRIPTION',
11 | FIELD => 'FIELD',
12 | FRAGMENT_DEFINITION => 'FRAGMENT_DEFINITION',
13 | FRAGMENT_SPREAD => 'FRAGMENT_SPREAD',
14 | INLINE_FRAGMENT => 'INLINE_FRAGMENT',
15 | # Schema Definitions
16 | SCHEMA => 'SCHEMA',
17 | SCALAR => 'SCALAR',
18 | OBJECT => 'OBJECT',
19 | FIELD_DEFINITION => 'FIELD_DEFINITION',
20 | ARGUMENT_DEFINITION => 'ARGUMENT_DEFINITION',
21 | INTERFACE => 'INTERFACE',
22 | UNION => 'UNION',
23 | ENUM => 'ENUM',
24 | ENUM_VALUE => 'ENUM_VALUE',
25 | INPUT_OBJECT => 'INPUT_OBJECT',
26 | INPUT_FIELD_DEFINITION => 'INPUT_FIELD_DEFINITION',
27 | };
28 |
29 | # use Exporter qw/import/;
30 |
31 | # our @EXPORT_OK = qw/
32 | # QUERY
33 | # MUTATION
34 | # SUBSCRIPTION
35 | # FIELD
36 | # FRAGMENT_DEFINITION
37 | # FRAGMENT_SPREAD
38 | # INLINE_FRAGMENT
39 | # SCHEMA
40 | # SCALAR
41 | # OBJECT
42 | # FIELD_DEFINITION
43 | # ARGUMENT_DEFINITION
44 | # INTERFACE
45 | # UNION
46 | # ENUM
47 | # ENUM_VALUE
48 | # INPUT_OBJECT
49 | # INPUT_FIELD_DEFINITION
50 | # /;
51 |
52 | # our %EXPORT_TAGS = (
53 | # DirectiveLocation => [qw/
54 | # QUERY
55 | # MUTATION
56 | # SUBSCRIPTION
57 | # FIELD
58 | # FRAGMENT_DEFINITION
59 | # FRAGMENT_SPREAD
60 | # INLINE_FRAGMENT
61 | # SCHEMA
62 | # SCALAR
63 | # OBJECT
64 | # FIELD_DEFINITION
65 | # ARGUMENT_DEFINITION
66 | # INTERFACE
67 | # UNION
68 | # ENUM
69 | # ENUM_VALUE
70 | # INPUT_OBJECT
71 | # INPUT_FIELD_DEFINITION
72 | # /],
73 | # );
74 |
75 | use GraphQL::Util qw/assert_valid_name/;
76 | use GraphQL::Util::Type qw/is_input_type/;
77 |
78 | sub name { shift->{name} }
79 | sub description { shift->{description} }
80 | sub locations { shift->{locations} }
81 | sub args { shift->{args} }
82 |
83 | sub new {
84 | my ($class, %config) = @_;
85 |
86 | die "Directive must be named.\n" unless $config{name};
87 | assert_valid_name($config{name});
88 |
89 | die "Must provide locations for directive.\n"
90 | if ref($config{locations}) ne 'ARRAY';
91 |
92 | my $self = bless {
93 | name => $config{name},
94 | description => $config{description},
95 | locations => $config{locations},
96 |
97 | args => [],
98 | }, $class;
99 |
100 | my $args = $config{args};
101 | if ($args) {
102 | die "\@$config{name} args must be an object with argument names as keys.\n"
103 | if ref($args) ne 'HASH';
104 |
105 | for my $arg_name (keys %$args) {
106 | assert_valid_name($arg_name);
107 |
108 | my $arg = $args->{ $arg_name };
109 | die "\@$config{name}($arg_name:) argument type must be "
110 | . "Input Type but got: ${ \$arg->{type}->to_string }.\n" unless is_input_type($arg->{type});
111 |
112 | push @{ $self->{args} }, {
113 | name => $arg_name,
114 | description => $arg->{description},
115 | type => $arg->{type},
116 | default_value => $arg->{default_value},
117 | };
118 | }
119 | }
120 |
121 | return $self;
122 | }
123 |
124 | 1;
125 |
126 | __END__
127 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/unique_argument_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub duplicate_arg {
15 | my ($arg_name, $l1, $c1, $l2, $c2) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::UniqueArgumentNames::duplicate_arg_message($arg_name),
18 | locations => [{ line => $l1, column => $c1 }, { line => $l2, column => $c2 }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'no arguments on field' => sub {
24 | expect_passes_rule('UniqueArgumentNames', '
25 | {
26 | field
27 | }
28 | ');
29 | };
30 |
31 | subtest 'no arguments on directive' => sub {
32 | expect_passes_rule('UniqueArgumentNames', '
33 | {
34 | field @directive
35 | }
36 | ');
37 | };
38 |
39 | subtest 'argument on field' => sub {
40 | expect_passes_rule('UniqueArgumentNames', '
41 | {
42 | field(arg: "value")
43 | }
44 | ');
45 | };
46 |
47 | subtest 'argument on directive' => sub {
48 | expect_passes_rule('UniqueArgumentNames', '
49 | {
50 | field @directive(arg: "value")
51 | }
52 | ');
53 | };
54 |
55 | subtest 'same argument on two fields' => sub {
56 | expect_passes_rule('UniqueArgumentNames', '
57 | {
58 | one: field(arg: "value")
59 | two: field(arg: "value")
60 | }
61 | ');
62 | };
63 |
64 | subtest 'same argument on field and directive' => sub {
65 | expect_passes_rule('UniqueArgumentNames', '
66 | {
67 | field(arg: "value") @directive(arg: "value")
68 | }
69 | ');
70 | };
71 |
72 | subtest 'same argument on two directives' => sub {
73 | expect_passes_rule('UniqueArgumentNames', '
74 | {
75 | field @directive1(arg: "value") @directive2(arg: "value")
76 | }
77 | ');
78 | };
79 |
80 | subtest 'multiple field arguments' => sub {
81 | expect_passes_rule('UniqueArgumentNames', '
82 | {
83 | field(arg1: "value", arg2: "value", arg3: "value")
84 | }
85 | ');
86 | };
87 |
88 | subtest 'multiple directive arguments' => sub {
89 | expect_passes_rule('UniqueArgumentNames', '
90 | {
91 | field @directive(arg1: "value", arg2: "value", arg3: "value")
92 | }
93 | ');
94 | };
95 |
96 | subtest 'duplicate field arguments' => sub {
97 | expect_fails_rule('UniqueArgumentNames', '
98 | {
99 | field(arg1: "value", arg1: "value")
100 | }
101 | ', [
102 | duplicate_arg('arg1', 3, 15, 3, 30)
103 | ]);
104 | };
105 |
106 | subtest 'many duplicate field arguments' => sub {
107 | expect_fails_rule('UniqueArgumentNames', '
108 | {
109 | field(arg1: "value", arg1: "value", arg1: "value")
110 | }
111 | ', [
112 | duplicate_arg('arg1', 3, 15, 3, 30),
113 | duplicate_arg('arg1', 3, 15, 3, 45)
114 | ]);
115 | };
116 |
117 | subtest 'duplicate directive arguments' => sub {
118 | expect_fails_rule('UniqueArgumentNames', '
119 | {
120 | field @directive(arg1: "value", arg1: "value")
121 | }
122 | ', [
123 | duplicate_arg('arg1', 3, 26, 3, 41)
124 | ]);
125 | };
126 |
127 | subtest 'many duplicate directive arguments' => sub {
128 | expect_fails_rule('UniqueArgumentNames', '
129 | {
130 | field @directive(arg1: "value", arg1: "value", arg1: "value")
131 | }
132 | ', [
133 | duplicate_arg('arg1', 3, 26, 3, 41),
134 | duplicate_arg('arg1', 3, 26, 3, 56)
135 | ]);
136 | };
137 |
138 | done_testing;
139 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/ProvidedNonNullArguments.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::ProvidedNonNullArguments;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Util qw/
8 | stringify_type
9 | key_map
10 | /;
11 | use GraphQL::Language::Visitor qw/FALSE/;
12 |
13 | sub missing_field_arg_message {
14 | my ($field_name, $arg_name, $type) = @_;
15 | return qq`Field "$field_name" argument "$arg_name" of type `
16 | . qq`"${ stringify_type($type) }" is required but not provided.`;
17 | }
18 |
19 | sub missing_directive_arg_message {
20 | my ($directive_name, $arg_name, $type) = @_;
21 | return qq`Directive "\@$directive_name" argument "$arg_name" of type `
22 | . qq`"${ stringify_type($type) }" is required but not provided.`;
23 | }
24 |
25 | # Provided required arguments
26 | #
27 | # A field or directive is only valid if all required (non-null) field arguments
28 | # have been provided.
29 | sub validate {
30 | my ($self, $context) = @_;
31 | return {
32 | Field => {
33 | # Validate on leave to allow for deeper errors to appear first->
34 | leave => sub {
35 | my (undef, $node) = @_;
36 |
37 | my $field_def = $context->get_field_def;
38 | if (!$field_def) {
39 | return FALSE;
40 | }
41 |
42 | my $arg_nodes = $node->{arguments} || [];
43 | my $arg_node_map = key_map($arg_nodes, sub { $_[0]->{name}{value} });
44 |
45 | for my $arg_def (@{ $field_def->{args} }) {
46 | my $arg_node = $arg_node_map->{ $arg_def->{name} };
47 | if (!$arg_node && $arg_def->{type}->isa('GraphQL::Type::NonNull')) {
48 | $context->report_error(
49 | GraphQLError(
50 | missing_field_arg_message(
51 | $node->{name}{value},
52 | $arg_def->{name},
53 | $arg_def->{type}
54 | ),
55 | [$node]
56 | )
57 | );
58 | }
59 | };
60 |
61 | return; # void
62 | }
63 | },
64 | Directive => {
65 | # Validate on leave to allow for deeper errors to appear first.
66 | leave => sub {
67 | my (undef, $node) = @_;
68 |
69 | my $directive_def = $context->get_directive;
70 | if (!$directive_def) {
71 | return FALSE;
72 | }
73 |
74 | my $arg_nodes = $node->{arguments} || [];
75 | my $arg_node_map = key_map($arg_nodes, sub { $_[0]->{name}{value} });
76 |
77 | for my $arg_def (@{ $directive_def->{args} }) {
78 | my $arg_node = $arg_node_map->{ $arg_def->{name} };
79 | if (!$arg_node && $arg_def->{type}->isa('GraphQL::Type::NonNull')) {
80 | $context->report_error(
81 | GraphQLError(
82 | missing_directive_arg_message(
83 | $node->{name}{value},
84 | $arg_def->{name},
85 | $arg_def->{type}
86 | ),
87 | [$node]
88 | )
89 | );
90 | }
91 | };
92 |
93 | return; # void
94 | }
95 | },
96 | };
97 | }
98 |
99 | 1;
100 |
101 | __END__
102 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/no_unused_fragments.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub unused_frag {
15 | my ($frag_name, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::NoUnusedFragments::unused_frag_message($frag_name),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'all fragment names are used' => sub {
24 | expect_passes_rule('NoUnusedFragments', '
25 | {
26 | human(id: 4) {
27 | ...HumanFields1
28 | ... on Human {
29 | ...HumanFields2
30 | }
31 | }
32 | }
33 | fragment HumanFields1 on Human {
34 | name
35 | ...HumanFields3
36 | }
37 | fragment HumanFields2 on Human {
38 | name
39 | }
40 | fragment HumanFields3 on Human {
41 | name
42 | }
43 | ');
44 | };
45 |
46 | subtest 'all fragment names are used by multiple operations' => sub {
47 | expect_passes_rule('NoUnusedFragments', '
48 | query Foo {
49 | human(id: 4) {
50 | ...HumanFields1
51 | }
52 | }
53 | query Bar {
54 | human(id: 4) {
55 | ...HumanFields2
56 | }
57 | }
58 | fragment HumanFields1 on Human {
59 | name
60 | ...HumanFields3
61 | }
62 | fragment HumanFields2 on Human {
63 | name
64 | }
65 | fragment HumanFields3 on Human {
66 | name
67 | }
68 | ');
69 | };
70 |
71 | subtest 'contains unknown fragments' => sub {
72 | expect_fails_rule('NoUnusedFragments', '
73 | query Foo {
74 | human(id: 4) {
75 | ...HumanFields1
76 | }
77 | }
78 | query Bar {
79 | human(id: 4) {
80 | ...HumanFields2
81 | }
82 | }
83 | fragment HumanFields1 on Human {
84 | name
85 | ...HumanFields3
86 | }
87 | fragment HumanFields2 on Human {
88 | name
89 | }
90 | fragment HumanFields3 on Human {
91 | name
92 | }
93 | fragment Unused1 on Human {
94 | name
95 | }
96 | fragment Unused2 on Human {
97 | name
98 | }
99 | ', [
100 | unused_frag('Unused1', 22, 7),
101 | unused_frag('Unused2', 25, 7),
102 | ]);
103 | };
104 |
105 | subtest 'contains unknown fragments with ref cycle' => sub {
106 | expect_fails_rule('NoUnusedFragments', '
107 | query Foo {
108 | human(id: 4) {
109 | ...HumanFields1
110 | }
111 | }
112 | query Bar {
113 | human(id: 4) {
114 | ...HumanFields2
115 | }
116 | }
117 | fragment HumanFields1 on Human {
118 | name
119 | ...HumanFields3
120 | }
121 | fragment HumanFields2 on Human {
122 | name
123 | }
124 | fragment HumanFields3 on Human {
125 | name
126 | }
127 | fragment Unused1 on Human {
128 | name
129 | ...Unused2
130 | }
131 | fragment Unused2 on Human {
132 | name
133 | ...Unused1
134 | }
135 | ', [
136 | unused_frag('Unused1', 22, 7),
137 | unused_frag('Unused2', 26, 7),
138 | ]);
139 | };
140 |
141 | subtest 'contains unknown and undef fragments' => sub {
142 | expect_fails_rule('NoUnusedFragments', '
143 | query Foo {
144 | human(id: 4) {
145 | ...bar
146 | }
147 | }
148 | fragment foo on Human {
149 | name
150 | }
151 | ', [
152 | unused_frag('foo', 7, 7),
153 | ]);
154 | };
155 |
156 | done_testing;
157 |
--------------------------------------------------------------------------------
/t/graphql/execute-mutations.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 | use Test::Deep;
7 | use JSON qw/encode_json/;
8 |
9 | use GraphQL qw/:types/;
10 | use GraphQL::Language::Parser qw/parse/;
11 | use GraphQL::Execute qw/execute/;
12 |
13 | {
14 | package NumberHolder;
15 | sub theNumber { shift->{the_number} };
16 |
17 | sub new {
18 | my ($class, $original_number) = @_;
19 | bless { the_number => $original_number }, $class;
20 | }
21 |
22 | package Root;
23 | sub number_holder { shift->{number_holder} };
24 |
25 | sub new {
26 | my ($class, $original_number) = @_;
27 | bless {
28 | number_holder => NumberHolder->new($original_number),
29 | }, $class;
30 | }
31 |
32 | sub immediately_change_the_number {
33 | my ($self, $new_number) = @_;
34 | $self->{number_holder}{the_number} = $new_number;
35 | return $self->number_holder;
36 | }
37 |
38 | sub fail_to_change_the_number {
39 | die "Cannot change the number\n";
40 | }
41 | }
42 |
43 | my $numberHolderType = GraphQLObjectType(
44 | fields => {
45 | theNumber => { type => GraphQLInt },
46 | },
47 | name => 'NumberHolder',
48 | );
49 |
50 | my $schema = GraphQLSchema(
51 | query => GraphQLObjectType(
52 | name => 'Query',
53 | fields => {
54 | numberHolder => { type => $numberHolderType },
55 | },
56 | ),
57 | mutation => GraphQLObjectType(
58 | name => 'Mutation',
59 | fields => {
60 | immediatelyChangeTheNumber => {
61 | type => $numberHolderType,
62 | args => { newNumber => { type => GraphQLInt } },
63 | resolve => sub {
64 | my ($obj, $args) = @_;
65 | return $obj->immediately_change_the_number($args->{newNumber});
66 | },
67 | },
68 | failToChangeTheNumber => {
69 | type => $numberHolderType,
70 | args => { newNumber => { type => GraphQLInt } },
71 | resolve => sub {
72 | my ($obj, $args) = @_;
73 | return $obj->fail_to_change_the_number($args->{newNumber});
74 | },
75 | },
76 | },
77 | ),
78 | );
79 |
80 | subtest 'evaluates mutations serially' => sub {
81 | my $doc = <<'EOQ';
82 | mutation M {
83 | first: immediatelyChangeTheNumber(newNumber: 1) {
84 | theNumber
85 | },
86 | third: immediatelyChangeTheNumber(newNumber: 3) {
87 | theNumber
88 | }
89 | fifth: immediatelyChangeTheNumber(newNumber: 5) {
90 | theNumber
91 | }
92 | }
93 | EOQ
94 |
95 | my $result = execute($schema, parse($doc), Root->new(6));
96 | is_deeply $result, {
97 | data => {
98 | first => { theNumber => 1 },
99 | third => { theNumber => 3 },
100 | fifth => { theNumber => 5 },
101 | },
102 | };
103 | };
104 |
105 | subtest 'evaluates mutations correctly in the presence of a failed mutation' => sub {
106 | my $doc = <<'EOQ';
107 | mutation M {
108 | first: immediatelyChangeTheNumber(newNumber: 1) {
109 | theNumber
110 | },
111 | third: failToChangeTheNumber(newNumber: 3) {
112 | theNumber
113 | }
114 | fifth: immediatelyChangeTheNumber(newNumber: 5) {
115 | theNumber
116 | }
117 | }
118 | EOQ
119 |
120 | my $result = execute($schema, parse($doc), Root->new(6));
121 | is_deeply $result->{data}, {
122 | first => { theNumber => 1 },
123 | third => undef, # null
124 | fifth => { theNumber => 5 },
125 | };
126 | is scalar(@{ $result->{errors} }), 1;
127 | is $result->{errors}[0]{message}, "Cannot change the number\n";
128 | is_deeply $result->{errors}[0]{locations}, [{ line => 5, column => 3 }];
129 | };
130 |
131 | done_testing;
132 |
--------------------------------------------------------------------------------
/lib/GraphQL/Error.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Error;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Language::Location qw/get_location/;
7 |
8 | use Carp qw/longmess/;
9 | use DDP;
10 | use Exporter qw/import/;
11 |
12 | our @EXPORT_OK = (qw/
13 | GraphQLError
14 |
15 | format_error
16 | located_error
17 | syntax_error
18 | /);
19 |
20 | sub GraphQLError {
21 | my ($message, $nodes, $source, $positions, $path, $original_error) = @_;
22 |
23 | # Compute locations in the source for the given nodes/positions.
24 | my $_source = $source;
25 | if (!$_source && $nodes && @$nodes) {
26 | my $node = $nodes->[0];
27 | $_source = $node && $node->{loc} && $node->{loc}{source};
28 | }
29 |
30 | my $_positions = $positions;
31 | if (!$_positions && $nodes) {
32 | $_positions = [map { $_->{loc}{start} } grep { %{ $_->{loc} } } @$nodes];
33 | }
34 |
35 | if ($_positions && !@$_positions) {
36 | $_positions = undef;
37 | }
38 |
39 | my $_locations;
40 | if ($_source && $_positions) {
41 | $_locations = [map { get_location($_source, $_) } @$_positions];
42 | }
43 |
44 | return bless {
45 | message => $message,
46 | locations => $_locations,
47 | path => $path,
48 | nodes => $nodes,
49 | source => $_source,
50 | positions => $_positions,
51 | original_error => $original_error,
52 | }, __PACKAGE__;
53 | }
54 |
55 | sub format_error {
56 | my $error = shift;
57 | die "Received null or undefined error.\n" unless $error;
58 | return {
59 | message => $error->{message},
60 | locations => $error->{locations},
61 | path => $error->{path},
62 | };
63 | }
64 |
65 | # Given an arbitrary Error, presumably thrown while attempting to execute a
66 | # GraphQL operation, produce a new GraphQLError aware of the location in the
67 | # document responsible for the original Error.
68 | sub located_error {
69 | my ($original_error, $nodes, $path) = @_;
70 |
71 | # warn longmess 'located error';
72 |
73 | # Note: this uses a brand-check to support GraphQL errors originating from
74 | # other contexts.
75 | if ($original_error && $original_error->{path}) {
76 | return $original_error;
77 | }
78 |
79 | my $message =
80 | $original_error
81 | ? $original_error->{message} || String($original_error)
82 | : 'An unknown error occurred.';
83 |
84 | return GraphQLError(
85 | $message,
86 | $original_error && $original_error->{nodes} || $nodes,
87 | $original_error && $original_error->{source},
88 | $original_error && $original_error->{positions},
89 | $path,
90 | $original_error
91 | );
92 | }
93 |
94 | sub syntax_error {
95 | my ($source, $position, $description) = @_;
96 | my $location = get_location($source, $position);
97 |
98 | my $error = sprintf "Syntax Error %s (%d:%d) %s\n\n%s",
99 | $source->name, $location->{line}, $location->{column}, $description,
100 | _highlight_source_at_location($source, $location);
101 |
102 | return $error;
103 | }
104 |
105 | sub _highlight_source_at_location {
106 | my ($source, $location) = @_;
107 |
108 | my $line = $location->{line};
109 |
110 | my $prev_line_num = $line - 1;
111 | my $line_num = $line;
112 | my $next_line_num = $line + 1;
113 |
114 | my $pad_len = length($next_line_num);
115 |
116 | my @lines = split /\n/, $source->body, -1;
117 |
118 | return
119 | ($line >= 2
120 | ? _lpad($pad_len, $prev_line_num) . ': ' . $lines[$line - 2] . "\n" : '')
121 | . _lpad($pad_len, $line_num) . ': ' . $lines[$line - 1] . "\n"
122 | . (join ' ', ('') x (2+$pad_len+$location->{column})) . "^\n"
123 | . ($line < scalar(@lines)
124 | ? _lpad($pad_len, $next_line_num) . ': ' . $lines[$line] . "\n" : '');
125 | }
126 |
127 | sub _lpad {
128 | my ($len, $str) = @_;
129 | return (join ' ', ('') x ($len-length($str)+1)) . $str;
130 | }
131 |
132 | 1;
133 |
134 | __END__
135 |
--------------------------------------------------------------------------------
/lib/GraphQL/Language/Kinds.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Language::Kinds;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use constant {
7 | # Name
8 | NAME => 'Name',
9 |
10 | # Document
11 | DOCUMENT => 'Document',
12 | OPERATION_DEFINITION => 'OperationDefinition',
13 | VARIABLE_DEFINITION => 'VariableDefinition',
14 | VARIABLE => 'Variable',
15 | SELECTION_SET => 'SelectionSet',
16 | FIELD => 'Field',
17 | ARGUMENT => 'Argument',
18 |
19 | # Fragments
20 | FRAGMENT_SPREAD => 'FragmentSpread',
21 | INLINE_FRAGMENT => 'InlineFragment',
22 | FRAGMENT_DEFINITION => 'FragmentDefinition',
23 |
24 | # Values
25 | INT => 'IntValue',
26 | FLOAT => 'FloatValue',
27 | STRING => 'StringValue',
28 | BOOLEAN => 'BooleanValue',
29 | NULL => 'NullValue',
30 | ENUM => 'EnumValue',
31 | LIST => 'ListValue',
32 | OBJECT => 'ObjectValue',
33 | OBJECT_FIELD => 'ObjectField',
34 |
35 | # Directives
36 | DIRECTIVE => 'Directive',
37 |
38 | # Types
39 | NAMED_TYPE => 'NamedType',
40 | LIST_TYPE => 'ListType',
41 | NON_NULL_TYPE => 'NonNullType',
42 |
43 | # Type System Definitions
44 | SCHEMA_DEFINITION => 'SchemaDefinition',
45 | OPERATION_TYPE_DEFINITION => 'OperationTypeDefinition',
46 |
47 | # Type Definitions
48 | SCALAR_TYPE_DEFINITION => 'ScalarTypeDefinition',
49 | OBJECT_TYPE_DEFINITION => 'ObjectTypeDefinition',
50 | FIELD_DEFINITION => 'FieldDefinition',
51 | INPUT_VALUE_DEFINITION => 'InputValueDefinition',
52 | INTERFACE_TYPE_DEFINITION => 'InterfaceTypeDefinition',
53 | UNION_TYPE_DEFINITION => 'UnionTypeDefinition',
54 | ENUM_TYPE_DEFINITION => 'EnumTypeDefinition',
55 | ENUM_VALUE_DEFINITION => 'EnumValueDefinition',
56 | INPUT_OBJECT_TYPE_DEFINITION => 'InputObjectTypeDefinition',
57 |
58 | # Type Extensions
59 | TYPE_EXTENSION_DEFINITION => 'TypeExtensionDefinition',
60 |
61 | # Directive Definitions
62 | DIRECTIVE_DEFINITION => 'DirectiveDefinition',
63 | };
64 |
65 | use Exporter qw/import/;
66 |
67 | our @EXPORT_OK = (qw/
68 | Kind
69 |
70 | NAME
71 | DOCUMENT
72 | OPERATION_DEFINITION
73 | VARIABLE_DEFINITION
74 | VARIABLE
75 | SELECTION_SET
76 | FIELD
77 | ARGUMENT
78 | FRAGMENT_SPREAD
79 | INLINE_FRAGMENT
80 | FRAGMENT_DEFINITION
81 | INT
82 | FLOAT
83 | STRING
84 | BOOLEAN
85 | NULL
86 | ENUM
87 | LIST
88 | OBJECT
89 | OBJECT_FIELD
90 | DIRECTIVE
91 | NAMED_TYPE
92 | LIST_TYPE
93 | NON_NULL_TYPE
94 | SCHEMA_DEFINITION
95 | OPERATION_TYPE_DEFINITION
96 | SCALAR_TYPE_DEFINITION
97 | OBJECT_TYPE_DEFINITION
98 | FIELD_DEFINITION
99 | INPUT_VALUE_DEFINITION
100 | INTERFACE_TYPE_DEFINITION
101 | UNION_TYPE_DEFINITION
102 | ENUM_TYPE_DEFINITION
103 | ENUM_VALUE_DEFINITION
104 | INPUT_OBJECT_TYPE_DEFINITION
105 | TYPE_EXTENSION_DEFINITION
106 | DIRECTIVE_DEFINITION
107 | /);
108 |
109 | our %EXPORT_TAGS = (
110 | all => [qw/
111 | NAME
112 | DOCUMENT
113 | OPERATION_DEFINITION
114 | VARIABLE_DEFINITION
115 | VARIABLE
116 | SELECTION_SET
117 | FIELD
118 | ARGUMENT
119 | FRAGMENT_SPREAD
120 | INLINE_FRAGMENT
121 | FRAGMENT_DEFINITION
122 | INT
123 | FLOAT
124 | STRING
125 | BOOLEAN
126 | NULL
127 | ENUM
128 | LIST
129 | OBJECT
130 | OBJECT_FIELD
131 | DIRECTIVE
132 | NAMED_TYPE
133 | LIST_TYPE
134 | NON_NULL_TYPE
135 | SCHEMA_DEFINITION
136 | OPERATION_TYPE_DEFINITION
137 | SCALAR_TYPE_DEFINITION
138 | OBJECT_TYPE_DEFINITION
139 | FIELD_DEFINITION
140 | INPUT_VALUE_DEFINITION
141 | INTERFACE_TYPE_DEFINITION
142 | UNION_TYPE_DEFINITION
143 | ENUM_TYPE_DEFINITION
144 | ENUM_VALUE_DEFINITION
145 | INPUT_OBJECT_TYPE_DEFINITION
146 | TYPE_EXTENSION_DEFINITION
147 | DIRECTIVE_DEFINITION
148 | /],
149 | );
150 |
151 | sub Kind { __PACKAGE__ }
152 |
153 | 1;
154 |
155 | __END__
156 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/KnownArgumentNames.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::KnownArgumentNames;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Language::Kinds qw/Kind/;
8 | use GraphQL::Language::Parser;
9 | use GraphQL::Util qw/
10 | stringify_type
11 | find
12 | suggestion_list
13 | quoted_or_list
14 | /;
15 |
16 | sub unknown_arg_message {
17 | my ($arg_name, $field_name, $type, $suggested_args) = @_;
18 |
19 | my $message = qq`Unknown argument "$arg_name" on field "$field_name" of `
20 | . qq`type "${ stringify_type($type) }".`;
21 |
22 | if ($suggested_args && @$suggested_args) {
23 | $message .= ' Did you mean ' . quoted_or_list($suggested_args) . '?';
24 | }
25 |
26 | return $message;
27 | }
28 |
29 | sub unknown_directive_arg_message {
30 | my ($arg_name, $directive_name, $suggested_args) = @_;
31 |
32 | my $message = qq`Unknown argument "$arg_name" on directive "\@$directive_name".`;
33 |
34 | if ($suggested_args && @$suggested_args) {
35 | $message .= ' Did you mean ' . quoted_or_list($suggested_args) . '?';
36 | }
37 |
38 | return $message;
39 | }
40 |
41 | # Known argument names
42 | #
43 | # A GraphQL field is only valid if all supplied arguments are defined by
44 | # that field.
45 | sub validate {
46 | my ($self, $context) = @_;
47 | return {
48 | Argument => sub {
49 | my (undef, $node, $key, $parent, $path, $ancestors) = @_;
50 | my $argument_of = $ancestors->[scalar(@$ancestors) - 1];
51 |
52 | if ($argument_of->{kind} eq Kind->FIELD) {
53 | my $field_def = $context->get_field_def;
54 | if ($field_def) {
55 | my $field_arg_def = find(
56 | $field_def->{args},
57 | sub { $_[0]->{name} eq $node->{name}{value} }
58 | );
59 |
60 | if (!$field_arg_def) {
61 | my $parent_type = $context->get_parent_type;
62 | die unless $parent_type;
63 |
64 | $context->report_error(
65 | GraphQLError(
66 | unknown_arg_message(
67 | $node->{name}{value},
68 | $field_def->{name},
69 | $parent_type->{name},
70 | suggestion_list(
71 | $node->{name}{value},
72 | [map { $_->{name} } @{ $field_def->{args} }]
73 | )
74 | ),
75 | [$node]
76 | )
77 | );
78 | }
79 | }
80 | }
81 | elsif ($argument_of->{kind} eq Kind->DIRECTIVE) {
82 | my $directive = $context->get_directive;
83 | if ($directive) {
84 | my $directive_arg_def = find(
85 | $directive->{args},
86 | sub { $_[0]->{name} eq $node->{name}{value} }
87 | );
88 |
89 | if (!$directive_arg_def) {
90 | $context->report_error(
91 | GraphQLError(
92 | unknown_directive_arg_message(
93 | $node->{name}{value},
94 | $directive->{name},
95 | suggestion_list(
96 | $node->{name}{value},
97 | [map { $_->{name} } @{ $directive->{args} }]
98 | )
99 | ),
100 | [$node]
101 | )
102 | );
103 | }
104 | }
105 | }
106 |
107 | return; # void
108 | }
109 | }
110 | }
111 |
112 | 1;
113 |
114 | __END__
115 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/known_argument_names.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub unknown_arg {
15 | my ($arg_name, $field_name, $type_name, $suggested_args, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::KnownArgumentNames::unknown_arg_message($arg_name, $field_name, $type_name, $suggested_args),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | sub unknown_directive_arg {
24 | my ($arg_name, $directive_name, $suggested_args, $line, $column) = @_;
25 | return {
26 | message => GraphQL::Validator::Rule::KnownArgumentNames::unknown_directive_arg_message($arg_name, $directive_name, $suggested_args),
27 | locations => [{ line => $line, column => $column }],
28 | path => undef,
29 | };
30 | }
31 |
32 | subtest 'single arg is known' => sub {
33 | expect_passes_rule('KnownArgumentNames', '
34 | fragment argOnRequiredArg on Dog {
35 | doesKnowCommand(dogCommand: SIT)
36 | }
37 | ');
38 | };
39 |
40 | subtest 'multiple args are known' => sub {
41 | expect_passes_rule('KnownArgumentNames', '
42 | fragment multipleArgs on ComplicatedArgs {
43 | multipleReqs(req1: 1, req2: 2)
44 | }
45 | ');
46 | };
47 |
48 | subtest 'ignores args of unknown fields' => sub {
49 | expect_passes_rule('KnownArgumentNames', '
50 | fragment argOnUnknownField on Dog {
51 | unknownField(unknown_arg: SIT)
52 | }
53 | ');
54 | };
55 |
56 | subtest 'multiple args in reverse order are known' => sub {
57 | expect_passes_rule('KnownArgumentNames', '
58 | fragment multipleArgsReverseOrder on ComplicatedArgs {
59 | multipleReqs(req2: 2, req1: 1)
60 | }
61 | ');
62 | };
63 |
64 | subtest 'no args on optional arg' => sub {
65 | expect_passes_rule('KnownArgumentNames', '
66 | fragment noArgOnOptionalArg on Dog {
67 | isHousetrained
68 | }
69 | ');
70 | };
71 |
72 | subtest 'args are known deeply' => sub {
73 | expect_passes_rule('KnownArgumentNames', '
74 | {
75 | dog {
76 | doesKnowCommand(dogCommand: SIT)
77 | }
78 | human {
79 | pet {
80 | ... on Dog {
81 | doesKnowCommand(dogCommand: SIT)
82 | }
83 | }
84 | }
85 | }
86 | ');
87 | };
88 |
89 | subtest 'directive args are known' => sub {
90 | expect_passes_rule('KnownArgumentNames', '
91 | {
92 | dog @skip(if: true)
93 | }
94 | ');
95 | };
96 |
97 | subtest 'undirective args are invalid' => sub {
98 | expect_fails_rule('KnownArgumentNames', '
99 | {
100 | dog @skip(unless: true)
101 | }
102 | ', [
103 | unknown_directive_arg('unless', 'skip', [], 3, 19),
104 | ]);
105 | };
106 |
107 | subtest 'invalid arg name' => sub {
108 | expect_fails_rule('KnownArgumentNames', '
109 | fragment invalidArgName on Dog {
110 | doesKnowCommand(unknown: true)
111 | }
112 | ', [
113 | unknown_arg('unknown', 'doesKnowCommand', 'Dog', [], 3, 25),
114 | ]);
115 | };
116 |
117 | subtest 'unknown args amongst known args' => sub {
118 | expect_fails_rule('KnownArgumentNames', '
119 | fragment oneGoodArgOneInvalidArg on Dog {
120 | doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true)
121 | }
122 | ', [
123 | unknown_arg('whoknows', 'doesKnowCommand', 'Dog', [], 3, 25),
124 | unknown_arg('unknown', 'doesKnowCommand', 'Dog', [], 3, 55),
125 | ]);
126 | };
127 |
128 | subtest 'unknown args deeply' => sub {
129 | expect_fails_rule('KnownArgumentNames', '
130 | {
131 | dog {
132 | doesKnowCommand(unknown: true)
133 | }
134 | human {
135 | pet {
136 | ... on Dog {
137 | doesKnowCommand(unknown: true)
138 | }
139 | }
140 | }
141 | }
142 | ', [
143 | unknown_arg('unknown', 'doesKnowCommand', 'Dog', [], 4, 27),
144 | unknown_arg('unknown', 'doesKnowCommand', 'Dog', [], 9, 31),
145 | ]);
146 | };
147 |
148 | done_testing;
149 |
--------------------------------------------------------------------------------
/t/graphql/execute-nonnull.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use DDP;
6 | use Test::More;
7 | use Test::Deep;
8 | use JSON qw/encode_json/;
9 |
10 | use GraphQL qw/:types/;
11 | use GraphQL::Execute qw/execute/;
12 | use GraphQL::Language::Parser qw/parse/;
13 | use GraphQL::Nullish qw/NULLISH/;
14 |
15 | my $sync_error = bless { message => 'sync' }, 'GraphQL::Error';
16 | my $nonnull_sync_error = bless { message => 'nonNullSync' }, 'GraphQL::Error';
17 |
18 | my $throwing_data;
19 | $throwing_data = {
20 | sync => sub { die $sync_error; },
21 | nonNullSync => sub { die $nonnull_sync_error; },
22 | nest => sub {
23 | return $throwing_data;
24 | },
25 | nonNullNest => sub {
26 | return $throwing_data;
27 | },
28 | };
29 |
30 | my $nulling_data;
31 | $nulling_data = {
32 | sync => sub { NULLISH },
33 | nonNullSync => sub { NULLISH },
34 | nest => sub {
35 | return $nulling_data;
36 | },
37 | nonNullNest => sub {
38 | return $nulling_data;
39 | },
40 | };
41 |
42 | my $data_type;
43 | $data_type = GraphQLObjectType(
44 | name => 'DataType',
45 | fields => sub { {
46 | sync => { type => GraphQLString },
47 | nonNullSync => { type => GraphQLNonNull(GraphQLString) },
48 | nest => { type => $data_type },
49 | nonNullNest => { type => GraphQLNonNull($data_type) },
50 | } },
51 | );
52 | my $schema = GraphQLSchema(
53 | query => $data_type,
54 | );
55 |
56 | subtest 'nulls a nullable field' => sub {
57 | my $doc = <<'EOQ';
58 | query Q {
59 | sync
60 | }
61 | EOQ
62 |
63 | my $ast = parse($doc);
64 |
65 | cmp_deeply execute($schema, $ast, $throwing_data), {
66 | data => {
67 | sync => undef,
68 | },
69 | errors => [noclass(superhashof({
70 | message => $sync_error->{message},
71 | locations => [{ line => 2, column => 9 }]
72 | }))],
73 | };
74 | };
75 |
76 | subtest 'nulls a synchronously returned object that contains a non-nullable field that throws synchronously' => sub {
77 | my $doc = <<'EOQ';
78 | query Q {
79 | nest {
80 | nonNullSync,
81 | }
82 | }
83 | EOQ
84 |
85 | my $ast = parse($doc);
86 |
87 | cmp_deeply execute($schema, $ast, $throwing_data), {
88 | data => {
89 | nest => undef
90 | },
91 | errors => [noclass(superhashof({
92 | message => $nonnull_sync_error->{message},
93 | locations => [{ line => 3, column => 11 }]
94 | }))],
95 | };
96 | };
97 |
98 | subtest 'nulls a nullable field that synchronously returns undef' => sub {
99 | my $doc = <<'EOQ';
100 | query Q {
101 | sync
102 | }
103 | EOQ
104 |
105 | my $ast = parse($doc);
106 |
107 | my $expected = {
108 | data => {
109 | sync => undef,
110 | }
111 | };
112 |
113 | cmp_deeply execute($schema, $ast, $nulling_data), $expected;
114 | };
115 |
116 | subtest 'nulls a returned object that contains a non-nullable field that returns null' => sub {
117 | plan skip_all => 'TODO';
118 |
119 | my $doc = <<'EOQ';
120 | query Q {
121 | nest {
122 | nonNullSync
123 | }
124 | }
125 | EOQ
126 |
127 | my $ast = parse($doc);
128 | # p execute($schema, $ast, $nulling_data);
129 | cmp_deeply execute($schema, $ast, $nulling_data), {
130 | data => {
131 | nest => undef
132 | },
133 | errors => [noclass(superhashof({
134 | message => 'Cannot return null for non-nullable field DataType.nonNullSync.',
135 | locations => [{ line => 4, column => 11 }]
136 | }))],
137 | }
138 | };
139 |
140 | subtest 'nulls the top level if sync non-nullable field throws' => sub {
141 | my $doc = <<'EOQ';
142 | query Q { nonNullSync }
143 | EOQ
144 |
145 | cmp_deeply execute($schema, parse($doc), $throwing_data), {
146 | data => undef,
147 | errors => [noclass(superhashof({
148 | message => $nonnull_sync_error->{message},
149 | locations => [{ line => 1, column => 17 }]
150 | }))]
151 | };
152 | };
153 |
154 | subtest 'nulls the top level if sync non-nullable field returns undef' => sub {
155 | plan skip_all => 'TODO';
156 |
157 | my $doc = <<'EOQ';
158 | query Q { nonNullSync }
159 | EOQ
160 |
161 | cmp_deeply execute($schema, parse($doc), $nulling_data), {
162 | data => undef,
163 | errors => [noclass(superhashof({
164 | message => 'Cannot return undef for non-nullable field DataType.nonnulnull.',
165 | locations => [{ line => 2, column => 17 }],
166 | }))],
167 | };
168 | };
169 |
170 | done_testing;
171 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/KnownDirectives.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::KnownDirectives;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use DDP;
7 | use List::Util qw/reduce/;
8 |
9 | use GraphQL::Error qw/GraphQLError/;
10 | use GraphQL::Type::Directive;
11 | use GraphQL::Language::Kinds qw/Kind/;
12 | use GraphQL::Language::Parser;
13 | use GraphQL::Util qw/find/;
14 |
15 | sub DirectiveLocation { 'GraphQL::Type::Directive' }
16 |
17 | sub unknown_directive_message {
18 | my $directive_name = shift;
19 | return qq`Unknown directive "$directive_name".`;
20 | }
21 |
22 | sub misplaced_directive_message {
23 | my ($directive_name, $location) = @_;
24 | return qq`Directive "$directive_name" may not be used on $location.`;
25 | }
26 |
27 | # Known directives
28 | #
29 | # A GraphQL document is only valid if all `@directives` are known by the
30 | # schema and legally positioned.
31 | sub validate {
32 | my ($self, $context) = @_;
33 | return {
34 | Directive => sub {
35 | my (undef, $node, $key, $parent, $path, $ancestors) = @_;
36 | my $directive_def = find(
37 | $context->get_schema->get_directives,
38 | sub { $_[0]->{name} eq $node->{name}{value} }
39 | );
40 |
41 | if (!$directive_def) {
42 | $context->report_error(
43 | GraphQLError(
44 | unknown_directive_message($node->{name}{value}),
45 | [$node]
46 | )
47 | );
48 | return;
49 | }
50 |
51 | my $candidate_location = get_directive_location_for_ast_path($ancestors);
52 | if (!$candidate_location) {
53 | $context->report_error(
54 | GraphQLError(
55 | misplaced_directive_message($node->{name}{value}, $node->{type}),
56 | [$node]
57 | )
58 | );
59 | }
60 | elsif (reduce { $a && !($b eq $candidate_location) } 1, @{ $directive_def->{locations} }) {
61 | $context->report_error(
62 | GraphQLError(
63 | misplaced_directive_message($node->{name}{value}, $candidate_location),
64 | [$node]
65 | )
66 | );
67 | }
68 |
69 | return; # void
70 | }
71 | };
72 | }
73 |
74 | sub get_directive_location_for_ast_path {
75 | my $ancestors = shift;
76 | my $applied_to = $ancestors->[scalar(@$ancestors) - 1];
77 |
78 | if ($applied_to->{kind} eq Kind->OPERATION_DEFINITION) {
79 | if ($applied_to->{operation} eq 'query') { return DirectiveLocation->QUERY }
80 | elsif ($applied_to->{operation} eq 'mutation') { return DirectiveLocation->MUTATION }
81 | elsif ($applied_to->{operation} eq 'subscription') { return DirectiveLocation->SUBSCRIPTION }
82 | }
83 | elsif ($applied_to->{kind} eq Kind->FIELD) {
84 | return DirectiveLocation->FIELD;
85 | }
86 | elsif ($applied_to->{kind} eq Kind->FRAGMENT_SPREAD) {
87 | return DirectiveLocation->FRAGMENT_SPREAD;
88 | }
89 | elsif ($applied_to->{kind} eq Kind->INLINE_FRAGMENT) {
90 | return DirectiveLocation->INLINE_FRAGMENT;
91 | }
92 | elsif ($applied_to->{kind} eq Kind->FRAGMENT_DEFINITION) {
93 | return DirectiveLocation->FRAGMENT_DEFINITION;
94 | }
95 | elsif ($applied_to->{kind} eq Kind->SCHEMA_DEFINITION) {
96 | return DirectiveLocation->SCHEMA;
97 | }
98 | elsif ($applied_to->{kind} eq Kind->SCALAR_TYPE_DEFINITION) {
99 | return DirectiveLocation->SCALAR;
100 | }
101 | elsif ($applied_to->{kind} eq Kind->OBJECT_TYPE_DEFINITION) {
102 | return DirectiveLocation->OBJECT;
103 | }
104 | elsif ($applied_to->{kind} eq Kind->FIELD_DEFINITION) {
105 | return DirectiveLocation->FIELD_DEFINITION;
106 | }
107 | elsif ($applied_to->{kind} eq Kind->INTERFACE_TYPE_DEFINITION) {
108 | return DirectiveLocation->INTERFACE;
109 | }
110 | elsif ($applied_to->{kind} eq Kind->UNION_TYPE_DEFINITION) {
111 | return DirectiveLocation->UNION;
112 | }
113 | elsif ($applied_to->{kind} eq Kind->ENUM_TYPE_DEFINITION) {
114 | return DirectiveLocation->ENUM;
115 | }
116 | elsif ($applied_to->{kind} eq Kind->ENUM_VALUE_DEFINITION) {
117 | return DirectiveLocation->ENUM_VALUE;
118 | }
119 | elsif ($applied_to->{kind} eq Kind->INPUT_OBJECT_TYPE_DEFINITION) {
120 | return DirectiveLocation->INPUT_OBJECT;
121 | }
122 | elsif ($applied_to->{kind} eq Kind->INPUT_VALUE_DEFINITION) {
123 | my $parent_node = $ancestors->[scalar(@$ancestors) - 3];
124 | return $parent_node->{kind} eq Kind->INPUT_OBJECT_TYPE_DEFINITION
125 | ? DirectiveLocation->INPUT_FIELD_DEFINITION
126 | : DirectiveLocation->ARGUMENT_DEFINITION;
127 | }
128 |
129 | return;
130 | }
131 |
132 | 1;
133 |
134 | __END__
135 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Rule/FieldsOnCorrectType.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Rule::FieldsOnCorrectType;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use GraphQL::Error qw/GraphQLError/;
7 | use GraphQL::Util qw/
8 | stringify_type
9 | suggestion_list
10 | quoted_or_list
11 | /;
12 | use GraphQL::Util::Type qw/
13 | is_abstract_type
14 | /;
15 |
16 | sub undefined_field_message {
17 | my ($field_name, $type, $suggested_type_names, $suggested_field_names) = @_;
18 |
19 | my $message = qq`Cannot query field "$field_name" on type "${ stringify_type($type) }".`;
20 |
21 | if ($suggested_type_names && @$suggested_type_names) {
22 | my $suggestions = quoted_or_list($suggested_type_names);
23 | $message .= " Did you mean to use an inline fragment on $suggestions?";
24 | }
25 | elsif ($suggested_field_names && @$suggested_field_names) {
26 | my $suggestions = quoted_or_list($suggested_field_names);
27 | $message .= " Did you mean $suggestions?";
28 | }
29 |
30 | return $message;
31 | }
32 |
33 | # Fields on correct type
34 | #
35 | # A GraphQL document is only valid if all fields selected are defined by the
36 | # parent type, or are an allowed meta field such as __typename.
37 | sub validate {
38 | my ($self, $context) = @_;
39 | return {
40 | Field => sub {
41 | my (undef, $node) = @_;
42 |
43 | my $type = $context->get_parent_type;
44 | if ($type) {
45 | my $field_def = $context->get_field_def;
46 | unless ($field_def) {
47 | # This field doesn't exist, lets look for suggestions.
48 | my $schema = $context->get_schema;
49 | my $field_name = $node->{name}{value};
50 |
51 | # First determine if there are any suggested types to
52 | # condition on.
53 | my $suggested_type_names = get_suggested_type_names(
54 | $schema, $type, $field_name
55 | );
56 |
57 | # If there are no suggested types, then perhaps this was a type?
58 | my $suggested_field_names = scalar(@$suggested_type_names) != 0
59 | ? []
60 | : get_suggested_field_names($schema, $type, $field_name);
61 |
62 | # Report an error, including helpful suggestions.
63 | $context->report_error(
64 | GraphQLError(
65 | undefined_field_message(
66 | $field_name,
67 | $type,
68 | $suggested_type_names,
69 | $suggested_field_names
70 | ),
71 | [$node]
72 | )
73 | );
74 | }
75 | }
76 |
77 | return;
78 | }
79 | }
80 | }
81 |
82 | # Go through all of the implementations of type, as well as the interfaces
83 | # that they implement. If any of those types include the provided field,
84 | # suggest them, sorted by how often the type is referenced, starting
85 | # with Interfaces.
86 | sub get_suggested_type_names {
87 | my ($schema, $type, $field_name) = @_;
88 |
89 | if (is_abstract_type($type)) {
90 | my @suggested_object_types;
91 | my %interface_usage_count;
92 |
93 | for my $possible_type (@{ $schema->get_possible_types($type) }) {
94 | unless ($possible_type->get_fields->{ $field_name }) {
95 | last;
96 | }
97 |
98 | # This object type defines this field.
99 | push @suggested_object_types, $possible_type->name;
100 |
101 | for my $possible_interface (@{ $possible_type->get_interfaces }) {
102 | unless ($possible_interface->get_fields->{ $field_name }) {
103 | last;
104 | }
105 |
106 | # This interface type defines this field.
107 | $interface_usage_count{ $possible_interface->name }++;
108 | }
109 | }
110 |
111 | # Suggest interface types based on how common they are.
112 | my @suggested_interface_types =
113 | sort { $interface_usage_count{$b} <=> $interface_usage_count{$a} }
114 | keys %interface_usage_count;
115 |
116 | # Suggest both interface and object types.
117 | return [@suggested_interface_types, @suggested_object_types];
118 | }
119 |
120 | # Otherwise, must be an Object type, which does not have possible fields.
121 | return [];
122 | }
123 |
124 | # For the field name provided, determine if there are any similar field names
125 | # that may be the result of a typo.
126 | sub get_suggested_field_names {
127 | my ($schema, $type, $field_name) = @_;
128 |
129 | if ($type->isa('GraphQL::Type::Object')
130 | || $type->isa('GraphQL::Type::Interface'))
131 | {
132 | my @possible_field_names = keys %{ $type->get_fields };
133 | return suggestion_list($field_name, \@possible_field_names);
134 | }
135 |
136 | # Otherwise, must be a Union type, which does not define fields.
137 | return;
138 | }
139 |
140 | 1;
141 |
142 | __END__
143 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/default_values_of_correct_type.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub default_for_non_null_arg {
15 | my ($var_name, $type_name, $guess_type_name, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::DefaultValuesOfCorrectType::default_for_non_null_arg_message($var_name, $type_name, $guess_type_name),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | sub bad_value {
24 | my ($var_name, $type_name, $val, $line, $column, $errors) = @_;
25 |
26 | my $real_errors;
27 | if (!$errors) {
28 | $real_errors = [qq`Expected type "${type_name}", found ${val}.`];
29 | }
30 | else {
31 | $real_errors = $errors;
32 | }
33 |
34 | return {
35 | message => GraphQL::Validator::Rule::DefaultValuesOfCorrectType::bad_value_for_default_arg_message($var_name, $type_name, $val, $real_errors),
36 | locations => [ { line => $line, column => $column } ],
37 | path => undef,
38 | };
39 | }
40 |
41 | subtest 'variables with no default values' => sub {
42 | expect_passes_rule('DefaultValuesOfCorrectType', '
43 | query NullableValues($a: Int, $b: String, $c: ComplexInput) {
44 | dog { name }
45 | }
46 | ');
47 | };
48 |
49 | subtest 'required variables without default values' => sub {
50 | expect_passes_rule('DefaultValuesOfCorrectType', '
51 | query RequiredValues($a: Int!, $b: String!) {
52 | dog { name }
53 | }
54 | ');
55 | };
56 |
57 | subtest 'variables with valid default values' => sub {
58 | expect_passes_rule('DefaultValuesOfCorrectType', '
59 | query WithDefaultValues(
60 | $a: Int = 1,
61 | $b: String = "ok",
62 | $c: ComplexInput = { requiredField: true, intField: 3 }
63 | ) {
64 | dog { name }
65 | }
66 | ');
67 | };
68 |
69 | subtest 'variables with valid default null values' => sub {
70 | expect_passes_rule('DefaultValuesOfCorrectType', '
71 | query WithDefaultValues(
72 | $a: Int = null,
73 | $b: String = null,
74 | $c: ComplexInput = { requiredField: true, intField: null }
75 | ) {
76 | dog { name }
77 | }
78 | ');
79 | };
80 |
81 | subtest 'variables with invalid default null values' => sub {
82 | expect_fails_rule('DefaultValuesOfCorrectType', '
83 | query WithDefaultValues(
84 | $a: Int! = null,
85 | $b: String! = null,
86 | $c: ComplexInput = { requiredField: null, intField: null }
87 | ) {
88 | dog { name }
89 | }',
90 | [
91 | default_for_non_null_arg('a', 'Int!', 'Int', 3, 20),
92 | bad_value('a', 'Int!', 'null', 3, 20, ['Expected "Int!", found null.' ]),
93 | default_for_non_null_arg('b', 'String!', 'String', 4, 23),
94 | bad_value('b', 'String!', 'null', 4, 23, ['Expected "String!", found null.']),
95 | bad_value('c', 'ComplexInput', '{requiredField: null, intField: null}', 5, 28, ['In field "requiredField": Expected "Boolean!", found null.']),
96 | ]);
97 | };
98 |
99 | subtest 'no required variables with default values' => sub {
100 | expect_fails_rule('DefaultValuesOfCorrectType', '
101 | query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") {
102 | dog { name }
103 | }
104 | ', [
105 | default_for_non_null_arg('a', 'Int!', 'Int', 2, 49),
106 | default_for_non_null_arg('b', 'String!', 'String', 2, 66)
107 | ]);
108 | };
109 |
110 | subtest 'variables with invalid default values' => sub {
111 | expect_fails_rule('DefaultValuesOfCorrectType', '
112 | query InvalidDefaultValues(
113 | $a: Int = "one",
114 | $b: String = 4,
115 | $c: ComplexInput = "notverycomplex"
116 | ) {
117 | dog { name }
118 | }
119 | ', [
120 | bad_value('a', 'Int', '"one"', 3, 19, [
121 | 'Expected type "Int", found "one".'
122 | ]),
123 | bad_value('b', 'String', '4', 4, 22, [
124 | 'Expected type "String", found 4.'
125 | ]),
126 | bad_value('c', 'ComplexInput', '"notverycomplex"', 5, 28, [
127 | 'Expected "ComplexInput", found not an object.'
128 | ])
129 | ]);
130 | };
131 |
132 | subtest 'complex variables missing required field' => sub {
133 | expect_fails_rule('DefaultValuesOfCorrectType', '
134 | query MissingRequiredField($a: ComplexInput = {intField: 3}) {
135 | dog { name }
136 | }
137 | ', [
138 | bad_value('a', 'ComplexInput', '{intField: 3}', 2, 53, [
139 | 'In field "requiredField": Expected "Boolean!", found null.'
140 | ])
141 | ]);
142 | };
143 |
144 | subtest 'list variables with invalid item' => sub {
145 | expect_fails_rule('DefaultValuesOfCorrectType', '
146 | query InvalidItem($a: [String] = ["one", 2]) {
147 | dog { name }
148 | }
149 | ', [
150 | bad_value('a', '[String]', '["one", 2]', 2, 40, [
151 | 'In element #1: Expected type "String", found 2.'
152 | ])
153 | ]);
154 | };
155 |
156 | done_testing;
157 |
--------------------------------------------------------------------------------
/t/graphql/execute-directives.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use Test::More;
5 | use JSON qw/encode_json/;
6 |
7 | use GraphQL qw/:types/;
8 | use GraphQL::Language::Parser qw/parse/;
9 | use GraphQL::Execute qw/execute/;
10 |
11 | my $schema = GraphQLSchema(
12 | query => GraphQLObjectType(
13 | name => 'TestType',
14 | fields => {
15 | a => { type => GraphQLString },
16 | b => { type => GraphQLString },
17 | },
18 | ),
19 | );
20 |
21 | my %data = (
22 | a => sub { 'a' },
23 | b => sub { 'b' },
24 | );
25 |
26 | sub execute_test_query {
27 | my $doc = shift;
28 | return execute($schema, parse($doc), \%data);
29 | }
30 |
31 | subtest 'works without directives' => sub {
32 | is_deeply execute_test_query('{ a, b }'), { data => { a => 'a', b => 'b' } },
33 | 'basic query works';
34 | };
35 |
36 | subtest 'works on scalars' => sub {
37 | is_deeply execute_test_query('{ a, b @include(if: true) }'),
38 | { data => { a => 'a', b => 'b' } }, 'if true includes scalar';
39 |
40 | is_deeply execute_test_query('{ a, b @include(if: false) }'),
41 | { data => { a => 'a' } }, 'if false omits on scalar';
42 |
43 | is_deeply execute_test_query('{ a, b @skip(if: false) }'),
44 | { data => { a => 'a', b => 'b' } }, 'unless false includes scalar';
45 |
46 | is_deeply execute_test_query('{ a, b @skip(if: true) }'),
47 | { data => { a => 'a' } }, 'unless true omits scalar';
48 | };
49 |
50 | subtest 'works on fragment spreads' => sub {
51 | my $q;
52 |
53 | $q = <<'EOQ';
54 | query Q {
55 | a
56 | ...Frag @include(if: false)
57 | }
58 | fragment Frag on TestType {
59 | b
60 | }
61 | EOQ
62 | is_deeply execute_test_query($q), { data => { a => 'a' } }, 'if false omits fragment spread';
63 |
64 | $q = <<'EOQ';
65 | query Q {
66 | a
67 | ...Frag @include(if: true)
68 | }
69 | fragment Frag on TestType {
70 | b
71 | }
72 | EOQ
73 | is_deeply execute_test_query($q), { data => { a => 'a', b => 'b' } }, 'if true includes fragment spread';
74 |
75 | $q = <<'EOQ';
76 | query Q {
77 | a
78 | ...Frag @skip(if: false)
79 | }
80 | fragment Frag on TestType {
81 | b
82 | }
83 | EOQ
84 | is_deeply execute_test_query($q), { data => { a => 'a', b => 'b' } }, 'unless false includes fragment spread';
85 |
86 | $q = <<'EOQ';
87 | query Q {
88 | a
89 | ...Frag @skip(if: true)
90 | }
91 | fragment Frag on TestType {
92 | b
93 | }
94 | EOQ
95 | is_deeply execute_test_query($q), { data => { a => 'a' } }, 'unless true omits fragment spread';
96 | };
97 |
98 | subtest 'works on inline fragment' => sub {
99 | my $q;
100 |
101 | $q = <<'EOQ';
102 | query Q {
103 | a
104 | ... on TestType @include(if: false) {
105 | b
106 | }
107 | }
108 | EOQ
109 | is_deeply execute_test_query($q), { data => { a => 'a' } },
110 | 'if false omits inline fragment';
111 |
112 | $q = <<'EOQ';
113 | query Q {
114 | a
115 | ... on TestType @include(if: true) {
116 | b
117 | }
118 | }
119 | EOQ
120 | is_deeply execute_test_query($q), { data => { a => 'a', b => 'b' } },
121 | 'if true includes inline fragment' ;
122 |
123 | $q = <<'EOQ';
124 | query Q {
125 | a
126 | ... on TestType @skip(if: false) {
127 | b
128 | }
129 | }
130 | EOQ
131 | is_deeply execute_test_query($q), { data => { a => 'a', b => 'b' } },
132 | 'unless false includes inline fragment' ;
133 |
134 | $q = <<'EOQ';
135 | query Q {
136 | a
137 | ... on TestType @skip(if: true) {
138 | b
139 | }
140 | }
141 | EOQ
142 | is_deeply execute_test_query($q), { data => { a => 'a' } },
143 | 'unless true includes inline fragment' ;
144 | };
145 |
146 | subtest 'works on anonymous inline fragment' => sub {
147 | my $q;
148 |
149 | $q = <<'EOQ';
150 | query Q {
151 | a
152 | ... @include(if: false) {
153 | b
154 | }
155 | }
156 | EOQ
157 | is_deeply execute_test_query($q), { data => { a => 'a' } },
158 | 'if false omits anonymous inline fragment';
159 |
160 | $q = <<'EOQ';
161 | query Q {
162 | a
163 | ... @include(if: true) {
164 | b
165 | }
166 | }
167 | EOQ
168 | is_deeply execute_test_query($q), { data => { a => 'a', b => 'b' } },
169 | 'if true includes anonymous inline fragment';
170 |
171 | $q = <<'EOQ';
172 | query Q {
173 | a
174 | ... @skip(if: false) {
175 | b
176 | }
177 | }
178 | EOQ
179 | is_deeply execute_test_query($q), { data => { a => 'a', b => 'b' } },
180 | 'unless false includes anonymous inline fragment';
181 |
182 | $q = <<'EOQ';
183 | query Q {
184 | a
185 | ... @skip(if: true) {
186 | b
187 | }
188 | }
189 | EOQ
190 | is_deeply execute_test_query($q), { data => { a => 'a' } },
191 | 'unless true includes anonymous inline fragment';
192 | };
193 |
194 | subtest 'works with skip and include directives' => sub {
195 | is_deeply execute_test_query('{ a, b @include(if: true) @skip(if: false) }'),
196 | { data => { a => 'a', b => 'b' } }, 'include and no skip';
197 |
198 | is_deeply execute_test_query('{ a, b @include(if: true) @skip(if: true) }'),
199 | { data => { a => 'a' } }, 'include and skip';
200 |
201 | is_deeply execute_test_query('{ a, b @include(if: false) @skip(if: false) }'),
202 | { data => { a => 'a' } }, 'no include or skip';
203 | };
204 |
205 | done_testing;
206 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/no_unused_variables.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub unused_var {
15 | my ($var_name, $op_name, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::NoUnusedVariables::unused_variable_message($var_name, $op_name),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | subtest 'uses all variables' => sub {
24 | expect_passes_rule('NoUnusedVariables', '
25 | query ($a: String, $b: String, $c: String) {
26 | field(a: $a, b: $b, c: $c)
27 | }
28 | ');
29 | };
30 |
31 | subtest 'uses all variables deeply' => sub {
32 | expect_passes_rule('NoUnusedVariables', '
33 | query Foo($a: String, $b: String, $c: String) {
34 | field(a: $a) {
35 | field(b: $b) {
36 | field(c: $c)
37 | }
38 | }
39 | }
40 | ');
41 | };
42 |
43 | subtest 'uses all variables deeply in inline fragments' => sub {
44 | expect_passes_rule('NoUnusedVariables', '
45 | query Foo($a: String, $b: String, $c: String) {
46 | ... on Type {
47 | field(a: $a) {
48 | field(b: $b) {
49 | ... on Type {
50 | field(c: $c)
51 | }
52 | }
53 | }
54 | }
55 | }
56 | ');
57 | };
58 |
59 | subtest 'uses all variables in fragments' => sub {
60 | expect_passes_rule('NoUnusedVariables', '
61 | query Foo($a: String, $b: String, $c: String) {
62 | ...FragA
63 | }
64 | fragment FragA on Type {
65 | field(a: $a) {
66 | ...FragB
67 | }
68 | }
69 | fragment FragB on Type {
70 | field(b: $b) {
71 | ...FragC
72 | }
73 | }
74 | fragment FragC on Type {
75 | field(c: $c)
76 | }
77 | ');
78 | };
79 |
80 | subtest 'variable used by fragment in multiple operations' => sub {
81 | expect_passes_rule('NoUnusedVariables', '
82 | query Foo($a: String) {
83 | ...FragA
84 | }
85 | query Bar($b: String) {
86 | ...FragB
87 | }
88 | fragment FragA on Type {
89 | field(a: $a)
90 | }
91 | fragment FragB on Type {
92 | field(b: $b)
93 | }
94 | ');
95 | };
96 |
97 | subtest 'variable used by recursive fragment' => sub {
98 | expect_passes_rule('NoUnusedVariables', '
99 | query Foo($a: String) {
100 | ...FragA
101 | }
102 | fragment FragA on Type {
103 | field(a: $a) {
104 | ...FragA
105 | }
106 | }
107 | ');
108 | };
109 |
110 | subtest 'variable not used' => sub {
111 | expect_fails_rule('NoUnusedVariables', '
112 | query ($a: String, $b: String, $c: String) {
113 | field(a: $a, b: $b)
114 | }
115 | ', [
116 | unused_var('c', undef, 2, 38)
117 | ]);
118 | };
119 |
120 | subtest 'multiple variables not used' => sub {
121 | expect_fails_rule('NoUnusedVariables', '
122 | query Foo($a: String, $b: String, $c: String) {
123 | field(b: $b)
124 | }
125 | ', [
126 | unused_var('a', 'Foo', 2, 17),
127 | unused_var('c', 'Foo', 2, 41)
128 | ]);
129 | };
130 |
131 | subtest 'variable not used in fragments' => sub {
132 | expect_fails_rule('NoUnusedVariables', '
133 | query Foo($a: String, $b: String, $c: String) {
134 | ...FragA
135 | }
136 | fragment FragA on Type {
137 | field(a: $a) {
138 | ...FragB
139 | }
140 | }
141 | fragment FragB on Type {
142 | field(b: $b) {
143 | ...FragC
144 | }
145 | }
146 | fragment FragC on Type {
147 | field
148 | }
149 | ', [
150 | unused_var('c', 'Foo', 2, 41)
151 | ]);
152 | };
153 |
154 | subtest 'multiple variables not used in fragments' => sub {
155 | expect_fails_rule('NoUnusedVariables', '
156 | query Foo($a: String, $b: String, $c: String) {
157 | ...FragA
158 | }
159 | fragment FragA on Type {
160 | field {
161 | ...FragB
162 | }
163 | }
164 | fragment FragB on Type {
165 | field(b: $b) {
166 | ...FragC
167 | }
168 | }
169 | fragment FragC on Type {
170 | field
171 | }
172 | ', [
173 | unused_var('a', 'Foo', 2, 17),
174 | unused_var('c', 'Foo', 2, 41)
175 | ]);
176 | };
177 |
178 | subtest 'variable not used by unreferenced fragment' => sub {
179 | expect_fails_rule('NoUnusedVariables', '
180 | query Foo($b: String) {
181 | ...FragA
182 | }
183 | fragment FragA on Type {
184 | field(a: $a)
185 | }
186 | fragment FragB on Type {
187 | field(b: $b)
188 | }
189 | ', [
190 | unused_var('b', 'Foo', 2, 17)
191 | ]);
192 | };
193 |
194 | subtest 'variable not used by fragment used by other operation' => sub {
195 | expect_fails_rule('NoUnusedVariables', '
196 | query Foo($b: String) {
197 | ...FragA
198 | }
199 | query Bar($a: String) {
200 | ...FragB
201 | }
202 | fragment FragA on Type {
203 | field(a: $a)
204 | }
205 | fragment FragB on Type {
206 | field(b: $b)
207 | }
208 | ', [
209 | unused_var('b', 'Foo', 2, 17),
210 | unused_var('a', 'Bar', 5, 17)
211 | ]);
212 | };
213 |
214 | done_testing;
215 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use DDP;
7 |
8 | use Exporter qw/import/;
9 | our @EXPORT_OK = (qw/validate SPECIFIED_RULES/);
10 |
11 | use GraphQL::Language::Visitor qw/
12 | visit
13 | visit_in_parallel
14 | visit_with_typeinfo
15 | /;
16 | use GraphQL::TypeInfo;
17 | use GraphQL::Validator::Context;
18 | use GraphQL::Validator::Rule::UniqueOperationNames;
19 | use GraphQL::Validator::Rule::LoneAnonymousOperation;
20 | use GraphQL::Validator::Rule::KnownTypeNames;
21 | use GraphQL::Validator::Rule::FragmentsOnCompositeTypes;
22 | use GraphQL::Validator::Rule::VariablesAreInputTypes;
23 | use GraphQL::Validator::Rule::ScalarLeafs;
24 | use GraphQL::Validator::Rule::FieldsOnCorrectType;
25 | use GraphQL::Validator::Rule::UniqueFragmentNames;
26 | use GraphQL::Validator::Rule::KnownFragmentNames;
27 | use GraphQL::Validator::Rule::NoUnusedFragments;
28 | use GraphQL::Validator::Rule::PossibleFragmentSpreads;
29 | use GraphQL::Validator::Rule::NoFragmentCycles;
30 | use GraphQL::Validator::Rule::UniqueVariableNames;
31 | use GraphQL::Validator::Rule::NoUndefinedVariables;
32 | use GraphQL::Validator::Rule::NoUnusedVariables;
33 | use GraphQL::Validator::Rule::KnownDirectives;
34 | use GraphQL::Validator::Rule::UniqueDirectivesPerLocation;
35 | use GraphQL::Validator::Rule::KnownArgumentNames;
36 | use GraphQL::Validator::Rule::UniqueArgumentNames;
37 | use GraphQL::Validator::Rule::ArgumentsOfCorrectType;
38 | use GraphQL::Validator::Rule::ProvidedNonNullArguments;
39 | use GraphQL::Validator::Rule::DefaultValuesOfCorrectType;
40 | use GraphQL::Validator::Rule::VariablesInAllowedPosition;
41 | use GraphQL::Validator::Rule::OverlappingFieldsCanBeMerged;
42 | use GraphQL::Validator::Rule::UniqueInputFieldNames;
43 |
44 | use constant {
45 | SPECIFIED_RULES => [
46 | # Spec Section: "Operation Name Uniqueness"
47 | 'UniqueOperationNames',
48 |
49 | # Spec Section: "Lone Anonymous Operation"
50 | 'LoneAnonymousOperation',
51 |
52 | # Spec Section: "Fragment Spread Type Existence"
53 | 'KnownTypeNames',
54 |
55 | # Spec Section: "Fragments on Composite Types"
56 | 'FragmentsOnCompositeTypes',
57 |
58 | # Spec Section: "Variables are Input Types"
59 | 'VariablesAreInputTypes',
60 |
61 | # Spec Section: "Leaf Field Selections"
62 | 'ScalarLeafs',
63 |
64 | # Spec Section: "Field Selections on Objects, Interfaces, and Unions Types"
65 | 'FieldsOnCorrectType',
66 |
67 | # Spec Section: "Fragment Name Uniqueness"
68 | 'UniqueFragmentNames',
69 |
70 | # Spec Section: "Fragment spread target defined"
71 | 'KnownFragmentNames',
72 |
73 | # Spec Section: "Fragments must be used"
74 | 'NoUnusedFragments',
75 |
76 | # Spec Section: "Fragment spread is possible"
77 | 'PossibleFragmentSpreads',
78 |
79 | # Spec Section: "Fragments must not form cycles"
80 | 'NoFragmentCycles',
81 |
82 | # Spec Section: "Variable Uniqueness"
83 | 'UniqueVariableNames',
84 |
85 | # Spec Section: "All Variable Used Defined"
86 | 'NoUndefinedVariables',
87 |
88 | # Spec Section: "All Variables Used"
89 | 'NoUnusedVariables',
90 |
91 | # Spec Section: "Directives Are Defined"
92 | 'KnownDirectives',
93 |
94 | # Spec Section: "Directives Are Unique Per Location"
95 | 'UniqueDirectivesPerLocation',
96 |
97 | # Spec Section: "Argument Names"
98 | 'KnownArgumentNames',
99 |
100 | # Spec Section: "Argument Uniqueness"
101 | 'UniqueArgumentNames',
102 |
103 | # Spec Section: "Argument Values Type Correctness"
104 | 'ArgumentsOfCorrectType',
105 |
106 | # Spec Section: "Argument Optionality"
107 | 'ProvidedNonNullArguments',
108 |
109 | # Spec Section: "Variable Default Values Are Correctly Typed"
110 | 'DefaultValuesOfCorrectType',
111 |
112 | # Spec Section: "All Variable Usages Are Allowed"
113 | 'VariablesInAllowedPosition',
114 |
115 | # Spec Section: "Field Selection Merging"
116 | # TODO 'OverlappingFieldsCanBeMerged',
117 |
118 | # Spec Section: "Input Object Field Uniqueness"
119 | 'UniqueInputFieldNames',
120 | ],
121 | };
122 |
123 | sub validate {
124 | my ($schema, $ast, $rules, $type_info) = @_;
125 |
126 | die "Must provide schema\n" unless $schema;
127 | die "Must provide document\n" unless $ast;
128 |
129 | die "Schema must be an instance of GraphQL::Type::Schema.\n"
130 | unless $schema->isa('GraphQL::Type::Schema');
131 |
132 | return visit_using_rules(
133 | $schema,
134 | $type_info || GraphQL::TypeInfo->new($schema),
135 | $ast,
136 | $rules || SPECIFIED_RULES,
137 | );
138 | }
139 |
140 | # This uses a specialized visitor which runs multiple visitors in parallel,
141 | # while maintaining the visitor skip and break API.
142 | sub visit_using_rules {
143 | my ($schema, $type_info, $ast, $rules) = @_;
144 |
145 | my $context = GraphQL::Validator::Context->new(
146 | schema => $schema,
147 | ast => $ast,
148 | type_info => $type_info,
149 | );
150 |
151 | my @visitors =
152 | map { "GraphQL::Validator::Rule::$_"->validate($context) } @$rules;
153 |
154 | # Visit the whole document with each instance of all provided rules.
155 | visit($ast, visit_with_typeinfo($type_info, visit_in_parallel(\@visitors)));
156 |
157 | return $context->get_errors;
158 | }
159 |
160 | 1;
161 |
162 | __END__
163 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/known_directives.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub unknown_directive {
15 | my ($directive_name, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::KnownDirectives::unknown_directive_message($directive_name),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | sub misplaced_directive {
24 | my ($directive_name, $placement, $line, $column) = @_;
25 | return {
26 | message => GraphQL::Validator::Rule::KnownDirectives::misplaced_directive_message($directive_name, $placement),
27 | locations => [{ line => $line, column => $column }],
28 | path => undef,
29 | };
30 | }
31 |
32 | subtest 'with no directives' => sub {
33 | expect_passes_rule('KnownDirectives', '
34 | query Foo {
35 | name
36 | ...Frag
37 | }
38 |
39 | fragment Frag on Dog {
40 | name
41 | }
42 | ');
43 | };
44 |
45 | subtest 'with known directives' => sub {
46 | expect_passes_rule('KnownDirectives', '
47 | {
48 | dog @include(if: true) {
49 | name
50 | }
51 | human @skip(if: false) {
52 | name
53 | }
54 | }
55 | ');
56 | };
57 |
58 | subtest 'with unknown directive' => sub {
59 | expect_fails_rule('KnownDirectives', '
60 | {
61 | dog @unknown(directive: "value") {
62 | name
63 | }
64 | }
65 | ', [
66 | unknown_directive('unknown', 3, 13)
67 | ]);
68 | };
69 |
70 | subtest 'with many unknown directives' => sub {
71 | expect_fails_rule('KnownDirectives', '
72 | {
73 | dog @unknown(directive: "value") {
74 | name
75 | }
76 | human @unknown(directive: "value") {
77 | name
78 | pets @unknown(directive: "value") {
79 | name
80 | }
81 | }
82 | }
83 | ', [
84 | unknown_directive('unknown', 3, 13),
85 | unknown_directive('unknown', 6, 15),
86 | unknown_directive('unknown', 8, 16)
87 | ]);
88 | };
89 |
90 | subtest 'with well placed directives' => sub {
91 | expect_passes_rule('KnownDirectives', '
92 | query Foo @onQuery {
93 | name @include(if: true)
94 | ...Frag @include(if: true)
95 | skippedField @skip(if: true)
96 | ...SkippedFrag @skip(if: true)
97 | }
98 |
99 | mutation Bar @onMutation {
100 | someField
101 | }
102 | ');
103 | };
104 |
105 | subtest 'with misplaced directives' => sub {
106 | expect_fails_rule('KnownDirectives', '
107 | query Foo @include(if: true) {
108 | name @onQuery
109 | ...Frag @onQuery
110 | }
111 |
112 | mutation Bar @onQuery {
113 | someField
114 | }
115 | ', [
116 | misplaced_directive('include', 'QUERY', 2, 17),
117 | misplaced_directive('onQuery', 'FIELD', 3, 14),
118 | misplaced_directive('onQuery', 'FRAGMENT_SPREAD', 4, 17),
119 | misplaced_directive('onQuery', 'MUTATION', 7, 20),
120 | ]);
121 | };
122 |
123 | subtest 'within schema language' => sub {
124 | subtest 'with well placed directives' => sub {
125 | expect_passes_rule('KnownDirectives', '
126 | type MyObj implements MyInterface @onObject {
127 | myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition
128 | }
129 |
130 | scalar MyScalar @onScalar
131 |
132 | interface MyInterface @onInterface {
133 | myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition
134 | }
135 |
136 | union MyUnion @onUnion = MyObj | Other
137 |
138 | enum MyEnum @onEnum {
139 | MY_VALUE @onEnumValue
140 | }
141 |
142 | input MyInput @onInputObject {
143 | myField: Int @onInputFieldDefinition
144 | }
145 |
146 | schema @onSchema {
147 | query: MyQuery
148 | }
149 | ');
150 | };
151 |
152 | subtest 'with misplaced directives' => sub {
153 | expect_fails_rule('KnownDirectives', '
154 | type MyObj implements MyInterface @onInterface {
155 | myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition
156 | }
157 |
158 | scalar MyScalar @onEnum
159 |
160 | interface MyInterface @onObject {
161 | myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition
162 | }
163 |
164 | union MyUnion @onEnumValue = MyObj | Other
165 |
166 | enum MyEnum @onScalar {
167 | MY_VALUE @onUnion
168 | }
169 |
170 | input MyInput @onEnum {
171 | myField: Int @onArgumentDefinition
172 | }
173 |
174 | schema @onObject {
175 | query: MyQuery
176 | }
177 | ', [
178 | misplaced_directive('onInterface', 'OBJECT', 2, 43),
179 | misplaced_directive('onInputFieldDefinition', 'ARGUMENT_DEFINITION', 3, 30),
180 | misplaced_directive('onInputFieldDefinition', 'FIELD_DEFINITION', 3, 63),
181 | misplaced_directive('onEnum', 'SCALAR', 6, 25),
182 | misplaced_directive('onObject', 'INTERFACE', 8, 31),
183 | misplaced_directive('onInputFieldDefinition', 'ARGUMENT_DEFINITION', 9, 30),
184 | misplaced_directive('onInputFieldDefinition', 'FIELD_DEFINITION', 9, 63),
185 | misplaced_directive('onEnumValue', 'UNION', 12, 23),
186 | misplaced_directive('onScalar', 'ENUM', 14, 21),
187 | misplaced_directive('onUnion', 'ENUM_VALUE', 15, 20),
188 | misplaced_directive('onEnum', 'INPUT_OBJECT', 18, 23),
189 | misplaced_directive('onArgumentDefinition', 'INPUT_FIELD_DEFINITION', 19, 24),
190 | misplaced_directive('onObject', 'SCHEMA', 22, 16),
191 | ]);
192 | };
193 | };
194 |
195 | done_testing;
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | GraphQL - A Perl port of the reference implementation of [GraphQL](http://graphql.org/).
4 |
5 | # SYNOPSIS
6 |
7 | use GraphQL qw/:types graphql/;
8 |
9 | my $schema = GraphQLSchema(
10 | query => $Query,
11 | mutation => $Mutation;
12 | );
13 |
14 | my $result = graphql($schema, $query);
15 |
16 | # DESCRIPTION
17 |
18 | GraphQL is a port of the [reference GraphQL implementation](https://github.com/graphql/graphql-js)
19 | implements GraphQL types, parser, validation, execution, and introspection.
20 |
21 | # TYPES
22 |
23 | To import all available GraphQL types use `:types` tag from [GraphQL](https://metacpan.org/pod/GraphQL) class.
24 |
25 | ## Object Types and Fields
26 |
27 | ## Object
28 |
29 | Object represents a list of named fields, each of which yield a value of a
30 | specific type.
31 |
32 | GraphQLObjectType(
33 | name => '',
34 | fields => {
35 | ...
36 | },
37 | );
38 |
39 | Possible parameters of an object:
40 |
41 | - name;
42 | - fields - see ["Fields"](#fields);
43 | - description - optional;
44 | - interfaces - optional;
45 | - is\_type\_of - optional;
46 |
47 | ### Fields
48 |
49 | List of named fields.
50 |
51 | {
52 | args => {
53 | ...
54 | },
55 | type => GraphQLString,
56 | resolve => sub {
57 | my ($obj, $args) = @_;
58 | ...
59 | },
60 | }
61 |
62 | Possible argument of a field:
63 |
64 | - type;
65 | - args - see ["Arguments"](#arguments);
66 | - resolve - must a code ref if passed;
67 | - description - optional;
68 | - deprecation\_reason - optional;
69 |
70 | ### Arguments
71 |
72 | Arguments are applicable to fields and should defined like a HASH ref of
73 | arguments of HASH ref with type.
74 |
75 | {
76 | arg_name => {
77 | type => GraphQL,
78 | description => 'Argument description',
79 | },
80 | }
81 |
82 | Possible parameters of an argument:
83 |
84 | - type;
85 | - description - optional;
86 | - default\_value - optional;
87 |
88 | [GraphQL::Language::Object](https://metacpan.org/pod/GraphQL::Language::Object)
89 |
90 | ## Scalar Types
91 |
92 | GraphQL provides a number of built‐in scalars, but type systems can add
93 | additional scalars with semantic meaning.
94 |
95 | - GraphQLBoolean
96 | - GraphQLFloat
97 | - GraphQLInt
98 | - GraphQLID
99 | - GraphQLString
100 |
101 | [GraphQL::Language::Scalar](https://metacpan.org/pod/GraphQL::Language::Scalar)
102 |
103 | ## Enumeration Types
104 |
105 | Enumeration types are a special kind of scalar that is restricted to a
106 | particular set of allowed values.
107 |
108 | GraphQLEnumType(
109 | name => 'Color',
110 | values => {
111 | RED => { value => 0 },
112 | GREEN => { value => 1 },
113 | BLUE => { value => 2 },
114 | },
115 | );
116 |
117 | [GraphQL::Language::Enum](https://metacpan.org/pod/GraphQL::Language::Enum)
118 |
119 | ## Lists
120 |
121 | List modifier marks type as _List_, which indicates that this field will return
122 | an array of that type.
123 |
124 | GraphQLList($Type);
125 |
126 | The ["Non-Null"](#non-null) and ["List"](#list) modifiers can be combined.
127 |
128 | GraphQLList(GraphQLNonNull($Type)); # [$Type!]
129 |
130 | [GraphQL::Language::List](https://metacpan.org/pod/GraphQL::Language::List)
131 |
132 | ## Non-Null
133 |
134 | The Non-Null type modifier means that server always expects to return a
135 | non-null value for a field.
136 | Getting a null value will trigger a GraphQL execution error, letting the client
137 | know that something has gone wrong.
138 |
139 | GraphQLList($Type);
140 |
141 | The ["Non-Null"](#non-null) and ["List"](#list) modifiers can be combined.
142 |
143 | GraphQLNonNull(GraphQLList($Type)); # [$Type]!
144 |
145 | [GraphQL::Language::NonNull](https://metacpan.org/pod/GraphQL::Language::NonNull)
146 |
147 | ## Interfaces
148 |
149 | Like many type systems, GraphQL supports interfaces. An Interface is an abstract
150 | type that includes a certain set of fields that a type must include to implement
151 | the interface.
152 |
153 | - name;
154 | - fields - see ["Fields"](#fields);
155 | - description - optional;
156 | - resolve\_type - must be a CODE ref, optional;
157 |
158 | GraphQLInterfaceType(
159 | name => 'Interface',
160 | fields => {
161 | ...
162 | },
163 | resolve_type => {
164 | my ($obj, $context, $info) = @_;
165 | ...
166 | }
167 | );
168 |
169 | [GraphQL::Language::Interface](https://metacpan.org/pod/GraphQL::Language::Interface)
170 |
171 | ## Union Types
172 |
173 | Union types are very similar to interfaces, but they don't get to specify any
174 | common fields between the types.
175 |
176 | GraphQLUnionType(
177 | name => 'Union',
178 | types => [$Type0, $Type1],
179 | );
180 |
181 | [GraphQL::Language::Union](https://metacpan.org/pod/GraphQL::Language::Union)
182 |
183 | ## Schema
184 |
185 | Every GraphQL service has a _query_ type and may or may not have a _mutation_ type.
186 | These types are the same as a regular object type, but they are special because
187 | they define the entry point of every GraphQL query.
188 |
189 | GraphQLSchema(
190 | query => $Query,
191 | mutation => $Mutation,
192 | );
193 |
194 | [GraphQL::Type::Schema](https://metacpan.org/pod/GraphQL::Type::Schema).
195 |
196 | # INTROSPECTION
197 |
198 | [GraphQL::Type::Introspection](https://metacpan.org/pod/GraphQL::Type::Introspection).
199 |
200 | # LIMITATIONS
201 |
202 | `Boolean`, `NULL`.
203 |
204 | # EXAMPLES
205 |
206 | See _examples_ directory.
207 |
208 | # GITHUB
209 |
210 | [https://github.com/khrt/graphql-perl](https://github.com/khrt/graphql-perl)
211 |
212 | # AUTHOR
213 |
214 | Artur Khabibullin - rtkh@cpan.org
215 |
216 | # LICENSE
217 |
218 | This module and all the modules in this package are governed by the same license
219 | as Perl itself.
220 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/possible_nonnull_arguments.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub missing_field_arg {
15 | my ($fieldName, $argName, $typeName, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::ProvidedNonNullArguments::missing_field_arg_message($fieldName, $argName, $typeName),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | sub missing_directive_arg {
24 | my ($directiveName, $argName, $typeName, $line, $column) = @_;
25 | return {
26 | message => GraphQL::Validator::Rule::ProvidedNonNullArguments::missing_directive_arg_message($directiveName, $argName, $typeName),
27 | locations => [{ line => $line, column => $column }],
28 | path => undef,
29 | };
30 | }
31 |
32 | subtest 'ignores unknown arguments' => sub {
33 | expect_passes_rule('ProvidedNonNullArguments', '
34 | {
35 | dog {
36 | isHousetrained(unknownArgument: true)
37 | }
38 | }
39 | ');
40 | };
41 |
42 | subtest 'Valid non-nullable value' => sub {
43 | subtest 'Arg on optional arg' => sub {
44 | expect_passes_rule('ProvidedNonNullArguments', '
45 | {
46 | dog {
47 | isHousetrained(atOtherHomes: true)
48 | }
49 | }
50 | ');
51 | };
52 |
53 | subtest 'No Arg on optional arg' => sub {
54 | expect_passes_rule('ProvidedNonNullArguments', '
55 | {
56 | dog {
57 | isHousetrained
58 | }
59 | }
60 | ');
61 | };
62 |
63 | subtest 'Multiple args' => sub {
64 | expect_passes_rule('ProvidedNonNullArguments', '
65 | {
66 | complicatedArgs {
67 | multipleReqs(req1: 1, req2: 2)
68 | }
69 | }
70 | ');
71 | };
72 |
73 | subtest 'Multiple args reverse order' => sub {
74 | expect_passes_rule('ProvidedNonNullArguments', '
75 | {
76 | complicatedArgs {
77 | multipleReqs(req2: 2, req1: 1)
78 | }
79 | }
80 | ');
81 | };
82 |
83 | subtest 'No args on multiple optional' => sub {
84 | expect_passes_rule('ProvidedNonNullArguments', '
85 | {
86 | complicatedArgs {
87 | multipleOpts
88 | }
89 | }
90 | ');
91 | };
92 |
93 | subtest 'One arg on multiple optional' => sub {
94 | expect_passes_rule('ProvidedNonNullArguments', '
95 | {
96 | complicatedArgs {
97 | multipleOpts(opt1: 1)
98 | }
99 | }
100 | ');
101 | };
102 |
103 | subtest 'Second arg on multiple optional' => sub {
104 | expect_passes_rule('ProvidedNonNullArguments', '
105 | {
106 | complicatedArgs {
107 | multipleOpts(opt2: 1)
108 | }
109 | }
110 | ');
111 | };
112 |
113 | subtest 'Multiple reqs on mixedList' => sub {
114 | expect_passes_rule('ProvidedNonNullArguments', '
115 | {
116 | complicatedArgs {
117 | multipleOptAndReq(req1: 3, req2: 4)
118 | }
119 | }
120 | ');
121 | };
122 |
123 | subtest 'Multiple reqs and one opt on mixedList' => sub {
124 | expect_passes_rule('ProvidedNonNullArguments', '
125 | {
126 | complicatedArgs {
127 | multipleOptAndReq(req1: 3, req2: 4, opt1: 5)
128 | }
129 | }
130 | ');
131 | };
132 |
133 | subtest 'All reqs and opts on mixedList' => sub {
134 | expect_passes_rule('ProvidedNonNullArguments', '
135 | {
136 | complicatedArgs {
137 | multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
138 | }
139 | }
140 | ');
141 | };
142 | };
143 |
144 | subtest 'Invalid non-nullable value' => sub {
145 | subtest 'Missing one non-nullable argument' => sub {
146 | expect_fails_rule('ProvidedNonNullArguments', '
147 | {
148 | complicatedArgs {
149 | multipleReqs(req2: 2)
150 | }
151 | }
152 | ', [
153 | missing_field_arg('multipleReqs', 'req1', 'Int!', 4, 13)
154 | ]);
155 | };
156 |
157 | subtest 'Missing multiple non-nullable arguments' => sub {
158 | plan skip_all => 'FAILS';
159 |
160 | expect_fails_rule('ProvidedNonNullArguments', '
161 | {
162 | complicatedArgs {
163 | multipleReqs
164 | }
165 | }
166 | ', [
167 | missing_field_arg('multipleReqs', 'req1', 'Int!', 4, 13),
168 | missing_field_arg('multipleReqs', 'req2', 'Int!', 4, 13),
169 | ]);
170 | };
171 |
172 | subtest 'Incorrect value and missing argument' => sub {
173 | expect_fails_rule('ProvidedNonNullArguments', '
174 | {
175 | complicatedArgs {
176 | multipleReqs(req1: "one")
177 | }
178 | }
179 | ', [
180 | missing_field_arg('multipleReqs', 'req2', 'Int!', 4, 13),
181 | ]);
182 | };
183 | };
184 |
185 | subtest 'Directive arguments' => sub {
186 | subtest 'ignores unknown directives' => sub {
187 | expect_passes_rule('ProvidedNonNullArguments', '
188 | {
189 | dog @unknown
190 | }
191 | ');
192 | };
193 |
194 | subtest 'with directives of valid types' => sub {
195 | expect_passes_rule('ProvidedNonNullArguments', '
196 | {
197 | dog @include(if: true) {
198 | name
199 | }
200 | human @skip(if: false) {
201 | name
202 | }
203 | }
204 | ');
205 | };
206 |
207 | subtest 'with directive with missing types' => sub {
208 | expect_fails_rule('ProvidedNonNullArguments', '
209 | {
210 | dog @include {
211 | name @skip
212 | }
213 | }
214 | ', [
215 | missing_directive_arg('include', 'if', 'Boolean!', 3, 15),
216 | missing_directive_arg('skip', 'if', 'Boolean!', 4, 18)
217 | ]);
218 | };
219 | };
220 |
221 | done_testing;
222 |
--------------------------------------------------------------------------------
/lib/GraphQL/Validator/Context.pm:
--------------------------------------------------------------------------------
1 | package GraphQL::Validator::Context;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use Carp qw/longmess/;
7 | use DDP {
8 | # indent => 2,
9 | # max_depth => 5,
10 | # index => 0,
11 | # class => {
12 | # internals => 0,
13 | # show_methods => 'none',
14 | # },
15 | # filters => {
16 | # 'GraphQL::Language::Token' => sub { shift->desc },
17 | # 'GraphQL::Language::Source' => sub { shift->name },
18 |
19 | # 'GraphQL::Type::Enum' => sub { shift->to_string },
20 | # 'GraphQL::Type::InputObject' => sub { shift->to_string },
21 | # 'GraphQL::Type::Interface' => sub { shift->to_string },
22 | # 'GraphQL::Type::List' => sub { shift->to_string },
23 | # 'GraphQL::Type::NonNull' => sub { shift->to_string },
24 | # 'GraphQL::Type::Object' => sub { shift->to_string },
25 | # 'GraphQL::Type::Scalar' => sub { shift->to_string },
26 | # 'GraphQL::Type::Union' => sub { shift->to_string },
27 | # },
28 | # caller_info => 0,
29 | };
30 | use List::Util qw/reduce/;
31 |
32 | use GraphQL::Language::Kinds qw/Kind/;
33 | use GraphQL::Language::Parser;
34 | use GraphQL::Language::Visitor qw/
35 | visit
36 | visit_with_typeinfo
37 | /;
38 |
39 | sub new {
40 | my ($class, %args) = @_;
41 |
42 | my $self = bless {
43 | schema => $args{schema},
44 | ast => $args{ast},
45 | type_info => $args{type_info},
46 |
47 | errors => [],
48 | # fragments => {},
49 | fragment_spreads => {},
50 | recursively_referenced_fragments => {},
51 | variable_usages => {},
52 | recursive_variable_usages => {},
53 | }, $class;
54 |
55 | return $self;
56 | }
57 |
58 | sub report_error {
59 | my ($self, $error) = @_;
60 | push @{ $self->{errors} }, $error;
61 | }
62 |
63 | sub get_errors { shift->{errors} }
64 | sub get_schema { shift->{schema} }
65 | sub get_document { shift->{ast} }
66 |
67 | sub get_fragment {
68 | my ($self, $name) = @_;
69 |
70 | my $fragments = $self->{fragments};
71 | if (!$fragments) {
72 | $self->{fragments} = $fragments = reduce {
73 | if ($b->{kind} eq Kind->FRAGMENT_DEFINITION) {
74 | $a->{ $b->{name}{value} } = $b;
75 | }
76 | $a;
77 | } {}, @{ $self->get_document->{definitions} };
78 | }
79 |
80 | return $fragments->{ $name };
81 | }
82 |
83 | sub get_fragment_spreads {
84 | my ($self, $node) = @_;
85 |
86 | # print 'node '; p $node;
87 | # warn longmess 'spreads';
88 |
89 | my $spreads = $self->{fragment_spreads}{$node};
90 | if (!$spreads) {
91 | $spreads = [];
92 |
93 | my @sets_to_visit = ($node);
94 | while (@sets_to_visit) {
95 | my $set = pop @sets_to_visit;
96 |
97 | for my $selection (@{ $set->{selections} }) {
98 | if ($selection->{kind} eq Kind->FRAGMENT_SPREAD) {
99 | push @$spreads, $selection;
100 | }
101 | elsif ($selection->{selection_set}) {
102 | push @sets_to_visit, $selection->{selection_set};
103 | }
104 | }
105 | }
106 |
107 | $self->{fragment_spreads}{ $node } = $spreads;
108 | }
109 |
110 | return $spreads;
111 | }
112 |
113 | sub get_recursively_referenced_fragments {
114 | my ($self, $operation) = @_;
115 |
116 | my $fragments = $self->{recursively_referenced_fragments}{ $operation };
117 | if (!$fragments) {
118 | $fragments = [];
119 |
120 | my %collected_names;
121 | my @nodes_to_visit = ($operation->{selection_set});
122 | while (@nodes_to_visit) {
123 | my $node = pop @nodes_to_visit;
124 | my $spreads = $self->get_fragment_spreads($node);
125 |
126 | for my $spread (@$spreads) {
127 | my $frag_name = $spread->{name}{value};
128 |
129 | unless ($collected_names{ $frag_name }) {
130 | $collected_names{ $frag_name } = 1;
131 | my $fragment = $self->get_fragment($frag_name);
132 |
133 | if ($fragment) {
134 | push @$fragments, $fragment;
135 | push @nodes_to_visit, $fragment->{selection_set};
136 | }
137 | }
138 | }
139 | }
140 |
141 | $self->{recursively_referenced_fragments}{ $operation } = $fragments;
142 | }
143 |
144 | return $fragments;
145 | }
146 |
147 | sub get_variable_usages {
148 | my ($self, $node) = @_;
149 |
150 | my $usages = $self->{variable_usages}{ $node };
151 | if (!$usages) {
152 | my @new_usages;
153 | my $type_info = GraphQL::TypeInfo->new($self->{schema});
154 |
155 | visit($node, visit_with_typeinfo($type_info, {
156 | VariableDefinition => sub { 0 },
157 | Variable => sub {
158 | my (undef, $variable) = @_;
159 | push @new_usages, { node => $variable, type => $type_info->get_input_type };
160 | return;
161 | }
162 | }));
163 |
164 | $usages = \@new_usages;
165 | $self->{_variable_usages}{ $node } = $usages;
166 | }
167 |
168 | return $usages;
169 | }
170 |
171 | sub get_recursive_variable_usages {
172 | my ($self, $operation) = @_;
173 |
174 | my $usages = $self->{recursive_variable_usages}{ $operation };
175 | if (!$usages) {
176 | $usages = $self->get_variable_usages($operation);
177 |
178 | my $fragments = $self->get_recursively_referenced_fragments($operation);
179 | for my $fragment (@$fragments) {
180 | push @$usages, @{ $self->get_variable_usages($fragment) };
181 | }
182 |
183 | $self->{recursive_variable_usages}{ $operation } = $usages;
184 | }
185 |
186 | return $usages;
187 | }
188 |
189 | sub get_type { shift->{type_info}->get_type }
190 | sub get_parent_type { shift->{type_info}->get_parent_type }
191 | sub get_input_type { shift->{type_info}->get_input_type }
192 | sub get_field_def { shift->{type_info}->get_field_def }
193 | sub get_directive { shift->{type_info}->get_directive }
194 | sub get_argument { shift->{type_info}->get_argument }
195 |
196 | 1;
197 |
198 | __END__
199 |
--------------------------------------------------------------------------------
/t/graphql/execute-lists.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use DDP;
6 | use Test::Deep;
7 | use Test::More;
8 | use JSON qw/encode_json/;
9 |
10 | use GraphQL qw/:types/;
11 | use GraphQL::Error qw/format_error/;
12 | use GraphQL::Language::Parser qw/parse/;
13 | use GraphQL::Execute qw/execute/;
14 |
15 | sub check {
16 | my ($test_type, $test_data, $expected, $name) = @_;
17 |
18 | my $data = { test => $test_data };
19 |
20 | my $data_type;
21 | $data_type = GraphQLObjectType(
22 | name => 'DataType',
23 | fields => sub { {
24 | test => { type => $test_type },
25 | nest => { type => $data_type, resolve => sub { $data } },
26 | } },
27 | );
28 |
29 | my $schema = GraphQLSchema(query => $data_type);
30 | my $ast = parse('{ nest { test } }');
31 |
32 | my $result = execute($schema, $ast, $data);
33 | cmp_deeply $result, $expected, $name;
34 | }
35 |
36 | subtest 'Execute: Accepts any iterable as list value' => sub {
37 | # check(
38 | # GraphQLList(GraphQLString),
39 | # new Set(['apple', 'banana', 'apple', 'coconut']),
40 | # { data => { nest => { test => ['apple', 'banana', 'coconut'] } } },
41 | # 'Accepts a Set as a List value'
42 | # );
43 |
44 | # function* yieldItems() {
45 | # yield 'one';
46 | # yield 2;
47 | # yield true;
48 | # }
49 |
50 | # check(
51 | # GraphQLList(GraphQLString),
52 | # yieldItems(),
53 | # { data => { nest => { test => ['one', '2', 'true'] } } },
54 | # 'Accepts an Generator function as a List value'
55 | # );
56 |
57 | check(
58 | GraphQLList(GraphQLString),
59 | ['one', 'two'],
60 | { data => { nest => { test => ['one', 'two'] } } },
61 | 'Accepts function arguments as a List value'
62 | );
63 |
64 | check(
65 | GraphQLList(GraphQLString),
66 | 'Singluar',
67 | {
68 | data => { nest => { test => undef } },
69 | errors => [noclass(superhashof({
70 | message => "Expected Iterable, but did not find one for field DataType.test.\n",
71 | locations => [{ line => 1, column => 10 }],
72 | # path => ['nest', 'test'],
73 | }))],
74 | },
75 | 'Does not accept (Iterable) String-literal as a List value'
76 | );
77 | };
78 |
79 | subtest 'Execute: Handles list nullability' => sub {
80 | subtest '[T]' => sub {
81 | my $type = GraphQLList(GraphQLInt);
82 |
83 | check(
84 | $type, [1, 2],
85 | { data => { nest => { test => [1, 2] } } },
86 | 'Contains values'
87 | );
88 |
89 | check(
90 | $type,
91 | [1, undef, 2],
92 | { data => { nest => { test => [1, undef, 2] } } },
93 | 'Contains null'
94 | );
95 |
96 | check($type, undef, { data => { nest => { test => undef } } },
97 | 'Returns null');
98 | };
99 |
100 | plan skip_all => 'TODO';
101 |
102 | subtest '[T]!' => sub {
103 | my $type = GraphQLNonNull(GraphQLList(GraphQLInt));
104 |
105 | check(
106 | $type, [1, 2],
107 | { data => { nest => { test => [1, 2] } } },
108 | 'Contains values'
109 | );
110 |
111 | check(
112 | $type,
113 | [1, undef, 2],
114 | { data => { nest => { test => [1, undef, 2] } } },
115 | 'Contains null'
116 | );
117 |
118 | check(
119 | $type,
120 | undef,
121 | {
122 | data => { nest => undef },
123 | errors => [noclass(superhashof({
124 | message => 'Cannot return null for non-nullable field DataType.test.',
125 | locations => [{ line => 1, column => 10 }],
126 | # path => ['nest', 'test']
127 | }))],
128 | },
129 | 'Returns null'
130 | );
131 | };
132 |
133 | subtest '[T!]' => sub {
134 | my $type = GraphQLList(GraphQLNonNull(GraphQLInt));
135 |
136 | check(
137 | $type, [1, 2],
138 | { data => { nest => { test => [1, 2] } } },
139 | 'Contains values'
140 | );
141 |
142 | check(
143 | $type,
144 | [1, undef, 2],
145 | {
146 | data => { nest => { test => undef } },
147 | errors => [noclass(superhashof({
148 | message => 'Cannot return null for non-nullable field DataType.test.',
149 | locations => [{ line => 1, column => 10 }],
150 | # path => ['nest', 'test', 1]
151 | }))],
152 | },
153 | 'Contains null'
154 | );
155 |
156 | check(
157 | $type,
158 | undef,
159 | { data => { nest => { test => undef } } },
160 | 'Returns null'
161 | );
162 | };
163 |
164 | subtest '[T!]!' => sub {
165 | my $type = GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLInt)));
166 |
167 | check(
168 | $type, [1, 2],
169 | { data => { nest => { test => [1, 2] } } },
170 | 'Contains values'
171 | );
172 |
173 | check(
174 | $type,
175 | [1, undef, 2],
176 | {
177 | data => { nest => undef },
178 | errors => [noclass(superhashof({
179 | message => 'Cannot return null for non-nullable field DataType.test.',
180 | locations => [{ line => 1, column => 10 }],
181 | # path => ['nest', 'test', 1]
182 | }))],
183 | },
184 | 'Contains null'
185 | );
186 |
187 | check(
188 | $type,
189 | undef,
190 | {
191 | data => { nest => undef },
192 | errors => [noclass(superhashof({
193 | message => 'Cannot return null for non-nullable field DataType.test.',
194 | locations => [{ line => 1, column => 10 }],
195 | # path => ['nest', 'test']
196 | }))],
197 | },
198 | 'Returns null'
199 | );
200 | };
201 | };
202 |
203 | done_testing;
204 |
--------------------------------------------------------------------------------
/t/graphql/execute-schema.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use DDP;
6 | use Test::More;
7 | use Test::Deep;
8 | use JSON qw/encode_json/;
9 |
10 | use GraphQL qw/graphql :types/;
11 | use GraphQL::Language::Parser qw/parse/;
12 | use GraphQL::Execute qw/execute/;
13 |
14 | subtest 'executes using a schema' => sub {
15 | my ($johnSmith, $article);
16 | $article = sub {
17 | my $id = shift;
18 |
19 | return {
20 | id => $id,
21 | isPublished => 'true',
22 | author => $johnSmith,
23 | title => "My Article $id",
24 | body => 'This is a post',
25 | hidden => 'This data is not exposed in the schema',
26 | keywords => ['foo', 'bar', 1, 1, undef]
27 | };
28 | };
29 |
30 | $johnSmith = {
31 | id => 123,
32 | name => 'John Smith',
33 | pic => sub {
34 | my ($width, $height) = @_;
35 | get_pic(123, $width, $height);
36 | },
37 | recentArticle => $article->(1),
38 | };
39 |
40 | my $BlogImage = GraphQLObjectType(
41 | name => 'Image',
42 | fields => {
43 | url => { type => GraphQLString },
44 | width => { type => GraphQLInt },
45 | height => { type => GraphQLInt },
46 | },
47 | );
48 |
49 | my $BlogArticle;
50 | my $BlogAuthor = GraphQLObjectType(
51 | name => 'Author',
52 | fields => sub { {
53 | id => { type => GraphQLString },
54 | name => { type => GraphQLString },
55 | pic => {
56 | args => {
57 | width => { type => GraphQLInt },
58 | height => { type => GraphQLInt }
59 | },
60 | type => $BlogImage,
61 | resolve => sub {
62 | my ($obj, $args) = @_;
63 | $obj->{pic}->($args->{width}, $args->{height})
64 | },
65 | },
66 | recentArticle => { type => $BlogArticle }
67 | } },
68 | );
69 |
70 | $BlogArticle = GraphQLObjectType(
71 | name => 'Article',
72 | fields => {
73 | id => { type => GraphQLNonNull(GraphQLString) },
74 | isPublished => { type => GraphQLBoolean },
75 | author => { type => $BlogAuthor },
76 | title => { type => GraphQLString },
77 | body => { type => GraphQLString },
78 | keywords => { type => GraphQLList(GraphQLString) }
79 | }
80 | );
81 |
82 | my $BlogQuery = GraphQLObjectType(
83 | name => 'Query',
84 | fields => {
85 | article => {
86 | type => $BlogArticle,
87 | args => { id => { type => GraphQLID } },
88 | resolve => sub {
89 | my (undef, $args) = @_;
90 | $article->($args->{id});
91 | },
92 | },
93 | feed => {
94 | type => GraphQLList($BlogArticle),
95 | resolve => sub {
96 | [
97 | $article->(1),
98 | $article->(2),
99 | $article->(3),
100 | $article->(4),
101 | $article->(5),
102 | $article->(6),
103 | $article->(7),
104 | $article->(8),
105 | $article->(9),
106 | $article->(10)
107 | ]
108 | },
109 | },
110 | }
111 | );
112 |
113 | my $BlogSchema = GraphQLSchema(
114 | query => $BlogQuery
115 | );
116 |
117 | sub get_pic {
118 | my ($uid, $width, $height) = @_;
119 | return {
120 | url => "cdn://$uid",
121 | width => "$width",
122 | height => "$height",
123 | };
124 | }
125 |
126 | my $request = <<'EOQ';
127 | {
128 | feed {
129 | id,
130 | title
131 | },
132 | article(id: "1") {
133 | ...articleFields,
134 | author {
135 | id,
136 | name,
137 | pic(width: 640, height: 480) {
138 | url,
139 | width,
140 | height
141 | },
142 | recentArticle {
143 | ...articleFields,
144 | keywords
145 | }
146 | }
147 | }
148 | }
149 |
150 | fragment articleFields on Article {
151 | id,
152 | isPublished,
153 | title,
154 | body,
155 | hidden,
156 | notdefined
157 | }
158 | EOQ
159 |
160 | # Note: this is intentionally not validating to ensure appropriate
161 | # behavior occurs when executing an invalid query.
162 | is_deeply execute($BlogSchema, parse($request)), {
163 | data => {
164 | feed => [
165 | { id => '1', title => 'My Article 1' },
166 | { id => '2', title => 'My Article 2' },
167 | { id => '3', title => 'My Article 3' },
168 | { id => '4', title => 'My Article 4' },
169 | { id => '5', title => 'My Article 5' },
170 | { id => '6', title => 'My Article 6' },
171 | { id => '7', title => 'My Article 7' },
172 | { id => '8', title => 'My Article 8' },
173 | { id => '9', title => 'My Article 9' },
174 | { id => '10', title => 'My Article 10' }
175 | ],
176 | article => {
177 | id => '1',
178 | isPublished => 1,
179 | title => 'My Article 1',
180 | body => 'This is a post',
181 |
182 | author => {
183 | id => '123',
184 | name => 'John Smith',
185 | pic => {
186 | url => 'cdn://123',
187 | width => 640,
188 | height => 480
189 | },
190 | recentArticle => {
191 | id => '1',
192 | isPublished => 1,
193 | title => 'My Article 1',
194 | body => 'This is a post',
195 | keywords => ['foo', 'bar', '1', '1', undef],
196 | }
197 | }
198 | }
199 | }
200 | };
201 | };
202 |
203 | done_testing;
204 |
--------------------------------------------------------------------------------
/t/graphql/validator/rules/possible_fragment_spreads.t:
--------------------------------------------------------------------------------
1 |
2 | use strict;
3 | use warnings;
4 |
5 | use Test::More;
6 |
7 | use FindBin qw/$Bin/;
8 | use lib "$Bin/../../..";
9 | use harness qw/
10 | expect_passes_rule
11 | expect_fails_rule
12 | /;
13 |
14 | sub error {
15 | my ($fragName, $parentType, $fragType, $line, $column) = @_;
16 | return {
17 | message => GraphQL::Validator::Rule::PossibleFragmentSpreads::type_incompatible_spread_message($fragName, $parentType, $fragType),
18 | locations => [{ line => $line, column => $column }],
19 | path => undef,
20 | };
21 | }
22 |
23 | sub error_anon {
24 | my ($parentType, $fragType, $line, $column) = @_;
25 | return {
26 | message => GraphQL::Validator::Rule::PossibleFragmentSpreads::type_incompatible_anon_spread_message($parentType, $fragType),
27 | locations => [{ line => $line, column => $column }],
28 | path => undef,
29 | };
30 | }
31 |
32 | subtest 'of the same object' => sub {
33 | expect_passes_rule('PossibleFragmentSpreads', '
34 | fragment objectWithinObject on Dog { ...dogFragment }
35 | fragment dogFragment on Dog { barkVolume }
36 | ');
37 | };
38 |
39 | subtest 'of the same object with inline fragment' => sub {
40 | expect_passes_rule('PossibleFragmentSpreads', '
41 | fragment objectWithinObjectAnon on Dog { ... on Dog { barkVolume } }
42 | ');
43 | };
44 |
45 | subtest 'object into an implemented interface' => sub {
46 | expect_passes_rule('PossibleFragmentSpreads', '
47 | fragment objectWithinInterface on Pet { ...dogFragment }
48 | fragment dogFragment on Dog { barkVolume }
49 | ');
50 | };
51 |
52 | subtest 'object into containing union' => sub {
53 | expect_passes_rule('PossibleFragmentSpreads', '
54 | fragment objectWithinUnion on CatOrDog { ...dogFragment }
55 | fragment dogFragment on Dog { barkVolume }
56 | ');
57 | };
58 |
59 | subtest 'union into contained object' => sub {
60 | expect_passes_rule('PossibleFragmentSpreads', '
61 | fragment unionWithinObject on Dog { ...catOrDogFragment }
62 | fragment catOrDogFragment on CatOrDog { __typename }
63 | ');
64 | };
65 |
66 | subtest 'union into overlapping interface' => sub {
67 | expect_passes_rule('PossibleFragmentSpreads', '
68 | fragment unionWithinInterface on Pet { ...catOrDogFragment }
69 | fragment catOrDogFragment on CatOrDog { __typename }
70 | ');
71 | };
72 |
73 | subtest 'union into overlapping union' => sub {
74 | expect_passes_rule('PossibleFragmentSpreads', '
75 | fragment unionWithinUnion on DogOrHuman { ...catOrDogFragment }
76 | fragment catOrDogFragment on CatOrDog { __typename }
77 | ');
78 | };
79 |
80 | subtest 'interface into implemented object' => sub {
81 | expect_passes_rule('PossibleFragmentSpreads', '
82 | fragment interfaceWithinObject on Dog { ...petFragment }
83 | fragment petFragment on Pet { name }
84 | ');
85 | };
86 |
87 | subtest 'interface into overlapping interface' => sub {
88 | expect_passes_rule('PossibleFragmentSpreads', '
89 | fragment interfaceWithinInterface on Pet { ...beingFragment }
90 | fragment beingFragment on Being { name }
91 | ');
92 | };
93 |
94 | subtest 'interface into overlapping interface in inline fragment' => sub {
95 | expect_passes_rule('PossibleFragmentSpreads', '
96 | fragment interfaceWithinInterface on Pet { ... on Being { name } }
97 | ');
98 | };
99 |
100 | subtest 'interface into overlapping union' => sub {
101 | expect_passes_rule('PossibleFragmentSpreads', '
102 | fragment interfaceWithinUnion on CatOrDog { ...petFragment }
103 | fragment petFragment on Pet { name }
104 | ');
105 | };
106 |
107 | subtest 'different object into object' => sub {
108 | expect_fails_rule('PossibleFragmentSpreads', '
109 | fragment invalidObjectWithinObject on Cat { ...dogFragment }
110 | fragment dogFragment on Dog { barkVolume }
111 | ', [error('dogFragment', 'Cat', 'Dog', 2, 51)]);
112 | };
113 |
114 | subtest 'different object into object in inline fragment' => sub {
115 | expect_fails_rule('PossibleFragmentSpreads', '
116 | fragment invalidObjectWithinObjectAnon on Cat {
117 | ... on Dog { barkVolume }
118 | }
119 | ', [error_anon('Cat', 'Dog', 3, 9)]);
120 | };
121 |
122 | subtest 'object into not implementing interface' => sub {
123 | expect_fails_rule('PossibleFragmentSpreads', '
124 | fragment invalidObjectWithinInterface on Pet { ...humanFragment }
125 | fragment humanFragment on Human { pets { name } }
126 | ', [error('humanFragment', 'Pet', 'Human', 2, 54)]);
127 | };
128 |
129 | subtest 'object into not containing union' => sub {
130 | expect_fails_rule('PossibleFragmentSpreads', '
131 | fragment invalidObjectWithinUnion on CatOrDog { ...humanFragment }
132 | fragment humanFragment on Human { pets { name } }
133 | ', [error('humanFragment', 'CatOrDog', 'Human', 2, 55)]);
134 | };
135 |
136 | subtest 'union into not contained object' => sub {
137 | expect_fails_rule('PossibleFragmentSpreads', '
138 | fragment invalidUnionWithinObject on Human { ...catOrDogFragment }
139 | fragment catOrDogFragment on CatOrDog { __typename }
140 | ', [error('catOrDogFragment', 'Human', 'CatOrDog', 2, 52)]);
141 | };
142 |
143 | subtest 'union into non overlapping interface' => sub {
144 | expect_fails_rule('PossibleFragmentSpreads', '
145 | fragment invalidUnionWithinInterface on Pet { ...humanOrAlienFragment }
146 | fragment humanOrAlienFragment on HumanOrAlien { __typename }
147 | ', [error('humanOrAlienFragment', 'Pet', 'HumanOrAlien', 2, 53)]);
148 | };
149 |
150 | subtest 'union into non overlapping union' => sub {
151 | expect_fails_rule('PossibleFragmentSpreads', '
152 | fragment invalidUnionWithinUnion on CatOrDog { ...humanOrAlienFragment }
153 | fragment humanOrAlienFragment on HumanOrAlien { __typename }
154 | ', [error('humanOrAlienFragment', 'CatOrDog', 'HumanOrAlien', 2, 54)]);
155 | };
156 |
157 | subtest 'interface into non implementing object' => sub {
158 | expect_fails_rule('PossibleFragmentSpreads', '
159 | fragment invalidInterfaceWithinObject on Cat { ...intelligentFragment }
160 | fragment intelligentFragment on Intelligent { iq }
161 | ', [error('intelligentFragment', 'Cat', 'Intelligent', 2, 54)]);
162 | };
163 |
164 | subtest 'interface into non overlapping interface' => sub {
165 | expect_fails_rule('PossibleFragmentSpreads', '
166 | fragment invalidInterfaceWithinInterface on Pet {
167 | ...intelligentFragment
168 | }
169 | fragment intelligentFragment on Intelligent { iq }
170 | ', [error('intelligentFragment', 'Pet', 'Intelligent', 3, 9)]);
171 | };
172 |
173 | subtest 'interface into non overlapping interface in inline fragment' => sub {
174 | expect_fails_rule('PossibleFragmentSpreads', '
175 | fragment invalidInterfaceWithinInterfaceAnon on Pet {
176 | ...on Intelligent { iq }
177 | }
178 | ', [error_anon('Pet', 'Intelligent', 3, 9)]);
179 | };
180 |
181 | subtest 'interface into non overlapping union' => sub {
182 | expect_fails_rule('PossibleFragmentSpreads', '
183 | fragment invalidInterfaceWithinUnion on HumanOrAlien { ...petFragment }
184 | fragment petFragment on Pet { name }
185 | ', [error('petFragment', 'HumanOrAlien', 'Pet', 2, 62)]);
186 | };
187 |
188 | done_testing;
189 |
--------------------------------------------------------------------------------