graphql-response_validator 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 59a6ee689aecc77afc491f97738976e94f41176120eb57ce4378cd94f921241d
4
+ data.tar.gz: 35021c4ccc45801799003f40c26af09267d70783e6bbdb9e088aac18f2945078
5
+ SHA512:
6
+ metadata.gz: de6caf3beae2a38edeeba7f76d290d48399b875ffa11594a6451b427969c98a685f1e9f2d4236f6143fe226276571d7865212d2b4650f99b6c639694b04e847a
7
+ data.tar.gz: 31143dca640e94861279b3406a348b2eb62624546028280aeed7648bfc6d59a7d7bea28582c6a25632e2a11160148b538b930a435469cb72a0b5a2fc183c0248
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ include:
15
+ - gemfile: Gemfile
16
+ ruby: 3.3
17
+ steps:
18
+ - run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV
19
+ - uses: actions/checkout@v4
20
+ - name: Setup Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby }}
24
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
25
+ - name: Run tests
26
+ run: |
27
+ gem install bundler -v 2.4.22
28
+ bundle install --jobs 4 --retry 3
29
+ bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,39 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+ .envrc
13
+ .byebug_history
14
+ .ruby-version
15
+ .rvmrc
16
+ .DS_Store
17
+
18
+ Gemfile.lock
19
+ secrets.json
20
+ example/*/*.graphql
21
+
22
+ ## Specific to RubyMotion:
23
+ .dat*
24
+ .repl_history
25
+ build/
26
+ *.bridgesupport
27
+ build-iPhoneOS/
28
+ build-iPhoneSimulator/
29
+
30
+ ## Documentation cache and generated files:
31
+ /.yardoc/
32
+ /_yardoc/
33
+ /doc/
34
+ /rdoc/
35
+
36
+ ## Environment normalization:
37
+ /.bundle/
38
+ /vendor/bundle
39
+ /lib/bundler/man/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+ gem 'pry'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Greg MacWilliam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # GraphQL response validator
2
+
3
+ Testing GraphQL queries using fixture responses runs the risk of false-positive tests when a query changes without its static response getting updated. This gem provides a simple utility for validating response fixtures against the shape of a query to assure that they match.
4
+
5
+ ```shell
6
+ gem "graphql-response_validator"
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ Build a test query and its response data into a `GraphQL::ResponseValidator`, then assert that the fixture is correct for the query as part of your test:
12
+
13
+ ```ruby
14
+ def test_my_stuff
15
+ request = %|{ widget { id title } }|
16
+ response = {
17
+ "data" => {
18
+ "widget" => {
19
+ "id" => "1",
20
+ "name" => "My widget", # << wrong, should be `title`
21
+ },
22
+ },
23
+ }
24
+
25
+ # check that the query is valid...
26
+ query = GraphQL::Query.new(MySchema, query: request)
27
+ assert query.static_errors.none?, query.static_errors.map(&:message).join("\n")
28
+
29
+ # check that the response is valid...
30
+ fixture = GraphQL::ResponseValidator.new(query, response)
31
+ assert fixture.valid?, fixture.errors.map(&:message).join("\n")
32
+ # Results in: "Expected data to provide field `widget.title`"
33
+ end
34
+ ```
35
+
36
+ ### Abstract selections
37
+
38
+ Abstract selections must include a type identity so that the validator knows what selection path(s) to follow. This can be done by including a `__typename` in abstract selection scopes:
39
+
40
+ ```ruby
41
+ def test_my_stuff
42
+ request = %|{
43
+ node(id: 1) {
44
+ ... on Product { title }
45
+ ... on Order { totalCost }
46
+ __typename
47
+ }
48
+ }|
49
+ response = {
50
+ "data" => {
51
+ "node" => {
52
+ "title" => "Ethereal wishing well",
53
+ "__typename" => "Product",
54
+ },
55
+ },
56
+ }
57
+
58
+ query = GraphQL::Query.new(MySchema, query: request)
59
+ fixture = GraphQL::ResponseValidator.new(query, response)
60
+
61
+ assert fixture.valid?, fixture.errors.first&.message
62
+ end
63
+ ```
64
+
65
+ Alternatively, you can use a system `__typename__` hint that exists only in response data, and this can be pruned from the response data after validating it:
66
+
67
+ ```ruby
68
+ def test_my_stuff
69
+ request = %|{
70
+ node(id: 1) {
71
+ ... on Product { title }
72
+ ... on Order { totalCost }
73
+ }
74
+ }|
75
+ response = {
76
+ "data" => {
77
+ "node" => {
78
+ "totalCost" => 23,
79
+ "__typename__" => "Order",
80
+ },
81
+ },
82
+ }
83
+
84
+ query = GraphQL::Query.new(MySchema, query: request)
85
+ fixture = GraphQL::ResponseValidator.new(query, response)
86
+
87
+ assert fixture.valid?, fixture.errors.first&.message
88
+
89
+ expected_result = { "data" => { "node" => { "totalCost" => 23 } } }
90
+ assert_equal expected_result, fixture.prune!.to_h
91
+ end
92
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t, args|
6
+ puts args
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task :default => :test
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "graphql/response_validator/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "graphql-response_validator"
8
+ spec.version = GraphQL::ResponseValidator::VERSION
9
+ spec.authors = ["Greg MacWilliam"]
10
+ spec.summary = "Validate that GraphQL response fixtures match their test queries."
11
+ spec.description = "Validate that GraphQL response fixtures match their test queries."
12
+ spec.homepage = "https://github.com/gmac/graphql-response_validator"
13
+ spec.license = "MIT"
14
+
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata = {
18
+ "homepage_uri" => "https://github.com/gmac/graphql-response_validator",
19
+ "changelog_uri" => "https://github.com/gmac/graphql-response_validator/releases",
20
+ "source_code_uri" => "https://github.com/gmac/graphql-response_validator",
21
+ "bug_tracker_uri" => "https://github.com/gmac/graphql-response_validator/issues",
22
+ }
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^test/})
26
+ end
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_runtime_dependency "graphql", ">= 2.0.0"
30
+
31
+ spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "rake", "~> 12.0"
33
+ spec.add_development_dependency "minitest", "~> 5.12"
34
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class ResponseValidator
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql"
4
+
5
+ module GraphQL
6
+ class ResponseValidator
7
+ ValidationError = Struct.new(:message, :path)
8
+
9
+ SYSTEM_TYPENAME = "__typename__"
10
+ SCALAR_VALIDATORS = {
11
+ "Boolean" => -> (data) { data.is_a?(TrueClass) || data.is_a?(FalseClass) },
12
+ "Float" => -> (data) { data.is_a?(Numeric) },
13
+ "ID" => -> (data) { data.is_a?(String) || data.is_a?(Integer) },
14
+ "Int" => -> (data) { data.is_a?(Integer) },
15
+ "JSON" => -> (data) { data.is_a?(Hash) },
16
+ "String" => -> (data) { data.is_a?(String) },
17
+ }.freeze
18
+
19
+ attr_reader :errors
20
+
21
+ def initialize(
22
+ query,
23
+ data,
24
+ scalar_validators: SCALAR_VALIDATORS,
25
+ system_typename: SYSTEM_TYPENAME
26
+ )
27
+ @query = query
28
+ @data = data
29
+ @errors = []
30
+ @valid = nil
31
+ @scalar_validators = scalar_validators
32
+ @system_typename = system_typename
33
+ @system_typenames = Set.new
34
+ end
35
+
36
+ def valid?
37
+ return @valid unless @valid.nil?
38
+
39
+ op = @query.selected_operation
40
+ parent_type = @query.root_type_for_operation(op.operation_type)
41
+ validate_selections(parent_type, op, @data["data"])
42
+ @valid = @errors.none?
43
+ end
44
+
45
+ def prune!
46
+ valid?
47
+ @system_typenames.each { _1.delete(@system_typename) }
48
+ self
49
+ end
50
+
51
+ def to_h
52
+ @data
53
+ end
54
+
55
+ private
56
+
57
+ def validate_selections(parent_type, parent_node, data_part, path = [])
58
+ if parent_type.non_null?
59
+ if !data_part.nil?
60
+ return validate_selections(parent_type.of_type, parent_node, data_part, path)
61
+ else
62
+ @errors << ValidationError.new("Expected non-null selection to provide value", path.dup)
63
+ return false
64
+ end
65
+
66
+ elsif data_part.nil?
67
+ # nullable node with a null value is okay
68
+ return true
69
+
70
+ elsif parent_type.list?
71
+ if data_part.is_a?(Array)
72
+ return data_part.all? { |item| validate_selections(parent_type.of_type, parent_node, item, path) }
73
+ else
74
+ @errors << ValidationError.new("Expected list selection to provide Array", path.dup)
75
+ return false
76
+ end
77
+
78
+ elsif parent_type.kind.leaf?
79
+ return validate_leaf(parent_type, data_part, path)
80
+
81
+ elsif !data_part.is_a?(Hash)
82
+ @errors << ValidationError.new("Expected composite selection to provide Hash", path.dup)
83
+ return false
84
+ end
85
+
86
+ parent_node.selections.all? do |node|
87
+ case node
88
+ when GraphQL::Language::Nodes::Field
89
+ begin
90
+ path << (node.alias || node.name)
91
+ unless data_part.key?(path.last)
92
+ @errors << ValidationError.new("Expected data to provide field", path.dup)
93
+ next false
94
+ end
95
+
96
+ next_value = data_part[path.last]
97
+ next_type = if node.name == "__typename"
98
+ annotation_type = @query.get_type(data_part[path.last])
99
+ unless annotation_type && @query.possible_types(parent_type).include?(annotation_type)
100
+ @errors << ValidationError.new("Expected selection to provide a possible type name of `#{parent_type.graphql_name}`", path.dup)
101
+ next false
102
+ end
103
+
104
+ @query.get_type("String")
105
+ else
106
+ @query.get_field(parent_type, node.name)&.type
107
+ end
108
+
109
+ unless next_type
110
+ @errors << ValidationError.new("Invalid selection of `#{parent_type.graphql_name}.#{node.name}`", path.dup)
111
+ next false
112
+ end
113
+
114
+ validate_selections(next_type, node, next_value, path)
115
+ ensure
116
+ path.pop
117
+ end
118
+
119
+ when GraphQL::Language::Nodes::InlineFragment
120
+ resolved_type = resolved_type(parent_type, data_part, path)
121
+ next false if resolved_type.nil?
122
+
123
+ fragment_type = node.type.nil? ? parent_type : @query.get_type(node.type.name)
124
+ next true unless @query.possible_types(fragment_type).include?(resolved_type)
125
+
126
+ validate_selections(fragment_type, node, data_part, path)
127
+
128
+ when GraphQL::Language::Nodes::FragmentSpread
129
+ resolved_type = resolved_type(parent_type, data_part, path)
130
+ next false if resolved_type.nil?
131
+
132
+ fragment_def = @query.fragments[node.name]
133
+ fragment_type = @query.get_type(fragment_def.type.name)
134
+ next true unless @query.possible_types(fragment_type).include?(resolved_type)
135
+
136
+ validate_selections(fragment_type, fragment_def, data_part, path)
137
+ end
138
+ end
139
+ end
140
+
141
+ def validate_leaf(parent_type, value, path)
142
+ valid = if parent_type.kind.enum?
143
+ parent_type.values.key?(value)
144
+ elsif parent_type.kind.scalar?
145
+ validator = @scalar_validators[parent_type.graphql_name]
146
+ validator.nil? || validator.call(value)
147
+ end
148
+
149
+ @errors << ValidationError.new("Expected a valid `#{parent_type.graphql_name}` value", path.dup) unless valid
150
+ valid
151
+ end
152
+
153
+ def resolved_type(parent_type, data_part, path)
154
+ return parent_type unless parent_type.kind.abstract?
155
+
156
+ typename = data_part["__typename"] || data_part[@system_typename]
157
+ if typename.nil?
158
+ @errors << ValidationError.new("Abstract position expects `__typename` or system typename hint", path.dup)
159
+ return nil
160
+ end
161
+
162
+ @system_typenames.add(data_part) if data_part.key?(@system_typename)
163
+ annotated_type = @query.get_type(typename)
164
+
165
+ if annotated_type.nil?
166
+ @errors << ValidationError.new("Abstract typename `#{typename}` is not a valid type", path.dup)
167
+ nil
168
+ elsif !@query.possible_types(parent_type).include?(annotated_type)
169
+ @errors << ValidationError.new("Abstract type `#{parent_type.graphql_name}` cannot be `#{typename}`", path.dup)
170
+ nil
171
+ else
172
+ annotated_type
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ require_relative "./response_validator/version"
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-response_validator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Greg MacWilliam
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-03-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.12'
69
+ description: Validate that GraphQL response fixtures match their test queries.
70
+ email:
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - ".github/workflows/ci.yml"
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE
79
+ - README.md
80
+ - Rakefile
81
+ - graphql-response_validator.gemspec
82
+ - lib/graphql/response_validator.rb
83
+ - lib/graphql/response_validator/version.rb
84
+ homepage: https://github.com/gmac/graphql-response_validator
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://github.com/gmac/graphql-response_validator
89
+ changelog_uri: https://github.com/gmac/graphql-response_validator/releases
90
+ source_code_uri: https://github.com/gmac/graphql-response_validator
91
+ bug_tracker_uri: https://github.com/gmac/graphql-response_validator/issues
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.0.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.2.15
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Validate that GraphQL response fixtures match their test queries.
111
+ test_files: []