diff --git a/guides/fields/introduction.md b/guides/fields/introduction.md index 1c62a52f39..c84fd8d3d9 100644 --- a/guides/fields/introduction.md +++ b/guides/fields/introduction.md @@ -21,7 +21,7 @@ The different elements of field definition are addressed below: - [Names](#field-names) identify the field in GraphQL - [Return types](#field-return-type) say what kind of data this field returns -- [Documentation](#field-documentation) includes description and deprecation notes +- [Documentation](#field-documentation) includes description, comments and deprecation notes - [Resolution behavior](#field-resolution) hooks up Ruby code to the GraphQL field - [Arguments](#field-arguments) allow fields to take input when they're queried - [Extra field metadata](#extra-field-metadata) for low-level access to the GraphQL-Ruby runtime @@ -67,7 +67,7 @@ field :scores, [Integer, null: true] # `[Int]`, may return a list or `nil`, the ## Field Documentation -Fields may be documented with a __description__ and may be __deprecated__. +Fields may be documented with a __description__, __comment__ and may be __deprecated__. __Descriptions__ can be added with the `field(...)` method as a positional argument, a keyword argument, or inside the block: @@ -85,6 +85,26 @@ field :name, String, null: false do end ``` +__Comments__ can be added with the `field(...)` method as a keyword argument, or inside the block: +```ruby +# `comment:` keyword +field :name, String, null: false, comment: "Rename to full name" + +# inside the block +field :name, String, null: false do + comment "Rename to full name" +end +``` + +Generates field name with comment above "Rename to full name" above. + +```graphql +type Foo { + # Rename to full name + name: String! +} +``` + __Deprecated__ fields can be marked by adding a `deprecation_reason:` keyword argument: ```ruby diff --git a/guides/type_definitions/enums.md b/guides/type_definitions/enums.md index 9feda4e5ed..120c08cfc9 100644 --- a/guides/type_definitions/enums.md +++ b/guides/type_definitions/enums.md @@ -59,6 +59,7 @@ end Each value may have: - A description (as the second argument or `description:` keyword) +- A comment (as a `comment:` keyword) - A deprecation reason (as `deprecation_reason:`), marking this value as deprecated - A corresponding Ruby value (as `value:`), see below diff --git a/guides/type_definitions/interfaces.md b/guides/type_definitions/interfaces.md index 7e95678d2a..254cfd18bf 100644 --- a/guides/type_definitions/interfaces.md +++ b/guides/type_definitions/interfaces.md @@ -80,6 +80,7 @@ Then, include that into each interface: ```ruby module Types::RetailItem include Types::BaseInterface + comment "TODO comment in the RetailItem interface" description "Something that can be bought" field :price, Types::Price, "How much this item costs", null: false diff --git a/guides/type_definitions/objects.md b/guides/type_definitions/objects.md index a61fec57a5..1718c2810b 100644 --- a/guides/type_definitions/objects.md +++ b/guides/type_definitions/objects.md @@ -71,6 +71,7 @@ end # then... class Types::TodoList < Types::BaseObject + comment "Comment of the TodoList type" description "A list of items which may be completed" field :name, String, "The unique name of this list", null: false diff --git a/guides/type_definitions/scalars.md b/guides/type_definitions/scalars.md index cada689ef3..209ef1cead 100644 --- a/guides/type_definitions/scalars.md +++ b/guides/type_definitions/scalars.md @@ -73,6 +73,7 @@ end # app/graphql/types/url.rb class Types::Url < Types::BaseScalar + comment "TODO comment of the scalar" description "A valid URL, transported as a string" def self.coerce_input(input_value, context) diff --git a/guides/type_definitions/unions.md b/guides/type_definitions/unions.md index d9ec127709..2c03a5b122 100644 --- a/guides/type_definitions/unions.md +++ b/guides/type_definitions/unions.md @@ -54,6 +54,7 @@ Then, extend that one for each union in your schema: ```ruby class Types::CommentSubject < Types::BaseUnion + comment "TODO comment on the union" description "Objects which may be commented on" possible_types Types::Post, Types::Image diff --git a/lib/graphql/language.rb b/lib/graphql/language.rb index a98eefb637..4fdef5b736 100644 --- a/lib/graphql/language.rb +++ b/lib/graphql/language.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require "graphql/language/block_string" +require "graphql/language/comment" require "graphql/language/printer" require "graphql/language/sanitized_printer" require "graphql/language/document_from_schema_definition" diff --git a/lib/graphql/language/comment.rb b/lib/graphql/language/comment.rb new file mode 100644 index 0000000000..a1064399bf --- /dev/null +++ b/lib/graphql/language/comment.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module GraphQL + module Language + module Comment + def self.print(str, indent: '') + lines = str.split("\n").map do |line| + comment_str = "".dup + comment_str << indent + comment_str << "# " + comment_str << line + comment_str.rstrip + end + + lines.join("\n") + "\n" + end + end + end +end diff --git a/lib/graphql/language/document_from_schema_definition.rb b/lib/graphql/language/document_from_schema_definition.rb index 2c4426ead1..6dd6c43773 100644 --- a/lib/graphql/language/document_from_schema_definition.rb +++ b/lib/graphql/language/document_from_schema_definition.rb @@ -58,6 +58,7 @@ def build_object_type_node(object_type) GraphQL::Language::Nodes::ObjectTypeDefinition.new( name: object_type.graphql_name, + comment: object_type.comment, interfaces: ints, fields: build_field_nodes(@types.fields(object_type)), description: object_type.description, @@ -68,6 +69,7 @@ def build_object_type_node(object_type) def build_field_node(field) GraphQL::Language::Nodes::FieldDefinition.new( name: field.graphql_name, + comment: field.comment, arguments: build_argument_nodes(@types.arguments(field)), type: build_type_name_node(field.type), description: field.description, @@ -78,6 +80,7 @@ def build_field_node(field) def build_union_type_node(union_type) GraphQL::Language::Nodes::UnionTypeDefinition.new( name: union_type.graphql_name, + comment: union_type.comment, description: union_type.description, types: @types.possible_types(union_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, directives: directives(union_type), @@ -87,6 +90,7 @@ def build_union_type_node(union_type) def build_interface_type_node(interface_type) GraphQL::Language::Nodes::InterfaceTypeDefinition.new( name: interface_type.graphql_name, + comment: interface_type.comment, interfaces: @types.interfaces(interface_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, description: interface_type.description, fields: build_field_nodes(@types.fields(interface_type)), @@ -97,6 +101,7 @@ def build_interface_type_node(interface_type) def build_enum_type_node(enum_type) GraphQL::Language::Nodes::EnumTypeDefinition.new( name: enum_type.graphql_name, + comment: enum_type.comment, values: @types.enum_values(enum_type).sort_by(&:graphql_name).map do |enum_value| build_enum_value_node(enum_value) end, @@ -108,6 +113,7 @@ def build_enum_type_node(enum_type) def build_enum_value_node(enum_value) GraphQL::Language::Nodes::EnumValueDefinition.new( name: enum_value.graphql_name, + comment: enum_value.comment, description: enum_value.description, directives: directives(enum_value), ) @@ -116,6 +122,7 @@ def build_enum_value_node(enum_value) def build_scalar_type_node(scalar_type) GraphQL::Language::Nodes::ScalarTypeDefinition.new( name: scalar_type.graphql_name, + comment: scalar_type.comment, description: scalar_type.description, directives: directives(scalar_type), ) @@ -130,6 +137,7 @@ def build_argument_node(argument) argument_node = GraphQL::Language::Nodes::InputValueDefinition.new( name: argument.graphql_name, + comment: argument.comment, description: argument.description, type: build_type_name_node(argument.type), default_value: default_value, @@ -142,6 +150,7 @@ def build_argument_node(argument) def build_input_object_node(input_object) GraphQL::Language::Nodes::InputObjectTypeDefinition.new( name: input_object.graphql_name, + comment: input_object.comment, fields: build_argument_nodes(@types.arguments(input_object)), description: input_object.description, directives: directives(input_object), diff --git a/lib/graphql/language/nodes.rb b/lib/graphql/language/nodes.rb index 7a3220e3a5..cd60f358d2 100644 --- a/lib/graphql/language/nodes.rb +++ b/lib/graphql/language/nodes.rb @@ -270,15 +270,17 @@ def scalars "col: nil", "pos: nil", "filename: nil", - "source: nil", + "source: nil" ] + IGNORED_MARSHALLING_KEYWORDS = [:comment] + def generate_initialize return if method_defined?(:marshal_load, false) # checking for `:initialize` doesn't work right scalar_method_names = @scalar_methods # TODO: These probably should be scalar methods, but `types` returns an array - [:types, :description].each do |extra_method| + [:types, :description, :comment].each do |extra_method| if method_defined?(extra_method) scalar_method_names += [extra_method] end @@ -307,6 +309,12 @@ def generate_initialize keywords = scalar_method_names.map { |m| "#{m}: #{m}"} + children_method_names.map { |m| "#{m}: #{m}" } + ignored_keywords = IGNORED_MARSHALLING_KEYWORDS.map do |keyword| + "#{keyword.to_s}: nil" + end + + marshalling_method_names = all_method_names - IGNORED_MARSHALLING_KEYWORDS + module_eval <<-RUBY, __FILE__, __LINE__ def initialize(#{arguments.join(", ")}) @line = line @@ -317,7 +325,7 @@ def initialize(#{arguments.join(", ")}) #{assignments.join("\n")} end - def self.from_a(filename, line, col, #{all_method_names.join(", ")}) + def self.from_a(filename, line, col, #{marshalling_method_names.join(", ")}, #{ignored_keywords.join(", ")}) self.new(filename: filename, line: line, col: col, #{keywords.join(", ")}) end @@ -325,12 +333,12 @@ def marshal_dump [ line, col, # use methods here to force them to be calculated @filename, - #{all_method_names.map { |n| "@#{n}," }.join} + #{marshalling_method_names.map { |n| "@#{n}," }.join} ] end def marshal_load(values) - @line, @col, @filename #{all_method_names.map { |n| ", @#{n}"}.join} = values + @line, @col, @filename #{marshalling_method_names.map { |n| ", @#{n}"}.join} = values end RUBY end @@ -635,7 +643,7 @@ class SchemaExtension < AbstractNode end class ScalarTypeDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, @@ -652,7 +660,7 @@ class ScalarTypeExtension < AbstractNode end class InputValueDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name, :type, :default_value children_methods({ directives: GraphQL::Language::Nodes::Directive, @@ -661,7 +669,7 @@ class InputValueDefinition < AbstractNode end class FieldDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name, :type children_methods({ arguments: GraphQL::Language::Nodes::InputValueDefinition, @@ -681,7 +689,7 @@ def merge(new_options) end class ObjectTypeDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name, :interfaces children_methods({ directives: GraphQL::Language::Nodes::Directive, @@ -700,7 +708,7 @@ class ObjectTypeExtension < AbstractNode end class InterfaceTypeDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name children_methods({ interfaces: GraphQL::Language::Nodes::TypeName, @@ -721,7 +729,7 @@ class InterfaceTypeExtension < AbstractNode end class UnionTypeDefinition < AbstractNode - attr_reader :description, :types + attr_reader :description, :comment, :types scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, @@ -739,7 +747,7 @@ class UnionTypeExtension < AbstractNode end class EnumValueDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, @@ -748,7 +756,7 @@ class EnumValueDefinition < AbstractNode end class EnumTypeDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, @@ -767,7 +775,7 @@ class EnumTypeExtension < AbstractNode end class InputObjectTypeDefinition < AbstractNode - attr_reader :description + attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, diff --git a/lib/graphql/language/printer.rb b/lib/graphql/language/printer.rb index 04563c2c1d..eb8a683351 100644 --- a/lib/graphql/language/printer.rb +++ b/lib/graphql/language/printer.rb @@ -255,14 +255,14 @@ def print_schema_definition(schema, extension: false) def print_scalar_type_definition(scalar_type, extension: false) - extension ? print_string("extend ") : print_description(scalar_type) + extension ? print_string("extend ") : print_description_and_comment(scalar_type) print_string("scalar ") print_string(scalar_type.name) print_directives(scalar_type.directives) end def print_object_type_definition(object_type, extension: false) - extension ? print_string("extend ") : print_description(object_type) + extension ? print_string("extend ") : print_description_and_comment(object_type) print_string("type ") print_string(object_type.name) print_implements(object_type) unless object_type.interfaces.empty? @@ -294,7 +294,7 @@ def print_input_value_definition(input_value) end def print_arguments(arguments, indent: "") - if arguments.all? { |arg| !arg.description } + if arguments.all? { |arg| !arg.description && !arg.comment } print_string("(") arguments.each_with_index do |arg, i| print_input_value_definition(arg) @@ -306,6 +306,7 @@ def print_arguments(arguments, indent: "") print_string("(\n") arguments.each_with_index do |arg, i| + print_comment(arg, indent: " " + indent, first_in_block: i == 0) print_description(arg, indent: " " + indent, first_in_block: i == 0) print_string(" ") print_string(indent) @@ -328,7 +329,7 @@ def print_field_definition(field) end def print_interface_type_definition(interface_type, extension: false) - extension ? print_string("extend ") : print_description(interface_type) + extension ? print_string("extend ") : print_description_and_comment(interface_type) print_string("interface ") print_string(interface_type.name) print_implements(interface_type) if interface_type.interfaces.any? @@ -337,7 +338,7 @@ def print_interface_type_definition(interface_type, extension: false) end def print_union_type_definition(union_type, extension: false) - extension ? print_string("extend ") : print_description(union_type) + extension ? print_string("extend ") : print_description_and_comment(union_type) print_string("union ") print_string(union_type.name) print_directives(union_type.directives) @@ -355,7 +356,7 @@ def print_union_type_definition(union_type, extension: false) end def print_enum_type_definition(enum_type, extension: false) - extension ? print_string("extend ") : print_description(enum_type) + extension ? print_string("extend ") : print_description_and_comment(enum_type) print_string("enum ") print_string(enum_type.name) print_directives(enum_type.directives) @@ -363,6 +364,7 @@ def print_enum_type_definition(enum_type, extension: false) print_string(" {\n") enum_type.values.each.with_index do |value, i| print_description(value, indent: " ", first_in_block: i == 0) + print_comment(value, indent: " ", first_in_block: i == 0) print_enum_value_definition(value) end print_string("}") @@ -377,7 +379,7 @@ def print_enum_value_definition(enum_value) end def print_input_object_type_definition(input_object_type, extension: false) - extension ? print_string("extend ") : print_description(input_object_type) + extension ? print_string("extend ") : print_description_and_comment(input_object_type) print_string("input ") print_string(input_object_type.name) print_directives(input_object_type.directives) @@ -385,6 +387,7 @@ def print_input_object_type_definition(input_object_type, extension: false) print_string(" {\n") input_object_type.fields.each.with_index do |field, i| print_description(field, indent: " ", first_in_block: i == 0) + print_comment(field, indent: " ", first_in_block: i == 0) print_string(" ") print_input_value_definition(field) print_string("\n") @@ -424,6 +427,18 @@ def print_description(node, indent: "", first_in_block: true) print_string(GraphQL::Language::BlockString.print(node.description, indent: indent)) end + def print_comment(node, indent: "", first_in_block: true) + return unless node.comment + + print_string("\n") if indent != "" && !first_in_block + print_string(GraphQL::Language::Comment.print(node.comment, indent: indent)) + end + + def print_description_and_comment(node) + print_description(node) + print_comment(node) + end + def print_field_definitions(fields) return if fields.empty? @@ -431,6 +446,7 @@ def print_field_definitions(fields) i = 0 fields.each do |field| print_description(field, indent: " ", first_in_block: i == 0) + print_comment(field, indent: " ", first_in_block: i == 0) print_string(" ") print_field_definition(field) print_string("\n") diff --git a/lib/graphql/schema/argument.rb b/lib/graphql/schema/argument.rb index 08cbc90086..6f2a2d65e6 100644 --- a/lib/graphql/schema/argument.rb +++ b/lib/graphql/schema/argument.rb @@ -50,11 +50,12 @@ def from_resolver? # @param deprecation_reason [String] # @param validates [Hash, nil] Options for building validators, if any should be applied # @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value` - def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: 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) + 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) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) @type_expr = type_expr || type @description = desc || description + @comment = comment @null = required != true @default_value = default_value if replace_null_with_default @@ -129,6 +130,17 @@ def description(text = nil) end end + attr_writer :comment + + # @return [String] Comment for this argument + def comment(text = nil) + if text + @comment = text + else + @comment + end + end + # @return [String] Deprecation reason for this argument def deprecation_reason(text = nil) if text diff --git a/lib/graphql/schema/enum.rb b/lib/graphql/schema/enum.rb index 3a20c59300..006963d7ab 100644 --- a/lib/graphql/schema/enum.rb +++ b/lib/graphql/schema/enum.rb @@ -59,6 +59,7 @@ class << self # Define a value for this enum # @option kwargs [String, Symbol] :graphql_name the GraphQL value for this, usually `SCREAMING_CASE` # @option kwargs [String] :description, the GraphQL description for this value, present in documentation + # @option kwargs [String] :comment, the GraphQL comment for this value, present in documentation # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`) # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here # @return [void] diff --git a/lib/graphql/schema/enum_value.rb b/lib/graphql/schema/enum_value.rb index 8510f909de..9a28e0d4ac 100644 --- a/lib/graphql/schema/enum_value.rb +++ b/lib/graphql/schema/enum_value.rb @@ -30,10 +30,11 @@ class EnumValue < GraphQL::Schema::Member # @return [Class] The enum type that owns this value attr_reader :owner - def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block) + def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, comment: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block) @graphql_name = graphql_name.to_s GraphQL::NameValidator.validate!(@graphql_name) @description = desc || description + @comment = comment @value = value == NOT_CONFIGURED ? @graphql_name : value if deprecation_reason self.deprecation_reason = deprecation_reason @@ -58,6 +59,13 @@ def description(new_desc = nil) @description end + def comment(new_comment = nil) + if new_comment + @comment = new_comment + end + @comment + end + def value(new_val = nil) unless new_val.nil? @value = new_val diff --git a/lib/graphql/schema/field.rb b/lib/graphql/schema/field.rb index da7a3bf52f..94fe420ea9 100644 --- a/lib/graphql/schema/field.rb +++ b/lib/graphql/schema/field.rb @@ -106,7 +106,7 @@ def subscription_scope # @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, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block) + 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 @@ -116,6 +116,10 @@ def self.from_options(name = nil, type = nil, desc = nil, resolver: nil, mutatio kwargs[:name] = name end + if comment + kwargs[:comment] = comment + end + if !type.nil? if desc if kwargs[:description] @@ -212,6 +216,7 @@ def method_conflict_warning? # @param owner [Class] The type that this field belongs to # @param null [Boolean] (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null` # @param description [String] Field description + # @param comment [String] Field comment # @param deprecation_reason [String] If present, the field is marked "deprecated" with this message # @param method [Symbol] The method to call on the underlying object to resolve this field (defaults to `name`) # @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`) @@ -236,7 +241,7 @@ def method_conflict_warning? # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method # @param validates [Array] Configurations for validating this field # @param fallback_value [Object] A fallback value if the method is not defined - def initialize(type: nil, name: nil, owner: nil, null: nil, description: 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) + 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) if name.nil? raise ArgumentError, "missing first `name` argument or keyword `name:`" end @@ -252,6 +257,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) @description = description + @comment = comment @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary self.deprecation_reason = deprecation_reason @@ -400,6 +406,20 @@ def description(text = nil) end end + # @param text [String] + # @return [String, nil] + def comment(text = nil) + if text + @comment = text + elsif !NOT_CONFIGURED.equal?(@comment) + @comment + elsif @resolver_class + @resolver_class.comment + else + nil + end + end + # Read extension instances from this field, # or add new classes/options to be initialized on this field. # Extensions are executed in the order they are added. diff --git a/lib/graphql/schema/interface.rb b/lib/graphql/schema/interface.rb index bf04573333..ada8e2428d 100644 --- a/lib/graphql/schema/interface.rb +++ b/lib/graphql/schema/interface.rb @@ -63,6 +63,7 @@ def included(child_class) child_class.introspection(introspection) child_class.description(description) + child_class.comment(nil) # If interfaces are mixed into each other, only define this class once if !child_class.const_defined?(:UnresolvedTypeError, false) add_unresolved_type_error(child_class) diff --git a/lib/graphql/schema/member/base_dsl_methods.rb b/lib/graphql/schema/member/base_dsl_methods.rb index 8f97dd6d7b..ea197ff8dd 100644 --- a/lib/graphql/schema/member/base_dsl_methods.rb +++ b/lib/graphql/schema/member/base_dsl_methods.rb @@ -50,12 +50,27 @@ def description(new_description = nil) end end + # Call this method to provide a new comment; OR + # call it without an argument to get the comment + # @param new_comment [String] + # @return [String, nil] + def comment(new_comment = NOT_CONFIGURED) + if !NOT_CONFIGURED.equal?(new_comment) + @comment = new_comment + elsif defined?(@comment) + @comment + else + nil + end + end + # This pushes some configurations _down_ the inheritance tree, # in order to prevent repetitive lookups at runtime. module ConfigurationExtension def inherited(child_class) child_class.introspection(introspection) child_class.description(description) + child_class.comment(nil) child_class.default_graphql_name = nil if defined?(@graphql_name) && @graphql_name && (self.name.nil? || graphql_name != default_graphql_name) diff --git a/lib/graphql/schema/resolver.rb b/lib/graphql/schema/resolver.rb index ce5e746885..703ba5f349 100644 --- a/lib/graphql/schema/resolver.rb +++ b/lib/graphql/schema/resolver.rb @@ -8,6 +8,7 @@ class Schema # - Arguments, via `.argument(...)` helper, which will be applied to the field. # - Return type, via `.type(..., null: ...)`, which will be applied to the field. # - Description, via `.description(...)`, which will be applied to the field + # - Comment, via `.comment(...)`, which will be applied to the field # - Resolution, via `#resolve(**args)` method, which will be called to resolve the field. # - `#object` and `#context` accessors for use during `#resolve`. # @@ -19,7 +20,7 @@ class Schema # @see {GraphQL::Function} `Resolver` is a replacement for `GraphQL::Function` class Resolver include Schema::Member::GraphQLTypeNames - # Really we only need description from here, but: + # Really we only need description & comment from here, but: extend Schema::Member::BaseDSLMethods extend GraphQL::Schema::Member::HasArguments extend GraphQL::Schema::Member::HasValidators diff --git a/spec/graphql/language/printer_spec.rb b/spec/graphql/language/printer_spec.rb index 894a6e0e69..220357277b 100644 --- a/spec/graphql/language/printer_spec.rb +++ b/spec/graphql/language/printer_spec.rb @@ -263,6 +263,174 @@ end end + it "handles comments" do + module MyInterface + include GraphQL::Schema::Interface + + comment "Interface comment" + end + + scalar = Class.new(GraphQL::Schema::Scalar) do + graphql_name "DateTime" + + comment "Scalar comment" + end + + query_type = Class.new(GraphQL::Schema::Object) do + implements MyInterface + + graphql_name "Query" + field :issue, Integer, comment: "Field comment" do + argument :number, Integer, comment: "Argument comment" + argument :date_time, scalar + end + + def issue(number:) + number + end + end + + enum_type = Class.new(GraphQL::Schema::Enum) do + graphql_name "UserRole" + + comment "Enum comment" + + value "ADMIN" + value "VIEWER", comment: "Enum value comment" + end + + input_object = Class.new(GraphQL::Schema::InputObject) do + graphql_name "CreateUserInput" + + comment "Input object comment" + + argument :first_name, String, comment: "Argument comment" + argument :role, enum_type do + comment "Argument comment" + end + end + + union = Class.new(GraphQL::Schema::Union) do + graphql_name "CreateUserResponse" + + comment "Union comment" + + possible_types( + Class.new(GraphQL::Schema::Object) do + graphql_name "CreateUserSuccess" + + field :user, (Class.new(GraphQL::Schema::Object) do + graphql_name "User" + + field :first_name, String, comment: "Field comment" + end) + end, + Class.new(GraphQL::Schema::Object) do + graphql_name "CreateUserError" + + comment "Object type comment" + + field :message, String, null: false do + comment "Field comment" + end + end + ) + end + + mutation = Class.new(GraphQL::Schema::Mutation) do + graphql_name "CreateUser" + comment "Mutation comment" + + argument :input, input_object, comment: "Input argument comment" + + field :payload, union, null: false + end + + mutation_type = Class.new(GraphQL::Schema::Object) do + graphql_name "Mutation" + + field :create_user, mutation: mutation + end + + schema = Class.new(GraphQL::Schema) do + query(query_type) + mutation(mutation_type) + end + + expected = <<~SCHEMA.chomp + # Object type comment + type CreateUserError { + # Field comment + message: String! + } + + # Input object comment + input CreateUserInput { + # Argument comment + firstName: String! + + # Argument comment + role: UserRole! + } + + """ + Autogenerated return type of CreateUser. + """ + type CreateUserPayload { + payload: CreateUserResponse! + } + + # Union comment + union CreateUserResponse = CreateUserError | CreateUserSuccess + + type CreateUserSuccess { + user: User + } + + # Scalar comment + scalar DateTime + + type Mutation { + # Mutation comment + createUser( + # Input argument comment + input: CreateUserInput! + ): CreateUserPayload + } + + # Interface comment + interface MyInterface + + type Query implements MyInterface { + # Field comment + issue( + dateTime: DateTime! + + # Argument comment + number: Int! + ): Int + } + + type User { + # Field comment + firstName: String + } + + # Enum comment + enum UserRole { + ADMIN + + # Enum value comment + VIEWER + } + SCHEMA + + assert_equal( + expected, + printer.print(schema.to_document), + ) + end + it "handles large ints" do query_type = Class.new(GraphQL::Schema::Object) do graphql_name "Query" diff --git a/spec/graphql/schema/argument_spec.rb b/spec/graphql/schema/argument_spec.rb index 6e1ddfe0f0..cc2079c6b2 100644 --- a/spec/graphql/schema/argument_spec.rb +++ b/spec/graphql/schema/argument_spec.rb @@ -29,11 +29,12 @@ def resolve(instruments:) class Query < GraphQL::Schema::Object field :field, String do - argument :arg, String, description: "test", required: false + argument :arg, String, description: "test", comment: "test comment", required: false argument :deprecated_arg, String, deprecation_reason: "don't use me!", required: false argument :arg_with_block, String, required: false do description "test" + comment "test comment" end argument :required_with_default_arg, Int, default_value: 1 argument :aliased_arg, String, required: false, as: :renamed @@ -147,6 +148,24 @@ def self.resolve_type(type, obj, ctx) end end + describe "#comment" do + let(:arg) { SchemaArgumentTest::Query.fields["field"].arguments["arg"] } + + it "sets comment" do + arg.comment "new comment" + assert_equal "new comment", arg.comment + end + + it "returns comment" do + assert_equal "test comment", SchemaArgumentTest::Query.fields["field"].arguments["argWithBlock"].comment + end + + it "has an assignment method" do + arg.comment = "another new comment" + assert_equal "another new comment", arg.comment + end + end + describe "as:" do it "uses that Symbol for Ruby kwargs" do query_str = <<-GRAPHQL diff --git a/spec/graphql/schema/field_spec.rb b/spec/graphql/schema/field_spec.rb index 129900bc34..1ace9ffc8a 100644 --- a/spec/graphql/schema/field_spec.rb +++ b/spec/graphql/schema/field_spec.rb @@ -61,6 +61,7 @@ field_defn = field :test do argument :test, String description "A Description." + comment "A Comment." type String end end @@ -72,6 +73,7 @@ assert_equal "test", object.fields["test"].arguments["test"].name assert_equal "A Description.", object.fields["test"].description + assert_equal "A Comment.", object.fields["test"].comment end it "sets connection? when type is given in a block" do @@ -100,11 +102,13 @@ field :test, String do |field| field.argument :test, String field.description "A Description." + field.comment "A Comment." end end assert_equal "test", object.fields["test"].arguments["test"].name assert_equal "A Description.", object.fields["test"].description + assert_equal "A Comment.", object.fields["test"].comment end it "accepts anonymous classes as type" do @@ -744,6 +748,7 @@ def ostruct_results it "Delegates many properties to its @resolver_class" do resolver = Class.new(GraphQL::Schema::Resolver) do description "description 1" + comment "comment 1" type [GraphQL::Types::Float], null: true argument :b, GraphQL::Types::Float @@ -755,6 +760,7 @@ def ostruct_results field.ensure_loaded assert_equal "description 1", field.description + assert_equal "comment 1", field.comment assert_equal "[Float!]", field.type.to_type_signature assert_equal 1, field.complexity assert_equal :resolve_with_support, field.resolver_method @@ -766,6 +772,7 @@ def ostruct_results assert_equal true, field.scoped? resolver.description("description 2") + resolver.comment("comment 2") resolver.type(GraphQL::Types::String, null: false) resolver.complexity(5) resolver.resolver_method(:blah) @@ -775,6 +782,7 @@ def ostruct_results resolver.argument(:c, GraphQL::Types::Boolean) assert_equal "description 2", field.description + assert_equal "comment 2", field.comment assert_equal "String!", field.type.to_type_signature assert_equal 5, field.complexity assert_equal :blah, field.resolver_method diff --git a/spec/graphql/schema/interface_spec.rb b/spec/graphql/schema/interface_spec.rb index 851da35deb..cbfa41c691 100644 --- a/spec/graphql/schema/interface_spec.rb +++ b/spec/graphql/schema/interface_spec.rb @@ -158,6 +158,25 @@ module InterfaceE end end + describe "comments" do + class SchemaWithInterface < GraphQL::Schema + module InterfaceWithComment + include GraphQL::Schema::Interface + comment "Interface comment" + end + + class Query < GraphQL::Schema::Object + implements InterfaceWithComment + end + + query(Query) + end + + it "assigns comment to the interface" do + assert_equal("Interface comment", SchemaWithInterface::Query.interfaces[0].comment) + end + end + describe "can implement other interfaces" do class InterfaceImplementsSchema < GraphQL::Schema module InterfaceA @@ -662,4 +681,22 @@ def pet(name:) end end end + + describe ".comment" do + it "isn't inherited" do + int1 = Module.new do + include GraphQL::Schema::Interface + graphql_name "Int1" + comment "TODO: fix this" + end + + int2 = Module.new do + include int1 + graphql_name "Int2" + end + + assert_equal "TODO: fix this", int1.comment + assert_nil int2.comment + end + end end diff --git a/spec/graphql/schema/object_spec.rb b/spec/graphql/schema/object_spec.rb index b732a68c4b..cc7fd003fd 100644 --- a/spec/graphql/schema/object_spec.rb +++ b/spec/graphql/schema/object_spec.rb @@ -396,15 +396,28 @@ def self.type_error(err, ctx) default_edge_shape = Class.new(GraphQL::Types::Relay::BaseEdge).instance_variables default_connection_shape = Class.new(GraphQL::Types::Relay::BaseConnection).instance_variables default_mutation_payload_shape = Class.new(GraphQL::Schema::RelayClassicMutation) { graphql_name("DoSomething") }.payload_type.instance_variables - expected_default_shapes = Set.new([ + expected_default_shapes = [ default_shape, default_shape_with_connection_type, default_edge_shape, default_connection_shape, default_mutation_payload_shape - ]) - - assert_equal expected_default_shapes, type_defn_shapes + ] + + type_defn_shapes_a = type_defn_shapes.to_a + assert type_defn_shapes_a.find { |sh| sh == default_shape }, "There's a match for default_shape" + assert type_defn_shapes_a.find { |sh| sh == default_shape_with_connection_type }, "There's a match for default_shape_with_connection_type" + assert type_defn_shapes_a.find { |sh| sh == default_edge_shape }, "There's a match for default_edge_shape" + assert type_defn_shapes_a.find { |sh| sh == default_connection_shape }, "There's a match for default_connection_shape" + assert type_defn_shapes_a.find { |sh| sh == default_mutation_payload_shape }, "There's a match for default_mutation_payload_shape" + + extra_shapes = type_defn_shapes_a - expected_default_shapes + extra_shapes_by_name = {} + extra_shapes.each do |shape| + name = example_shapes_by_name.key(shape) + extra_shapes_by_name[name] = shape + end + assert_equal({}, extra_shapes_by_name, "There aren't any extras shape profiles") end describe "overriding wrap" do @@ -476,4 +489,22 @@ def self.wrap(obj, ctx) assert_equal expected_log, log end end + + describe ".comment" do + it "isn't inherited and can be set to nil" do + obj1 = Class.new(GraphQL::Schema::Object) do + graphql_name "Obj1" + comment "TODO: fix this" + end + + obj2 = Class.new(obj1) do + graphql_name("Obj2") + end + + assert_equal "TODO: fix this", obj1.comment + assert_nil obj2.comment + obj1.comment(nil) + assert_nil obj1.comment + end + end end