diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 48ef821cd5..9839e36402 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,7 +53,7 @@ jobs: ruby: 3.3 graphql_reject_numbers_followed_by_names: 1 - gemfile: gemfiles/rails_8.1.gemfile - ruby: 3.5 + ruby: 4.0 graphql_reject_numbers_followed_by_names: 1 redis: 1 - gemfile: gemfiles/rails_master.gemfile diff --git a/Gemfile b/Gemfile index 77180b4d3d..c8e696ed0b 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,7 @@ end if RUBY_VERSION >= "3.2.0" gem "async", "~>2.0" + gem "minitest-mock" end # Required for running `jekyll algolia ...` (via `rake site:update_search_index`) diff --git a/gemfiles/mongoid_8.gemfile b/gemfiles/mongoid_8.gemfile index 0639b55bfd..3bfd49636f 100644 --- a/gemfiles/mongoid_8.gemfile +++ b/gemfiles/mongoid_8.gemfile @@ -12,5 +12,6 @@ gem "libev_scheduler" gem "evt" gem "async" gem "concurrent-ruby", "1.3.4" +gem "minitest-mock" gemspec path: "../" diff --git a/gemfiles/mongoid_9.gemfile b/gemfiles/mongoid_9.gemfile index b4d8679531..e2f7fe9f8d 100644 --- a/gemfiles/mongoid_9.gemfile +++ b/gemfiles/mongoid_9.gemfile @@ -10,5 +10,6 @@ gem "mongoid", "~> 9.0" gem "libev_scheduler" gem "evt" gem "async" +gem "minitest-mock" gemspec path: "../" diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile index 11c33d0ea5..1944687353 100644 --- a/gemfiles/rails_8.0.gemfile +++ b/gemfiles/rails_8.0.gemfile @@ -13,5 +13,6 @@ gem "sequel" gem "evt" gem "async" gem "google-protobuf" +gem "minitest-mock" gemspec path: "../" diff --git a/gemfiles/rails_8.1.gemfile b/gemfiles/rails_8.1.gemfile index a55c305eaa..566e6ffea9 100644 --- a/gemfiles/rails_8.1.gemfile +++ b/gemfiles/rails_8.1.gemfile @@ -13,5 +13,6 @@ gem "sequel" gem "evt" gem "async" gem "google-protobuf" +gem "minitest-mock" gemspec path: "../" diff --git a/gemfiles/rails_master.gemfile b/gemfiles/rails_master.gemfile index 635da2eec6..6aaaffc970 100644 --- a/gemfiles/rails_master.gemfile +++ b/gemfiles/rails_master.gemfile @@ -22,6 +22,7 @@ gem 'puma' gem 'sprockets-rails' gem 'capybara' gem 'selenium-webdriver' +gem "minitest-mock" gemspec path: "../" diff --git a/graphql.gemspec b/graphql.gemspec index 30bb67c69a..abb03cd849 100644 --- a/graphql.gemspec +++ b/graphql.gemspec @@ -37,7 +37,6 @@ Gem::Specification.new do |s| s.add_development_dependency "minitest" s.add_development_dependency "minitest-focus" - s.add_development_dependency "minitest-mock" s.add_development_dependency "minitest-reporters" s.add_development_dependency "ostruct" s.add_development_dependency "rake" diff --git a/lib/graphql/schema/field.rb b/lib/graphql/schema/field.rb index 98cf087d06..1cc7a236e5 100644 --- a/lib/graphql/schema/field.rb +++ b/lib/graphql/schema/field.rb @@ -109,52 +109,6 @@ def subscription_scope end attr_writer :subscription_scope - # Create a field instance from a list of arguments, keyword arguments, and a block. - # - # This method implements prioritization between the `resolver` or `mutation` defaults - # and the local overrides via other keywords. - # - # It also normalizes positional arguments into keywords for {Schema::Field#initialize}. - # @param resolver [Class] A {GraphQL::Schema::Resolver} class to use for field configuration - # @param mutation [Class] A {GraphQL::Schema::Mutation} class to use for field configuration - # @param subscription [Class] A {GraphQL::Schema::Subscription} class to use for field configuration - # @return [GraphQL::Schema:Field] an instance of `self` - # @see {.initialize} for other options - def self.from_options(name = nil, type = nil, desc = nil, comment: nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block) - if (resolver_class = resolver || mutation || subscription) - # Add a reference to that parent class - kwargs[:resolver_class] = resolver_class - end - - if name - kwargs[:name] = name - end - - if comment - kwargs[:comment] = comment - end - - if !type.nil? - if desc - if kwargs[:description] - raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc.inspect}, #{kwargs[:description].inspect})" - end - - kwargs[:description] = desc - kwargs[:type] = type - elsif (resolver || mutation) && type.is_a?(String) - # The return type should be copied from the resolver, and the second positional argument is the description - kwargs[:description] = type - else - kwargs[:type] = type - end - if type.is_a?(Class) && type < GraphQL::Schema::Mutation - raise ArgumentError, "Use `field #{name.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead" - end - end - new(**kwargs, &block) - end - # Can be set with `connection: true|false` or inferred from a type name ending in `*Connection` # @return [Boolean] if true, this field will be wrapped with Relay connection behavior def connection? diff --git a/lib/graphql/schema/member/has_fields.rb b/lib/graphql/schema/member/has_fields.rb index 8d289181b8..3a969c9dec 100644 --- a/lib/graphql/schema/member/has_fields.rb +++ b/lib/graphql/schema/member/has_fields.rb @@ -6,10 +6,42 @@ class Member # Shared code for Objects, Interfaces, Mutations, Subscriptions module HasFields # Add a field to this object or interface with the given definition - # @see {GraphQL::Schema::Field#initialize} for method signature + # @param name_positional [Symbol] Keyword `name:` also supported + # @param type_positional [Class, Array] Keyword `type:` also supported + # @param desc_positional [String] Keyword `description:` also supported + # @see {GraphQL::Schema::Field#initialize} for keywords # @return [GraphQL::Schema::Field] - def field(*args, **kwargs, &block) - field_defn = field_class.from_options(*args, owner: self, **kwargs, &block) + def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &block) + resolver = kwargs.delete(:resolver) + mutation = kwargs.delete(:mutation) + subscription = kwargs.delete(:subscription) + if (resolver_class = resolver || mutation || subscription) + # Add a reference to that parent class + kwargs[:resolver_class] = resolver_class + end + + kwargs[:name] ||= name_positional + if !type_positional.nil? + if desc_positional + if kwargs[:description] + raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})" + end + + kwargs[:description] = desc_positional + kwargs[:type] = type_positional + elsif (resolver || mutation) && type_positional.is_a?(String) + # The return type should be copied from the resolver, and the second positional argument is the description + kwargs[:description] = type_positional + else + kwargs[:type] = type_positional + end + + if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation + raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead" + end + end + + field_defn = field_class.new(owner: self, **kwargs, &block) add_field(field_defn) field_defn end diff --git a/spec/graphql/schema/directive_spec.rb b/spec/graphql/schema/directive_spec.rb index ea92524daa..9957332545 100644 --- a/spec/graphql/schema/directive_spec.rb +++ b/spec/graphql/schema/directive_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "spec_helper" -describe GraphQL::Schema::Directive do +describe "GraphQL Schema::Directive" do class MultiWord < GraphQL::Schema::Directive end @@ -78,13 +78,7 @@ class Thing < GraphQL::Schema::Object it "validates arguments" do err = assert_raises GraphQL::Schema::Directive::InvalidArgumentError do - GraphQL::Schema::Field.from_options( - name: :something, - type: String, - null: false, - owner: DirectiveTest::Thing, - directives: { DirectiveTest::Secret => {} } - ) + DirectiveTest::Thing.field(:something, String, null: false, directives: { DirectiveTest::Secret => {} }) end assert_equal "@secret.topSecret on Thing.something is invalid (nil): Expected value to not be null", err.message diff --git a/spec/graphql/schema/field_spec.rb b/spec/graphql/schema/field_spec.rb index f68ac18a31..1bd8693de2 100644 --- a/spec/graphql/schema/field_spec.rb +++ b/spec/graphql/schema/field_spec.rb @@ -30,7 +30,8 @@ it "camelizes the field name, unless camelize: false" do assert_equal 'inspectInput', field.name - underscored_field = GraphQL::Schema::Field.from_options(:underscored_field, String, null: false, camelize: false, owner: nil) do + example_obj = Class.new(GraphQL::Schema::Object) { graphql_name("Example") } + underscored_field = example_obj.field(:underscored_field, String, null: false, camelize: false, owner: nil) do argument :underscored_arg, String, camelize: false end.ensure_loaded @@ -141,7 +142,8 @@ class Query < GraphQL::Schema::Object type = Class.new(GraphQL::Schema::Object) do graphql_name 'MyType' end - field = GraphQL::Schema::Field.from_options(:my_field, type, owner: nil, null: true) + example_obj = Class.new(GraphQL::Schema::Object) { graphql_name("Example") } + field = example_obj.field(:my_field, type, owner: nil, null: true) assert_equal type, field.type end @@ -500,7 +502,8 @@ def argument_details(argument_details:, arg1: nil, arg2:) describe "#original_name" do it "is exactly the same as the passed in name" do - field = GraphQL::Schema::Field.from_options( + example_obj = Class.new(GraphQL::Schema::Object) { graphql_name("Example") } + field = example_obj.field( :my_field, String, null: false,