Skip to content

Commit 215e7fa

Browse files
authored
Merge pull request #5491 from rmosolgo/better-method-docs
Improve documentation for `field` and `argument` DSL methods
2 parents d43936b + 1194822 commit 215e7fa

8 files changed

Lines changed: 147 additions & 23 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ if RUBY_VERSION >= "3.0"
1414
end
1515

1616
if RUBY_VERSION >= "3.2.0"
17+
gem "minitest-mock"
1718
gem "async", "~>2.0"
1819
gem "minitest-mock"
1920
end

lib/graphql/schema/argument.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ def from_resolver?
3939
# @param arg_name [Symbol]
4040
# @param type_expr
4141
# @param desc [String]
42+
# @param type [Class, Array<Class>] Input type; positional argument also accepted
43+
# @param name [Symbol] positional argument also accepted # @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
44+
# @param definition_block [Proc] Called with the newly-created {Argument}
45+
# @param owner [Class] Private, used by GraphQL-Ruby during schema definition
4246
# @param required [Boolean, :nullable] if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
4347
# @param description [String]
4448
# @param default_value [Object]
49+
# @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
4550
# @param as [Symbol] Override the keyword name when passed to a method
4651
# @param prepare [Symbol] A method to call to transform this argument's valuebefore sending it to field resolution
4752
# @param camelize [Boolean] if true, the name will be camelized when building the schema
@@ -50,6 +55,8 @@ def from_resolver?
5055
# @param deprecation_reason [String]
5156
# @param validates [Hash, nil] Options for building validators, if any should be applied
5257
# @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value`
58+
# @param comment [String] Private, used by GraphQL-Ruby when parsing GraphQL schema files
59+
# @param ast_node [GraphQL::Language::Nodes::InputValueDefinition] Private, used by GraphQL-Ruby when parsing schema files
5360
def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
5461
arg_name ||= name
5562
@name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)

lib/graphql/schema/field.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ def method_conflict_warning?
209209
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
210210
# @param validates [Array<Hash>] Configurations for validating this field
211211
# @param fallback_value [Object] A fallback value if the method is not defined
212+
# @param dynamic_introspection [Boolean] (Private, used by GraphQL-Ruby)
213+
# @param relay_node_field [Boolean] (Private, used by GraphQL-Ruby)
214+
# @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
215+
# @param extras [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] Extra arguments to be injected into the resolver for this field
216+
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
212217
def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, 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: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
213218
if name.nil?
214219
raise ArgumentError, "missing first `name` argument or keyword `name:`"
@@ -301,7 +306,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON
301306

302307
@extensions = EMPTY_ARRAY
303308
@call_after_define = false
304-
set_pagination_extensions(connection_extension: connection_extension)
309+
set_pagination_extensions(connection_extension: NOT_CONFIGURED.equal?(connection_extension) ? self.class.connection_extension : connection_extension)
305310
# Do this last so we have as much context as possible when initializing them:
306311
if !extensions.empty?
307312
self.extensions(extensions)

lib/graphql/schema/member/has_arguments.rb

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,52 @@ def self.extended(cls)
1414
cls.extend(ClassConfigured)
1515
end
1616

