Skip to content

Commit

Permalink
Merge pull request #4069 from danajackson2/master
Browse files Browse the repository at this point in the history
Add fallback_value option to Field
  • Loading branch information
rmosolgo committed May 24, 2022
2 parents e0e78c1 + 1aeac86 commit 5f8aa65
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 10 deletions.
1 change: 1 addition & 0 deletions guides/fields/introduction.md
Expand Up @@ -87,6 +87,7 @@ By default, fields return values by:

- Trying to call a method on the underlying object; _OR_
- If the underlying object is a `Hash`, lookup a key in that hash.
- An optional `:fallback_value` can be supplied that will be used if the above fail.

The method name or hash key corresponds to the field name, so in this example:

Expand Down
22 changes: 17 additions & 5 deletions lib/graphql/schema/field.rb
Expand Up @@ -214,7 +214,8 @@ def method_conflict_warning?
# @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
# @param validates [Array<Hash>] Configurations for validating this field
def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_given, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, &definition_block)
# @fallback_value [Object] A fallback value if the method is not defined
def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_given, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: :not_given, &definition_block)
if name.nil?
raise ArgumentError, "missing first `name` argument or keyword `name:`"
end
Expand Down Expand Up @@ -280,6 +281,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_gi
@relay_nodes_field = relay_nodes_field
@ast_node = ast_node
@method_conflict_warning = method_conflict_warning
@fallback_value = fallback_value

arguments.each do |name, arg|
case arg
Expand Down Expand Up @@ -643,7 +645,7 @@ def resolve(object, args, query_ctx)
inner_object = obj.object

