Skip to content

Commit 17dd8cf

Browse files
committed
Merge master, update specs
2 parents 04c4330 + d43936b commit 17dd8cf

4 files changed

Lines changed: 94 additions & 123 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ end
1616
if RUBY_VERSION >= "3.2.0"
1717
gem "minitest-mock"
1818
gem "async", "~>2.0"
19+
gem "minitest-mock"
1920
end
2021

2122
# Required for running `jekyll algolia ...` (via `rake site:update_search_index`)

lib/graphql/language.rb

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,28 @@ def self.escape_single_quoted_newlines(query_str)
7979

8080
LEADING_REGEX = Regexp.union(" ", *Lexer::Punctuation.constants.map { |const| Lexer::Punctuation.const_get(const) })
8181

82+
# Optimized pattern using:
83+
# - Possessive quantifiers (*+, ++) to prevent backtracking in number patterns
84+
# - Atomic group (?>...) for IGNORE to prevent backtracking
85+
# - Single unified number pattern instead of three alternatives
86+
EFFICIENT_NUMBER_REGEXP = /-?(?:0|[1-9][0-9]*+)(?:\.[0-9]++)?(?:[eE][+-]?[0-9]++)?/
87+
EFFICIENT_IGNORE_REGEXP = /(?>[, \r\n\t]+|\#[^\n]*$)*/
88+
89+
MAYBE_INVALID_NUMBER = /\d[_a-zA-Z]/
90+
8291
INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP = %r{
8392
(?<leading>#{LEADING_REGEX})
84-
(
85-
((?<num>#{Lexer::INT_REGEXP}(#{Lexer::FLOAT_EXP_REGEXP})?)(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
86-
|
87-
((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP}#{Lexer::FLOAT_EXP_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
88-
|
89-
((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
90-
)}x
93+
(?<num>#{EFFICIENT_NUMBER_REGEXP})
94+
(?<name>#{Lexer::IDENTIFIER_REGEXP})
95+
#{EFFICIENT_IGNORE_REGEXP}
96+
:
97+
}x
9198

9299
def self.add_space_between_numbers_and_names(query_str)
93-
if query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
94-
query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<leading>\\k<num> \\k<name>:")
95-
else
96-
query_str
97-
end
100+
# Fast check for digit followed by identifier char. If this doesn't match, skip the more expensive regexp entirely.
101+
return query_str unless query_str.match?(MAYBE_INVALID_NUMBER)
102+
return query_str unless query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
103+
query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<leading>\\k<num> \\k<name>:")
98104
end
99105
end
100106
end

lib/graphql/schema/member/has_fields.rb

Lines changed: 56 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -10,107 +10,79 @@ module HasFields
1010
# @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
1111
# @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
1212
# @param desc_positional [String] Field description; `description:` keyword is also accepted
13-
# @param name [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
14-
# @param type [Class, GraphQL::BaseType, Array] The return type of this field; positional argument is also accepted
15-
# @param null [Boolean] (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
16-
# @param description [String] Field description; positional argument also accepted
17-
# @param comment [String] Field comment
18-
# @param deprecation_reason [String] If present, the field is marked "deprecated" with this message
19-
# @param method [Symbol] The method to call on the underlying object to resolve this field (defaults to `name`)
20-
# @param hash_key [String, Symbol] 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-
# @param dig [Array<String, Symbol>] The nested hash keys to lookup on the underlying hash to resolve this field using dig
22-
# @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`)
23-
# @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
24-
# @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
25-
# @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
26-
# @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results.
27-
# @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
28-
# @param arguments [{String=>GraphQL::Schema::Argument, Hash}] Arguments for this field (may be added in the block, also)
29-
# @param camelize [Boolean] If true, the field name will be camelized when building the schema
30-
# @param complexity [Numeric] When provided, set the complexity for this field
31-
# @param scope [Boolean] If true, the return type's `.scope_items` method will be called on the return value
32-
# @param subscription_scope [Symbol, String] A key in `context` which will be used to scope subscription payloads
33-
# @param extensions [Array<Class, Hash<Class => Object>>] Named extensions to apply to this field (see also {#extension})
34-
# @param directives [Hash{Class => Hash}] Directives to apply to this field
35-
# @param trace [Boolean] If true, a {GraphQL::Tracing} tracer will measure this scalar field
36-
# @param broadcastable [Boolean] Whether or not this field can be distributed in subscription broadcasts
37-
# @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
38-
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
39-
# @param validates [Array<Hash>] Configurations for validating this field
40-
# @param fallback_value [Object] A fallback value if the method is not defined
41-
# @param mutation [Class<GraphQL::Schema::Mutation>]
42-
# @param resolver [Class<GraphQL::Schema::Resolver>]
43-
# @param subscription [Class<GraphQL::Schema::Subscription>]
44-
# @param dynamic_introspection [Boolean] (Private, used by GraphQL-Ruby)
45-
# @param relay_node_field [Boolean] (Private, used by GraphQL-Ruby)
46-
# @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
47-
# @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
48-
# @param custom_kwargs [Hash] Application-specific keywords; must be handled by your base {Field} class
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+
49+
# @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.
4950
# @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}.
5051
# @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
5152
# @yieldreturn [void]
5253
# @return [GraphQL::Schema::Field]
53-
def field(name_positional = nil, type_positional = nil, desc_positional = nil, type: nil, name: 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: NOT_CONFIGURED, resolver: nil, subscription: nil, mutation: 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, **custom_kwargs, &definition_block)
54-
name ||= name_positional
54+
def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
55+
resolver = kwargs.delete(:resolver)
56+
mutation = kwargs.delete(:mutation)
57+
subscription = kwargs.delete(:subscription)
58+
if (resolver_class = resolver || mutation || subscription)
59+
# Add a reference to that parent class
60+
kwargs[:resolver_class] = resolver_class
61+
end
5562

63+
kwargs[:name] ||= name_positional
5664
if !type_positional.nil?
5765
if desc_positional
58-
if !NOT_CONFIGURED.equal?(description)
59-
raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{description.inspect})"
66+
if kwargs[:description]
67+
raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})"
6068
end
6169

62-
description = desc_positional
63-
type = type_positional
70+
kwargs[:description] = desc_positional
71+
kwargs[:type] = type_positional
6472
elsif (resolver || mutation) && type_positional.is_a?(String)
6573
# The return type should be copied from the resolver, and the second positional argument is the description
66-
description = type_positional
74+
kwargs[:description] = type_positional
6775
else
68-
type = type_positional
76+
kwargs[:type] = type_positional
6977
end
7078

7179
if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation
72-
raise ArgumentError, "Use `field #{name.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
80+
raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
7381
end
7482
end
7583

76-
field_defn = field_class.new(
77-
owner: self,
78-
type: type,
79-
name: name,
80-
null: null,
81-
description: description,
82-
comment: comment,
83-
deprecation_reason: deprecation_reason,
84-
method: method,
85-
hash_key: hash_key,
86-
dig: dig,
87-
resolver_method: resolver_method,
88-
connection: connection,
89-
max_page_size: max_page_size,
90-
default_page_size: default_page_size,
91-
scope: scope,
92-
introspection: introspection,
93-
camelize: camelize,
94-
trace: trace,
95-
complexity: complexity,
96-
ast_node: ast_node,
97-
extras: extras,
98-
extensions: extensions,
99-
connection_extension: connection_extension,
100-
resolver_class: resolver || mutation || subscription,
101-
subscription_scope: subscription_scope,
102-
relay_node_field: relay_node_field,
103-
relay_nodes_field: relay_nodes_field,
104-
method_conflict_warning: method_conflict_warning,
105-
broadcastable: broadcastable,
106-
arguments: arguments,
107-
directives: directives,
108-
validates: validates,
109-
fallback_value: fallback_value,
110-
dynamic_introspection: dynamic_introspection,
111-
**custom_kwargs,
112-
&definition_block
113-
)
84+
kwargs[:owner] = self
85+
field_defn = field_class.new(**kwargs, &definition_block)
11486
add_field(field_defn)
11587
field_defn
11688
end
@@ -333,7 +305,7 @@ def visible_interface_implementation?(type, context, warden)
333305
end
334306
end
335307

336-
# @param [GraphQL::Schema::Field]
308+
# @param field_defn [GraphQL::Schema::Field]
337309
# @return [String] A warning to give when this field definition might conflict with a built-in method
338310
def conflict_field_name_warning(field_defn)
339311
"#{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/field_spec.rb

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -927,35 +927,27 @@ class Connection < GraphQL::Schema::Object; end
927927
assert field.connection?
928928
end
929929

930-
describe "argument definitions" do
931-
it "matches the HasFields field arguments for better IDE support" do
932-
field_new_arguments = GraphQL::Schema::Field.instance_method(:initialize).parameters
933-
has_fields_field_arguments = GraphQL::Schema::Member::HasFields.instance_method(:field).parameters
934-
extra_field_args = [
935-
# These are merged into options
936-
[:opt, :name_positional],
937-
[:opt, :type_positional],
938-
[:opt, :desc_positional],
939-
# These become `resolver_class`
940-
[:key, :resolver],
941-
[:key, :subscription],
942-
[:key, :mutation],
943-
# This is a pass-thru for custom keywords
944-
[:keyrest, :custom_kwargs]
945-
]
946-
assert_equal extra_field_args, has_fields_field_arguments - field_new_arguments
947-
extra_new_args = [
948-
[:key, :owner], # populated by the receiver of `.field`
949-
[:key, :resolver_class] # from resolver/subscription/mutation
950-
]
951-
assert_equal extra_new_args, field_new_arguments - has_fields_field_arguments
952-
end
953-
930+
describe "argument documentation" do
954931
it "HasFields::field documents each argument" do
955932
has_fields_field_comment = File.read("./lib/graphql/schema/member/has_fields.rb")[/(\s+#[^\n]*\n)+\s+def field\(/m]
956-
has_field_field_doc_param_names = has_fields_field_comment.split("\n").map { |line| line[/@param (\S+)/]; $1 }.compact
957-
has_fields_field_argument_names = GraphQL::Schema::Member::HasFields.instance_method(:field).parameters.map { |param| param[1].to_s }
958-
assert_equal has_field_field_doc_param_names.sort, has_fields_field_argument_names.sort
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
959951
end
960952

961953
it "Field::initialize documents each argument" do

0 commit comments

Comments
 (0)