17-
# @see {GraphQL::Schema::Argument#initialize} for parameters
18-
# @return [GraphQL::Schema::Argument] An instance of {argument_class}, created from `*args`
19-
def argument(*args, **kwargs, &block)
20-
kwargs[:owner] = self
21-
loads = kwargs[:loads]
22-
if loads
23-
name = args[0]
24-
name_as_string = name.to_s
25-
26-
inferred_arg_name = case name_as_string
17+
# @param arg_name [Symbol] The underscore-cased name of this argument, `name:` keyword also accepted
18+
# @param type_expr The GraphQL type of this argument; `type:` keyword also accepted
19+
# @param desc [String] Argument description, `description:` keyword also accepted
20+
# @option kwargs [Boolean, :nullable] :required if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
21+
# @option kwargs [String] :description Positional argument also accepted
22+
# @option kwargs [Class, Array<Class>] :type Input type; positional argument also accepted
23+
# @option kwargs [Symbol] :name positional argument also accepted
24+
# @option kwargs [Object] :default_value
25+
# @option kwargs [Class, Array<Class>] :loads A GraphQL type to load for the given ID when one is present
26+
# @option kwargs [Symbol] :as Override the keyword name when passed to a method
27+
# @option kwargs [Symbol] :prepare A method to call to transform this argument's valuebefore sending it to field resolution
28+
# @option kwargs [Boolean] :camelize if true, the name will be camelized when building the schema
29+
# @option kwargs [Boolean] :from_resolver if true, a Resolver class defined this argument
30+
# @option kwargs [Hash{Class => Hash}] :directives
31+
# @option kwargs [String] :deprecation_reason
32+
# @option kwargs [String] :comment Private, used by GraphQL-Ruby when parsing GraphQL schema files
33+
# @option kwargs [GraphQL::Language::Nodes::InputValueDefinition] :ast_node Private, used by GraphQL-Ruby when parsing schema files
34+
# @option kwargs [Hash, nil] :validates Options for building validators, if any should be applied
35+
# @option kwargs [Boolean] :replace_null_with_default if `true`, incoming values of `null` will be replaced with the configured `default_value`
36+
# @param definition_block [Proc] Called with the newly-created {Argument}
37+
# @param kwargs [Hash] Keywords for defining an argument. Any keywords not documented here must be handled by your base Argument class.
38+
# @return [GraphQL::Schema::Argument] An instance of {argument_class} created from these arguments
39+
def argument(arg_name = nil, type_expr = nil, desc = nil, **kwargs, &definition_block)
40+
if kwargs[:loads]
41+
loads_name = arg_name || kwargs[:name]
42+
loads_name_as_string = loads_name.to_s
43+
44+
inferred_arg_name = case loads_name_as_string
2745
when /_id$/
28-
name_as_string.sub(/_id$/, "").to_sym
46+
loads_name_as_string.sub(/_id$/, "").to_sym
2947
when /_ids$/
30-
name_as_string.sub(/_ids$/, "")
48+
loads_name_as_string.sub(/_ids$/, "")
3149
.sub(/([^s])$/, "\\1s")
3250
.to_sym
3351
else
34-
name
52+
loads_name
3553
end
3654

3755
kwargs[:as] ||= inferred_arg_name
3856
end
39-
arg_defn = self.argument_class.new(*args, **kwargs, &block)
57+
kwargs[:owner] = self
58+
arg_defn = self.argument_class.new(
59+
arg_name, type_expr, desc,
60+
**kwargs,
61+
&definition_block
62+
)
4063
add_argument(arg_defn)
4164
arg_defn
4265
end