if defined?(@hash_key)
inner_object[@hash_key] || inner_object[@hash_key_str]
inner_object[@hash_key] || inner_object[@hash_key_str] || (@fallback_value != :not_given ? @fallback_value : nil)
elsif obj.respond_to?(resolver_method)
method_to_call = resolver_method
method_receiver = obj
Expand All @@ -659,13 +661,21 @@ def resolve(object, args, query_ctx)
elsif defined?(@hash_key)
if inner_object.key?(@hash_key)
inner_object[@hash_key]
else
elsif inner_object.key?(@hash_key_str)
inner_object[@hash_key_str]
elsif @fallback_value != :not_given
@fallback_value
else
nil
end
elsif inner_object.key?(@method_sym)
inner_object[@method_sym]
else
elsif inner_object.key?(@method_str)
inner_object[@method_str]
elsif @fallback_value != :not_given
@fallback_value
else
nil
end
elsif inner_object.respond_to?(@method_sym)
method_to_call = @method_sym
Expand All @@ -675,6 +685,8 @@ def resolve(object, args, query_ctx)
else
inner_object.public_send(@method_sym)
end
elsif @fallback_value != :not_given
@fallback_value
else
raise <<-ERR
Failed to implement #{@owner.graphql_name}.#{@name}, tried:
Expand All @@ -683,7 +695,7 @@ def resolve(object, args, query_ctx)
- `#{inner_object.class}##{@method_sym}`, which did not exist
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{inner_object}`, but it wasn't a Hash
To implement this field, define one of the methods above (and check for typos)
To implement this field, define one of the methods above (and check for typos), or supply a `fallback_value`.
ERR
end
end
Expand Down
1 change: 0 additions & 1 deletion spec/graphql/schema/field_spec.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
require "spec_helper"

describe GraphQL::Schema::Field do
describe "graphql definition" do
let(:object_class) { Jazz::Query }
Expand Down
144 changes: 140 additions & 4 deletions spec/graphql/schema/interface_spec.rb
Expand Up @@ -282,7 +282,7 @@ def thing
{
thing {
...on Node { id }
...on Named {
...on Named {
nid: id name
...on Node { nnid: id }
}
Expand All @@ -292,7 +292,7 @@ def thing
GRAPHQL

thing2 = result2.dig("data", "thing")

assert_equal "id", thing2["id"]
assert_equal "id", thing2["nid"]
assert_equal "id", thing2["tid"]
Expand Down Expand Up @@ -331,15 +331,151 @@ def thing

it "only lists each implemented interface once when introspecting" do
introspection = TransitiveInterfaceSchema.as_json
thing_type = introspection.dig("data", "__schema", "types").find do |type|
thing_type = introspection.dig("data", "__schema", "types").find do |type|
type["name"] == "Thing"
end
interfaces_names = thing_type["interfaces"].map { |i| i["name"] }.sort

assert_equal interfaces_names, ["Named", "Node", "Timestamped"]
end
end

describe "supplying a fallback_value to a field" do
DATABASE = [
{id: "1", name: "Hash thing"},
{id: "2"},
{id: "3", name: nil},
OpenStruct.new(id: "4", name: "OpenStruct thing"),
OpenStruct.new(id: "5"),
{id: "6", custom_name: "Hash Key Name"}
]

class FallbackValueSchema < GraphQL::Schema
module NodeWithFallbackInterface
include GraphQL::Schema::Interface

field :id, ID, null: false
field :name, String, fallback_value: "fallback"
end

module NodeWithHashKeyFallbackInterface
include GraphQL::Schema::Interface

field :id, ID, null: false
field :name, String, hash_key: :custom_name, fallback_value: "hash-key-fallback"
end

module NodeWithoutFallbackInterface
include GraphQL::Schema::Interface

field :id, ID, null: false
field :name, String
end

module NodeWithNilFallbackInterface
include GraphQL::Schema::Interface

field :id, ID, null: false
field :name, String, fallback_value: nil
end

class NodeWithFallbackType < GraphQL::Schema::Object
implements NodeWithFallbackInterface
end

class NodeWithHashKeyFallbackType < GraphQL::Schema::Object
implements NodeWithHashKeyFallbackInterface
end

class NodeWithNilFallbackType < GraphQL::Schema::Object
implements NodeWithNilFallbackInterface
end

class NodeWithoutFallbackType < GraphQL::Schema::Object
implements NodeWithoutFallbackInterface
end

class Query < GraphQL::Schema::Object
field :fallback, [NodeWithFallbackType]
def fallback
DATABASE
end

field :hash_key_fallback, [NodeWithHashKeyFallbackType]
def hash_key_fallback
DATABASE
end

field :no_fallback, [NodeWithoutFallbackType]
def no_fallback
DATABASE
end

field :nil_fallback, [NodeWithNilFallbackType]
def nil_fallback
DATABASE
end
end

query(Query)
end

it "uses fallback_value if supplied, but only if other ways don't work" do
result = FallbackValueSchema.execute("{ fallback { id name } }")
data = result["data"]["fallback"]
expected = [
{"id"=>"1", "name"=>"Hash thing"},
{"id"=>"2", "name"=>"fallback"},
{"id"=>"3", "name"=>nil},
{"id"=>"4", "name"=>"OpenStruct thing"},
{"id"=>"5", "name"=>"fallback"},
{"id"=>"6", "name"=>"fallback"},
]

assert_equal expected, data
end

it "uses fallback_value if supplied when hash key isn't present" do
result = FallbackValueSchema.execute("{ hashKeyFallback { id name } }")
data = result["data"]["hashKeyFallback"]
expected = [
{"id"=>"1", "name"=>"hash-key-fallback"},
{"id"=>"2", "name"=>"hash-key-fallback"},
{"id"=>"3", "name"=>"hash-key-fallback"},
{"id"=>"4", "name"=>"hash-key-fallback"},
{"id"=>"5", "name"=>"hash-key-fallback"},
{"id"=>"6", "name"=>"Hash Key Name"},
]

assert_equal expected, data
end

it "allows nil as fallback_value" do
result = FallbackValueSchema.execute("{ nilFallback { id name } }")
data = result["data"]["nilFallback"]
expected = [
{"id"=>"1", "name"=>"Hash thing"},
{"id"=>"2", "name"=>nil},
{"id"=>"3", "name"=>nil},
{"id"=>"4", "name"=>"OpenStruct thing"},
{"id"=>"5", "name"=>nil},
{"id"=>"6", "name"=>nil},
]

assert_equal expected, data
end

it "errors if no fallback_value is supplied and other ways don't work" do
err = assert_raises RuntimeError do
FallbackValueSchema.execute("{ noFallback { id name } }")
end

assert_includes err.message, "Failed to implement"
# Doesn't error until it gets to the OpenStructs.
assert_includes err.message, "OpenStruct"
end
end

describe "migrated legacy tests" do
let(:interface) { Dummy::Edible }

Expand Down

0 comments on commit 5f8aa65

Please sign in to comment.