├── 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 | --------------------------------------------------------------------------------