lib/graphql/schema/member/has_fields.rb

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,52 @@ class Schema
55
class Member
66
# Shared code for Objects, Interfaces, Mutations, Subscriptions
77
module HasFields
8+
include EmptyObjects
89
# Add a field to this object or interface with the given definition
9-
# @param name_positional [Symbol] Keyword `name:` also supported
10-
# @param type_positional [Class, Array<Class>] Keyword `type:` also supported
11-
# @param desc_positional [String] Keyword `description:` also supported
12-
# @see {GraphQL::Schema::Field#initialize} for keywords
10+
# @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
11+
# @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
12+
# @param desc_positional [String] Field description; `description:` keyword is also accepted
13+
# @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
14+
# @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted
15+
# @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
16+
# @option kwargs [String] :description Field description; positional argument also accepted
17+
# @option kwargs [String] :comment Field comment
18+
# @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message
19+
# @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`)
20+
# @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
21+
# @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
22+
# @option kwargs [Symbol] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
23+
# @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
24+
# @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
25+
# @option kwargs [Integer, nil] :max_page_size For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
26+
# @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results.
27+
# @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__`
28+
# @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also)
29+
# @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema
30+
# @option kwargs [Numeric] :complexity When provided, set the complexity for this field
31+
# @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value
32+
# @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads
33+
# @option kwargs [Array<Class, Hash<Class => Object>>] :extensions Named extensions to apply to this field (see also {#extension})
34+
# @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field
35+
# @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field
36+
# @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts
37+
# @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field
38+
# @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method
39+
# @option kwargs [Array<Hash>] :validates Configurations for validating this field
40+
# @option kwargs [Object] :fallback_value A fallback value if the method is not defined
41+
# @option kwargs [Class<GraphQL::Schema::Mutation>] :mutation
42+
# @option kwargs [Class<GraphQL::Schema::Resolver>] :resolver
43+
# @option kwargs [Class<GraphQL::Schema::Subscription>] :subscription
44+
# @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
45+
# @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
46+
# @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
47+
# @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
48+
# @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
49+
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
50+
# @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
51+
# @yieldreturn [void]
1352
# @return [GraphQL::Schema::Field]
14-
def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &block)
53+
def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
1554
resolver = kwargs.delete(:resolver)
1655
mutation = kwargs.delete(:mutation)
1756
subscription = kwargs.delete(:subscription)
@@ -41,7 +80,8 @@ def field(name_positional = nil, type_positional = nil, desc_positional = nil, *
4180
end
4281
end
4382

44-
field_defn = field_class.new(owner: self, **kwargs, &block)
83+
kwargs[:owner] = self
84+
field_defn = field_class.new(**kwargs, &definition_block)
4585
add_field(field_defn)
4686
field_defn
4787
end
@@ -264,7 +304,7 @@ def visible_interface_implementation?(type, context, warden)
264304
end
265305
end
266306

267-
# @param [GraphQL::Schema::Field]
307+
# @param field_defn [GraphQL::Schema::Field]
268308
# @return [String] A warning to give when this field definition might conflict with a built-in method
269309
def conflict_field_name_warning(field_defn)
270310
"#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."

spec/graphql/schema/argument_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,4 +798,21 @@ class Query < GraphQL::Schema::Object
798798
assert_equal 15, res2["data"]["add"]
799799
end
800800
end
801+
802+
describe "argument definitions" do
803+
it "HasArguments::argument documents each argument" do
804+
has_arguments_argument_comment = File.read("./lib/graphql/schema/member/has_arguments.rb")[/(\s+#[^\n]*\n)+\s+def argument\(/m]
805+
has_arguments_argument_doc_param_names = has_arguments_argument_comment.split("\n").map { |line| (line[/@param (\S+)/] || line[/@option kwargs \[.*\] :(\S+)/]); $1 }.compact
806+
argument_initialize_argument_names = GraphQL::Schema::Argument.instance_method(:initialize).parameters.map { |param| param[1].to_s }
807+
assert_equal ["kwargs"], has_arguments_argument_doc_param_names - argument_initialize_argument_names
808+
assert_equal ["owner"], argument_initialize_argument_names - has_arguments_argument_doc_param_names
809+
end
810+
811+
it "Argument::initialize documents each argument" do
812+
argument_initialize_comment = File.read("./lib/graphql/schema/argument.rb")[/(\s+#[^\n]*\n)+ {6}def initialize\(/m]
813+
argument_initialize_doc_param_names = argument_initialize_comment.split("\n").map { |line| line[/@param (\S+)/]; $1 }.compact
814+
argument_initialize_argument_names = GraphQL::Schema::Argument.instance_method(:initialize).parameters.map { |param| param[1].to_s }
815+
assert_equal argument_initialize_doc_param_names.sort, argument_initialize_argument_names.sort
816+
end
817+
end
801818
end

spec/graphql/schema/field_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22
require "spec_helper"
3+
34
describe GraphQL::Schema::Field do
45
describe "graphql definition" do
56
let(:object_class) { Jazz::Query }
@@ -925,4 +926,35 @@ class Connection < GraphQL::Schema::Object; end
925926
field = GraphQL::Schema::Field.new(name: "blah", owner: nil, type: FieldConnectionTest::Connection, connection: true)
926927
assert field.connection?
927928
end
929+
930+
describe "argument documentation" do
931+
it "HasFields::field documents each argument" do
932+
has_fields_field_comment = File.read("./lib/graphql/schema/member/has_fields.rb")[/(\s+#[^\n]*\n)+\s+def field\(/m]
933+
has_field_field_doc_param_names = has_fields_field_comment.split("\n").map do |line|
934+
line[/@param (\S+)/] || line[/@option kwargs \[.*\] :(\S+)/]
935+
$1
936+
end.compact
937+
938+
field_initialize_argument_names = GraphQL::Schema::Field.instance_method(:initialize).parameters.map { |param| param[1].to_s }
939+
940+
expected_differences = [
941+
"name_positional",
942+
"type_positional",
943+
"desc_positional",
944+
"mutation",
945+
"resolver",
946+
"subscription",
947+
"kwargs",
948+
]
949+
assert_equal expected_differences, has_field_field_doc_param_names - field_initialize_argument_names
950+
assert_equal ["owner", "resolver_class"], field_initialize_argument_names - has_field_field_doc_param_names
951+
end
952+
953+
it "Field::initialize documents each argument" do
954+
field_initialize_comment = File.read("./lib/graphql/schema/field.rb")[/(\s+#[^\n]*\n)+ {6}def initialize\(/m]
955+
field_initialize_doc_param_names = field_initialize_comment.split("\n").map { |line| line[/@param (\S+)/]; $1 }.compact
956+
field_initialize_argument_names = GraphQL::Schema::Field.instance_method(:initialize).parameters.map { |param| param[1].to_s }
957+
assert_equal field_initialize_doc_param_names.sort, field_initialize_argument_names.sort
958+
end
959+
end
928960
end

spec/spec_helper.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
require "minitest/autorun"
5252
require "minitest/focus"
5353
require "minitest/reporters"
54-
require "minitest/mock"
5554
require "graphql/batch"
5655

5756
running_in_rubymine = ENV["RM_INFO"]

0 commit comments

Comments
 (0)