From 4675dab2fa824bce9156ff87fff584795e0a1b34 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 7 Jun 2025 23:17:47 -0400 Subject: [PATCH] put fomatting hooks onto client. --- docs/composing_a_supergraph.md | 58 +++++++------- docs/error_handling.md | 18 +++-- lib/graphql/stitching.rb | 1 + lib/graphql/stitching/client.rb | 47 ++++++------ lib/graphql/stitching/composer.rb | 50 ++++--------- lib/graphql/stitching/formatter.rb | 62 +++++++++++++++ .../stitching/client/class_extension_test.rb | 75 +++++++++++++++++++ .../stitching/{ => client}/client_test.rb | 34 ++------- .../composer/merge_arguments_test.rb | 8 +- .../composer/merge_directive_test.rb | 8 +- .../stitching/composer/merge_enum_test.rb | 4 +- .../stitching/composer/merge_fields_test.rb | 6 +- .../composer/merge_input_object_test.rb | 4 +- .../composer/merge_interface_test.rb | 4 +- .../stitching/composer/merge_object_test.rb | 4 +- .../composer/merge_root_objects_test.rb | 12 --- .../stitching/composer/merge_scalar_test.rb | 4 +- .../stitching/composer/merge_union_test.rb | 4 +- test/test_helper.rb | 14 +++- 19 files changed, 260 insertions(+), 157 deletions(-) create mode 100644 lib/graphql/stitching/formatter.rb create mode 100644 test/graphql/stitching/client/class_extension_test.rb rename test/graphql/stitching/{ => client}/client_test.rb (92%) diff --git a/docs/composing_a_supergraph.md b/docs/composing_a_supergraph.md index 75675b22..739a6813 100644 --- a/docs/composing_a_supergraph.md +++ b/docs/composing_a_supergraph.md @@ -42,11 +42,7 @@ client = GraphQL::Stitching::Client.new( mutation_name: "Mutation", subscription_name: "Subscription", visibility_profiles: nil, # ["public", "private", ...] - description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") }, - deprecation_merger: ->(values_by_location, info) { values_by_location.values.first }, - default_value_merger: ->(values_by_location, info) { values_by_location.values.first }, - directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last }, - root_entrypoints: {}, + root_entrypoints: { "Query.ping" => "pings" }, }, locations: { # ... @@ -62,47 +58,49 @@ client = GraphQL::Stitching::Client.new( - **`visibility_profiles:`**, an array of [visibility profiles](./visibility.md) that the supergraph responds to. -- **`description_merger:`**, a [value merger function](#value-merger-functions) for merging element description strings from across locations. - -- **`deprecation_merger:`**, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations. - -- **`default_value_merger:`**, a [value merger function](#value-merger-functions) for merging argument default values from across locations. - -- **`directive_kwarg_merger:`**, a [value merger function](#value-merger-functions) for merging directive keyword arguments from across locations. - - **`root_entrypoints:`**, a hash of root field names mapped to their entrypoint locations, see [overlapping root fields](#overlapping-root-fields) below. -#### Value merger functions +### Value mergers -Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element attribute is used. A value merger function may customize this process by selecting a different value or computing a new one: +Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element property is used. Value merger methods can be customized by defining them on your own `Client` class: ```ruby -join_values_merger = ->(values_by_location, info) { values_by_location.values.compact.join("\n") } - -client = GraphQL::Stitching::Client.new( - composer_options: { - description_merger: join_values_merger, - deprecation_merger: join_values_merger, - default_value_merger: join_values_merger, - directive_kwarg_merger: join_values_merger, - }, -) +class MyClient < GraphQL::Stitching::Client + def merge_descriptions(values_by_location, info) + # return a merged element description string from across locations... + values_by_location.each_value.join("\n") + end + + def merge_deprecations(values_by_location, info) + # return a merged element deprecation string from across locations... + end + + def merge_default_values(values_by_location, info) + # return a merged argument default value from across locations... + end + + def merge_kwargs(values_by_location, info) + # return a merged directive keyword argument from across locations... + end +end + +client = MyClient.new(locations: ...) ``` -A merger function receives `values_by_location` and `info` arguments; these provide possible values keyed by location and info about where in the schema these values were encountered: +All merge functions receive `values_by_location` and `info` arguments; these provide possible values keyed by location and info about where in the schema these values were encountered. For example: ```ruby values_by_location = { - "users" => "A fabulous data type.", - "products" => "An excellent data type.", + "users" => "A fabulous data type description.", + "products" => "An excellent data type description.", } -info = { +info = GraphQL::Stitching::Formatter::Info.new( type_name: "Product", # field_name: ..., # argument_name: ..., # directive_name: ..., -} +) ``` ### Cached supergraphs diff --git a/docs/error_handling.md b/docs/error_handling.md index 8d1ba72a..777bef04 100644 --- a/docs/error_handling.md +++ b/docs/error_handling.md @@ -4,20 +4,28 @@ Failed stitching requests can be tricky to debug because it's not always obvious ### Supergraph errors -When exceptions happen while executing requests within the stitching layer, they will be rescued by the stitching client and trigger an `on_error` hook. You should add your stack's error reporting here and return a formatted error message to appear in [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) for the request. +When exceptions happen while executing requests within the stitching layer, they will be rescued by the stitching client and trigger an `on_error` hook. You can add your stack's error reporting here: ```ruby client = GraphQL::Stitching::Client.new(locations: { ... }) client.on_error do |request, err| # log the error Bugsnag.notify(err) +end +``` - # return a formatted message for the public response - "Whoops, please contact support abount request '#{request.context[:request_id]}'" +To modify the format of returned error messages that appear in [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors), extend `Client` and define your own `build_graphql_error` method: + +```ruby +class MyClient < GraphQL::Stitching::Client + def build_graphql_error(request, err) + graphql_error = super(request, err) + graphql_error["message"] << " Contact support about Request ID #{request.context[:request_id]}" + graphql_error + end end -# Result: -# { "errors" => [{ "message" => "Whoops, please contact support abount request '12345'" }] } +client = MyClient.new(locations: { ... }) ``` ### Subgraph errors diff --git a/lib/graphql/stitching.rb b/lib/graphql/stitching.rb index 37887c47..780cc4a4 100644 --- a/lib/graphql/stitching.rb +++ b/lib/graphql/stitching.rb @@ -71,6 +71,7 @@ def supports_visibility? end end +require_relative "stitching/formatter" require_relative "stitching/directives" require_relative "stitching/supergraph" require_relative "stitching/client" diff --git a/lib/graphql/stitching/client.rb b/lib/graphql/stitching/client.rb index f57d702d..39c34d36 100644 --- a/lib/graphql/stitching/client.rb +++ b/lib/graphql/stitching/client.rb @@ -7,6 +7,8 @@ module Stitching # Client is an out-of-the-box helper that assembles all # stitching components into a workflow that executes requests. class Client + include Formatter + class << self def from_definition(schema, executables:) new(supergraph: Supergraph.from_definition(schema, executables: executables)) @@ -30,6 +32,7 @@ def initialize(locations: nil, supergraph: nil, composer_options: {}) elsif supergraph supergraph else + composer_options = { formatter: self }.merge!(composer_options) composer = Composer.new(**composer_options) composer.perform(locations) end @@ -50,16 +53,16 @@ def execute(raw_query = nil, query: nil, variables: nil, operation_name: nil, co if validate validation_errors = request.validate - return error_result(request, validation_errors) unless validation_errors.empty? + return error_result(request, validation_errors.map(&:to_h)) unless validation_errors.empty? end load_plan(request) request.execute rescue GraphQL::ParseError, GraphQL::ExecutionError => e - error_result(request, [e]) + error_result(request, [e.to_h]) rescue StandardError => e - custom_message = @on_error.call(request, e) if @on_error - error_result(request, [{ "message" => custom_message || "An unexpected error occured." }]) + @on_error.call(request, e) if @on_error + error_result(request, [build_graphql_error(request, e)]) end def on_cache_read(&block) @@ -77,33 +80,33 @@ def on_error(&block) @on_error = block end + def read_cached_plan(request) + if @on_cache_read && (plan_json = @on_cache_read.call(request)) + Plan.from_json(JSON.parse(plan_json)) + end + end + + def write_cached_plan(request, plan) + @on_cache_write.call(request, JSON.generate(plan.as_json)) if @on_cache_write + end + private def load_plan(request) - if @on_cache_read && plan_json = @on_cache_read.call(request) - plan = Plan.from_json(JSON.parse(plan_json)) + plan = read_cached_plan(request) - # only use plans referencing current resolver versions - if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] } - return request.plan(plan) - end + # only use plans referencing current resolver versions + if plan && plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] } + return request.plan(plan) end - plan = request.plan - - if @on_cache_write - @on_cache_write.call(request, JSON.generate(plan.as_json)) + request.plan.tap do |plan| + write_cached_plan(request, plan) end - - plan end - def error_result(request, errors) - public_errors = errors.map! do |e| - e.is_a?(Hash) ? e : e.to_h - end - - GraphQL::Query::Result.new(query: request, values: { "errors" => public_errors }) + def error_result(request, graphql_errors) + GraphQL::Query::Result.new(query: request, values: { "errors" => graphql_errors }) end end end diff --git a/lib/graphql/stitching/composer.rb b/lib/graphql/stitching/composer.rb index 0652d6b3..79ecc6fe 100644 --- a/lib/graphql/stitching/composer.rb +++ b/lib/graphql/stitching/composer.rb @@ -20,12 +20,6 @@ class Composer t.get_field("f").get_argument("a").default_value end - # @api private - BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } } - - # @api private - VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) } - # @api private COMPOSITION_VALIDATORS = [ ValidateInterfaces, @@ -52,22 +46,14 @@ def initialize( mutation_name: "Mutation", subscription_name: "Subscription", visibility_profiles: [], - description_merger: nil, - deprecation_merger: nil, - default_value_merger: nil, - directive_kwarg_merger: nil, - root_field_location_selector: nil, - root_entrypoints: nil + root_entrypoints: nil, + formatter: nil ) @query_name = query_name @mutation_name = mutation_name @subscription_name = subscription_name - @description_merger = description_merger || BASIC_VALUE_MERGER - @deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER - @default_value_merger = default_value_merger || BASIC_VALUE_MERGER - @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER - @root_field_location_selector = root_field_location_selector @root_entrypoints = root_entrypoints || {} + @formatter = formatter || Formatter::Default @field_map = {} @resolver_map = {} @@ -424,12 +410,12 @@ def build_merged_arguments(type_name, members_by_location, owner, field_name: ni end unless default_values_by_location.empty? - kwargs[:default_value] = @default_value_merger.call(default_values_by_location, { + kwargs[:default_value] = @formatter.merge_default_values(default_values_by_location, Formatter::Info.new( type_name: type_name, field_name: field_name, argument_name: argument_name, directive_name: directive_name, - }.tap(&:compact!)) + )) end type = merge_value_types(type_name, value_types, argument_name: argument_name, field_name: field_name) @@ -458,7 +444,7 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n end directives_by_name_location.each do |directive_name, directives_by_location| - kwarg_merger = @directive_kwarg_merger + kwarg_formatter = @formatter directive_class = @schema_directives[directive_name] next unless directive_class @@ -481,19 +467,19 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n if (profiles = kwarg_values_by_name_location["profiles"]) @visibility_profiles.merge(profiles.each_value.reduce(&:|)) - kwarg_merger = VISIBILITY_PROFILES_MERGER + kwarg_formatter = Formatter::Default end end kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo| - memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, { + memo[kwarg_name.to_sym] = kwarg_formatter.merge_kwargs(kwarg_values_by_location, Formatter::Info.new( type_name: type_name, field_name: field_name, argument_name: argument_name, enum_value: enum_value, directive_name: directive_name, kwarg_name: kwarg_name, - }.tap(&:compact!)) + )) end owner.directive(directive_class, **kwargs) @@ -537,24 +523,24 @@ def merge_value_types(type_name, subgraph_types, field_name: nil, argument_name: # @!visibility private def merge_descriptions(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil) strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.description } - @description_merger.call(strings_by_location, { + @formatter.merge_descriptions(strings_by_location, Formatter::Info.new( type_name: type_name, field_name: field_name, argument_name: argument_name, enum_value: enum_value, - }.tap(&:compact!)) + )) end # @!scope class # @!visibility private def merge_deprecations(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil) strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.deprecation_reason } - @deprecation_merger.call(strings_by_location, { + @formatter.merge_deprecations(strings_by_location, Formatter::Info.new( type_name: type_name, field_name: field_name, argument_name: argument_name, enum_value: enum_value, - }.tap(&:compact!)) + )) end # @!scope class @@ -629,15 +615,7 @@ def select_root_field_locations(schema) next unless root_field_locations.length > 1 root_field_path = "#{root_type.graphql_name}.#{root_field_name}" - target_location = if @root_field_location_selector && @root_entrypoints.empty? - Warning.warn("Composer option `root_field_location_selector` is deprecated and will be removed.") - @root_field_location_selector.call(root_field_locations, { - type_name: root_type.graphql_name, - field_name: root_field_name, - }) - else - @root_entrypoints[root_field_path] || root_field_locations.last - end + target_location = @root_entrypoints[root_field_path] || root_field_locations.last unless root_field_locations.include?(target_location) raise CompositionError, "Invalid `root_entrypoints` configuration: `#{root_field_path}` has no `#{target_location}` location." diff --git a/lib/graphql/stitching/formatter.rb b/lib/graphql/stitching/formatter.rb new file mode 100644 index 00000000..10a254eb --- /dev/null +++ b/lib/graphql/stitching/formatter.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require_relative "type_resolver/arguments" +require_relative "type_resolver/keys" + +module GraphQL + module Stitching + module Formatter + class Default + extend Formatter + end + + class Info + attr_reader :type_name, :field_name, :argument_name, :enum_value, :directive_name, :kwarg_name + + def initialize( + type_name:, + field_name: nil, + argument_name: nil, + enum_value: nil, + directive_name: nil, + kwarg_name: nil + ) + @type_name = type_name + @field_name = field_name + @argument_name = argument_name + @enum_value = enum_value + @directive_name = directive_name + @kwarg_name = kwarg_name + end + end + + def merge_values(values_by_location, _info) + values_by_location.each_value.find { !_1.nil? } + end + + def merge_descriptions(values_by_location, info) + merge_values(values_by_location, info) + end + + def merge_deprecations(values_by_location, info) + merge_values(values_by_location, info) + end + + def merge_default_values(values_by_location, info) + merge_values(values_by_location, info) + end + + def merge_kwargs(values_by_location, info) + if info.directive_name == GraphQL::Stitching.visibility_directive + values_by_location.each_value.reduce(:&) + else + merge_values(values_by_location, info) + end + end + + def build_graphql_error(_request, _err) + { "message" => "An unexpected error occured." } + end + end + end +end diff --git a/test/graphql/stitching/client/class_extension_test.rb b/test/graphql/stitching/client/class_extension_test.rb new file mode 100644 index 00000000..40f8f869 --- /dev/null +++ b/test/graphql/stitching/client/class_extension_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "../../../schemas/example" + +describe "GraphQL::Stitching::Client" do + class MyClient < GraphQL::Stitching::Client + def merge_descriptions(values_by_location, _info) + values_by_location.values.join("/") + end + + def build_graphql_error(request, err) + { "message" => "Contact support about request #{request.context[:request_id]}." } + end + end + + def test_client_acts_as_composition_formatter + alpha = %| + """ + a + """ + type Query { a:Boolean } + | + bravo = %| + """ + b + """ + type Query { b:Boolean } + | + + client = MyClient.new(locations: { + "alpha" => { schema: GraphQL::Schema.from_definition(alpha) }, + "bravo" => { schema: GraphQL::Schema.from_definition(bravo) }, + }) + + assert_equal "a/b", client.supergraph.schema.query.description + end + + def test_client_builds_graphql_errors + client = MyClient.new(locations: { + products: { schema: Schemas::Example::Products }, + }) + + result = client.execute( + query: "query { invalidSelection }", + context: { request_id: "R2d2c3P0" }, + validate: false + ) + + expected_errors = [{ + "message" => "Contact support about request R2d2c3P0.", + }] + + assert_nil result["data"] + assert_equal expected_errors, result["errors"] + end + + def test_client_from_definition_builds_specific_class + alpha = %| + type T { id:ID! a:String } + type Query { a(id:ID!):T @stitch(key: "id") } + | + bravo = %| + type T { id:ID! b:String } + type Query { b(id:ID!):T @stitch(key: "id") } + | + + sdl = compose_definitions({ "alpha" => alpha, "bravo" => bravo }).to_definition + client = MyClient.from_definition(sdl, executables: { + "alpha" => Proc.new {}, + "bravo" => Proc.new {}, + }) + assert client.is_a?(MyClient) + end +end \ No newline at end of file diff --git a/test/graphql/stitching/client_test.rb b/test/graphql/stitching/client/client_test.rb similarity index 92% rename from test/graphql/stitching/client_test.rb rename to test/graphql/stitching/client/client_test.rb index 78e7b3c9..1f6142a8 100644 --- a/test/graphql/stitching/client_test.rb +++ b/test/graphql/stitching/client/client_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require_relative "../../schemas/example" +require_relative "../../../schemas/example" describe "GraphQL::Stitching::Client" do def setup_client @@ -297,7 +297,7 @@ def test_caching_hooks_receive_request_context assert_equal context[:key], write_context end - def test_invalid_query + def test_query_with_static_validation_errors client = GraphQL::Stitching::Client.new(locations: { products: { schema: Schemas::Example::Products, @@ -314,12 +314,14 @@ def test_invalid_query assert_equal expected_errors, result["errors"].map { _1.slice("message", "path") } end - def test_errors_are_handled_by_default + def test_stitching_errors_are_handled + called = false client = GraphQL::Stitching::Client.new(locations: { products: { schema: Schemas::Example::Products, } }) + client.on_error { called = true } result = client.execute('query { invalidSelection }', validate: false) @@ -327,31 +329,7 @@ def test_errors_are_handled_by_default "message" => "An unexpected error occured.", }] - assert_nil result["data"] - assert_equal expected_errors, result["errors"] - end - - def test_errors_trigger_hooks_that_may_return_a_custom_message - client = GraphQL::Stitching::Client.new(locations: { - products: { - schema: Schemas::Example::Products, - } - }) - - client.on_error do |req, _err| - "An error occured. Request id: #{req.context[:request_id]}" - end - - result = client.execute( - query: "query { invalidSelection }", - context: { request_id: "R2d2c3P0" }, - validate: false - ) - - expected_errors = [{ - "message" => "An error occured. Request id: R2d2c3P0", - }] - + assert called assert_nil result["data"] assert_equal expected_errors, result["errors"] end diff --git a/test/graphql/stitching/composer/merge_arguments_test.rb b/test/graphql/stitching/composer/merge_arguments_test.rb index 95bfabe5..721de063 100644 --- a/test/graphql/stitching/composer/merge_arguments_test.rb +++ b/test/graphql/stitching/composer/merge_arguments_test.rb @@ -90,7 +90,7 @@ def test_merges_argument_descriptions b = %|input Test { """b""" arg:String } type Query { test("""b""" arg:Test):String }| supergraph = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].arguments["arg"].description @@ -102,7 +102,7 @@ def test_merges_argument_deprecations b = %|input Test { arg:String @deprecated(reason:"b") } type Query { test(arg:Test @deprecated(reason:"b")):String }| supergraph = compose_definitions({ "a" => a, "b" => b }, { - deprecation_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].arguments["arg"].deprecation_reason @@ -121,7 +121,7 @@ def test_merges_argument_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) directive = supergraph.schema.types["Query"].fields["test"].arguments["arg"].directives.first @@ -170,7 +170,7 @@ def test_merged_field_arguments_with_selected_default_value c = %|type Query { test(arg:Int = 2):String }| supergraph = compose_definitions({ "a" => a, "b" => b, "c" => c }, { - default_value_merger: ->(values_by_location, _info) { values_by_location.values.max } + formatter: TestFormatter.new, }) assert_equal 2, supergraph.schema.types["Query"].fields["test"].arguments["arg"].default_value end diff --git a/test/graphql/stitching/composer/merge_directive_test.rb b/test/graphql/stitching/composer/merge_directive_test.rb index 9a877c09..437558bf 100644 --- a/test/graphql/stitching/composer/merge_directive_test.rb +++ b/test/graphql/stitching/composer/merge_directive_test.rb @@ -19,7 +19,7 @@ def test_merges_directive_definitions | supergraph = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) directive_definition = supergraph.schema.directives["fizzbuzz"] @@ -43,7 +43,7 @@ def test_combines_distinct_directives_assigned_to_an_element | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) directives = supergraph.schema.types["Test"].directives @@ -67,7 +67,7 @@ def test_omits_stitching_directives_and_includes_supergraph_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert !supergraph.schema.directives.key?("stitch") @@ -92,7 +92,7 @@ def test_merges_camel_case_directive_values | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) directives = supergraph.schema.get_type("Test").directives diff --git a/test/graphql/stitching/composer/merge_enum_test.rb b/test/graphql/stitching/composer/merge_enum_test.rb index e1855338..d2955fbe 100644 --- a/test/graphql/stitching/composer/merge_enum_test.rb +++ b/test/graphql/stitching/composer/merge_enum_test.rb @@ -9,7 +9,7 @@ def test_merges_enum_and_value_descriptions b = %|"""b""" enum Status { """b""" YES } type Query { status:Status }| supergraph = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Status"].description @@ -30,7 +30,7 @@ def test_merges_enum_and_value_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Status"].directives.first.arguments.keyword_arguments[:arg] diff --git a/test/graphql/stitching/composer/merge_fields_test.rb b/test/graphql/stitching/composer/merge_fields_test.rb index 49ba5c87..f8cce4b9 100644 --- a/test/graphql/stitching/composer/merge_fields_test.rb +++ b/test/graphql/stitching/composer/merge_fields_test.rb @@ -9,7 +9,7 @@ def test_merges_field_descriptions b = %{type Test { """b""" field: String } type Query { test:Test }} supergraph = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].fields["field"].description @@ -20,7 +20,7 @@ def test_merges_field_deprecations b = %{type Test { field: String @deprecated(reason:"b") } type Query { test:Test }} supergraph = compose_definitions({ "a" => a, "b" => b }, { - deprecation_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].fields["field"].deprecation_reason @@ -38,7 +38,7 @@ def test_merges_field_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Query"].fields["test"].directives.first.arguments.keyword_arguments[:arg] diff --git a/test/graphql/stitching/composer/merge_input_object_test.rb b/test/graphql/stitching/composer/merge_input_object_test.rb index c8b8cb63..6ad0b542 100644 --- a/test/graphql/stitching/composer/merge_input_object_test.rb +++ b/test/graphql/stitching/composer/merge_input_object_test.rb @@ -9,7 +9,7 @@ def test_merges_input_object_descriptions b = %{"""b""" input Test { field:String } type Query { get(test:Test):String }} info = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", info.schema.types["Test"].description @@ -29,7 +29,7 @@ def test_merges_input_object_and_field_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].directives.first.arguments.keyword_arguments[:arg] diff --git a/test/graphql/stitching/composer/merge_interface_test.rb b/test/graphql/stitching/composer/merge_interface_test.rb index 7f010dc8..3d062420 100644 --- a/test/graphql/stitching/composer/merge_interface_test.rb +++ b/test/graphql/stitching/composer/merge_interface_test.rb @@ -9,7 +9,7 @@ def test_merges_interface_descriptions b = %{"""b""" interface Test { field: String } type Query { test:Test }} supergraph = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].description @@ -29,7 +29,7 @@ def test_merges_interface_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].directives.first.arguments.keyword_arguments[:arg] diff --git a/test/graphql/stitching/composer/merge_object_test.rb b/test/graphql/stitching/composer/merge_object_test.rb index e1bd0839..7f5e734c 100644 --- a/test/graphql/stitching/composer/merge_object_test.rb +++ b/test/graphql/stitching/composer/merge_object_test.rb @@ -9,7 +9,7 @@ def test_merges_object_descriptions b = %{"""b""" type Test { field: String } type Query { test:Test }} supergraph = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].description @@ -29,7 +29,7 @@ def test_merges_object_directives | supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.types["Test"].directives.first.arguments.keyword_arguments[:arg] diff --git a/test/graphql/stitching/composer/merge_root_objects_test.rb b/test/graphql/stitching/composer/merge_root_objects_test.rb index f75ba9e8..880fb90b 100644 --- a/test/graphql/stitching/composer/merge_root_objects_test.rb +++ b/test/graphql/stitching/composer/merge_root_objects_test.rb @@ -64,18 +64,6 @@ def test_prioritizes_last_root_field_location_by_default assert_equal ["b", "a"], delegation_map["Mutation"]["f"] end - def test_prioritizes_root_field_location_selector_choice - a = "type Query { f:String } type Mutation { f:String }" - b = "type Query { f:String } type Mutation { f:String }" - - delegation_map = compose_definitions({ "a" => a, "b" => b }, { - root_field_location_selector: ->(_locations, _info) { "a" } - }).fields - - assert_equal ["a", "b"], delegation_map["Query"]["f"] - assert_equal ["a", "b"], delegation_map["Mutation"]["f"] - end - def test_prioritizes_root_entrypoints_locations a = "type Query { f:String } type Mutation { f:String }" b = "type Query { f:String } type Mutation { f:String }" diff --git a/test/graphql/stitching/composer/merge_scalar_test.rb b/test/graphql/stitching/composer/merge_scalar_test.rb index cee9611d..09014c7d 100644 --- a/test/graphql/stitching/composer/merge_scalar_test.rb +++ b/test/graphql/stitching/composer/merge_scalar_test.rb @@ -9,7 +9,7 @@ def test_merges_scalar_descriptions b = %{"""b""" scalar URL type Query { url:URL }} info = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", info.schema.get_type("URL").description @@ -29,7 +29,7 @@ def test_merges_scalar_directives GRAPHQL supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.get_type("Thing").directives.first.arguments.keyword_arguments[:arg] diff --git a/test/graphql/stitching/composer/merge_union_test.rb b/test/graphql/stitching/composer/merge_union_test.rb index d5bb8caf..4e394aae 100644 --- a/test/graphql/stitching/composer/merge_union_test.rb +++ b/test/graphql/stitching/composer/merge_union_test.rb @@ -18,7 +18,7 @@ def test_merges_union_descriptions b = %{type B { b:Int } """b""" union Thing = B type Query { thing:Thing }} info = compose_definitions({ "a" => a, "b" => b }, { - description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", info.schema.get_type("Thing").description @@ -40,7 +40,7 @@ def test_merges_union_directives GRAPHQL supergraph = compose_definitions({ "a" => a, "b" => b }, { - directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") } + formatter: TestFormatter.new, }) assert_equal "a/b", supergraph.schema.get_type("Thing").directives.first.arguments.keyword_arguments[:arg] diff --git a/test/test_helper.rb b/test/test_helper.rb index d310ee10..6dc00cec 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,7 +7,6 @@ # ignore warnings from auto-generated GraphQL lib code. Warning.ignore(/.*mismatched indentations.*/) Warning.ignore(/.*lib\/graphql\/language\/nodes.rb:.*/) - Warning.ignore(/Composer option `[^`]+` is deprecated.*/) end require 'bundler/setup' @@ -81,6 +80,19 @@ def sort_node_selections(node) end end +class TestFormatter + include GraphQL::Stitching::Formatter + + def merge_values(values_by_location, _info) + vals = values_by_location.each_value.reject(&:nil?) + vals.empty? ? nil : vals.join("/") + end + + def merge_default_values(values_by_location, _info) + values_by_location.values.max + end +end + def squish_string(str) str.gsub(/\s+/, " ").strip end