Commit 3bd11feb authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'ajk-gitlab-docs-deprecation-use-schema-types' into 'master'

Use Schema types when generating docs for GraphQL deprecations

See merge request gitlab-org/gitlab!56864
parents ee0b4e88 ffdd9a0f
...@@ -82,11 +82,16 @@ module Types ...@@ -82,11 +82,16 @@ module Types
argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue.' argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue.'
end end
field :instance_statistics_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type, field :instance_statistics_measurements,
type: Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
null: true, null: true,
description: 'Get statistics on the instance.', description: 'Get statistics on the instance.',
deprecated: { reason: 'This field was renamed. Use the `usageTrendsMeasurements` field instead', milestone: '13.10' }, resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver,
resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver deprecated: {
reason: :renamed,
replacement: 'Query.usageTrendsMeasurements',
milestone: '13.10'
}
field :usage_trends_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type, field :usage_trends_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
null: true, null: true,
......
...@@ -7,10 +7,34 @@ module Types ...@@ -7,10 +7,34 @@ module Types
# Deprecated, as we prefer uppercase enums # Deprecated, as we prefer uppercase enums
# https://gitlab.com/groups/gitlab-org/-/epics/1838 # https://gitlab.com/groups/gitlab-org/-/epics/1838
value 'updated_desc', 'Updated at descending order.', value: :updated_desc, deprecated: { reason: 'Use UPDATED_DESC', milestone: '13.5' } value 'updated_desc', 'Updated at descending order.',
value 'updated_asc', 'Updated at ascending order.', value: :updated_asc, deprecated: { reason: 'Use UPDATED_ASC', milestone: '13.5' } value: :updated_desc,
value 'created_desc', 'Created at descending order.', value: :created_desc, deprecated: { reason: 'Use CREATED_DESC', milestone: '13.5' } deprecated: {
value 'created_asc', 'Created at ascending order.', value: :created_asc, deprecated: { reason: 'Use CREATED_ASC', milestone: '13.5' } reason: :renamed,
replacement: 'UPDATED_DESC',
milestone: '13.5'
}
value 'updated_asc', 'Updated at ascending order.',
value: :updated_asc,
deprecated: {
reason: :renamed,
replacement: 'UPDATED_ASC',
milestone: '13.5'
}
value 'created_desc', 'Created at descending order.',
value: :created_desc,
deprecated: {
reason: :renamed,
replacement: 'CREATED_DESC',
milestone: '13.5'
}
value 'created_asc', 'Created at ascending order.',
value: :created_asc,
deprecated: {
reason: :renamed,
replacement: 'CREATED_ASC',
milestone: '13.5'
}
value 'UPDATED_DESC', 'Updated at descending order.', value: :updated_desc value 'UPDATED_DESC', 'Updated at descending order.', value: :updated_desc
value 'UPDATED_ASC', 'Updated at ascending order.', value: :updated_asc value 'UPDATED_ASC', 'Updated at ascending order.', value: :updated_asc
......
...@@ -24,7 +24,7 @@ module Types ...@@ -24,7 +24,7 @@ module Types
description: 'State of the user.' description: 'State of the user.'
field :email, GraphQL::STRING_TYPE, null: true, field :email, GraphQL::STRING_TYPE, null: true,
description: 'User email.', method: :public_email, description: 'User email.', method: :public_email,
deprecated: { reason: 'Use public_email', milestone: '13.7' } deprecated: { reason: :renamed, replacement: 'User.publicEmail', milestone: '13.7' }
field :public_email, GraphQL::STRING_TYPE, null: true, field :public_email, GraphQL::STRING_TYPE, null: true,
description: "User's public email." description: "User's public email."
field :avatar_url, GraphQL::STRING_TYPE, null: true, field :avatar_url, GraphQL::STRING_TYPE, null: true,
......
---
title: Change the way deprecation information is presented in GraphQL documentation
merge_request: 56864
author:
type: changed
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -40,7 +40,7 @@ module EE ...@@ -40,7 +40,7 @@ module EE
null: true, null: true,
description: "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard.", description: "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard.",
resolver: ::Resolvers::VulnerabilitiesHistoryResolver, resolver: ::Resolvers::VulnerabilitiesHistoryResolver,
deprecated: { reason: 'Use `vulnerabilitiesCountByDay`', milestone: '13.3' } deprecated: { reason: :discouraged, replacement: 'Query.vulnerabilitiesCountByDay', milestone: '13.3' }
field :geo_node, ::Types::Geo::GeoNodeType, field :geo_node, ::Types::Geo::GeoNodeType,
null: true, null: true,
......
...@@ -27,7 +27,10 @@ module Gitlab ...@@ -27,7 +27,10 @@ module Gitlab
MD MD
end end
def render_name_and_description(object, level = 3) # Template methods:
# Methods that return chunks of Markdown for insertion into the document
def render_name_and_description(object, owner: nil, level: 3)
content = [] content = []
content << "#{'#' * level} `#{object[:name]}`" content << "#{'#' * level} `#{object[:name]}`"
...@@ -35,10 +38,22 @@ module Gitlab ...@@ -35,10 +38,22 @@ module Gitlab
if object[:description].present? if object[:description].present?
desc = object[:description].strip desc = object[:description].strip
desc += '.' unless desc.ends_with?('.') desc += '.' unless desc.ends_with?('.')
end
if object[:is_deprecated]
owner = Array.wrap(owner)
deprecation = schema_deprecation(owner, object[:name])
content << (deprecation&.original_description || desc)
content << render_deprecation(object, owner, :block)
else
content << desc content << desc
end end
content.join("\n\n") content.compact.join("\n\n")
end
def render_return_type(query)
"Returns #{render_field_type(query[:type])}.\n"
end end
def sorted_by_name(objects) def sorted_by_name(objects)
...@@ -47,39 +62,25 @@ module Gitlab ...@@ -47,39 +62,25 @@ module Gitlab
objects.sort_by { |o| o[:name] } objects.sort_by { |o| o[:name] }
end end
def render_field(field) def render_field(field, owner)
row(render_name(field), render_field_type(field[:type]), render_description(field)) render_row(
render_name(field, owner),
render_field_type(field[:type]),
render_description(field, owner, :inline)
)
end end
def render_enum_value(value) def render_enum_value(enum, value)
row(render_name(value), render_description(value)) render_row(render_name(value, enum[:name]), render_description(value, enum[:name], :inline))
end end
def row(*values) def render_union_member(member)
"| #{values.join(' | ')} |" "- [`#{member}`](##{member.downcase})"
end end
def render_name(object) # QUERIES:
rendered_name = "`#{object[:name]}`"
rendered_name += ' **{warning-solid}**' if object[:is_deprecated]
rendered_name
end
# Returns the object description. If the object has been deprecated, # Methods that return parts of the schema, or related information:
# the deprecation reason will be returned in place of the description.
def render_description(object)
return object[:description] unless object[:is_deprecated]
"**Deprecated:** #{object[:deprecation_reason]}"
end
def render_field_type(type)
"[`#{type[:info]}`](##{type[:name].downcase})"
end
def render_return_type(query)
"Returns #{render_field_type(query[:type])}.\n"
end
# We are ignoring connections and built in types for now, # We are ignoring connections and built in types for now,
# they should be added when queries are generated. # they should be added when queries are generated.
...@@ -103,6 +104,83 @@ module Gitlab ...@@ -103,6 +104,83 @@ module Gitlab
!enum_type[:name].in?(%w[__DirectiveLocation __TypeKind]) !enum_type[:name].in?(%w[__DirectiveLocation __TypeKind])
end end
end end
private # DO NOT CALL THESE METHODS IN TEMPLATES
# Template methods
def render_row(*values)
"| #{values.map { |val| val.to_s.squish }.join(' | ')} |"
end
def render_name(object, owner = nil)
rendered_name = "`#{object[:name]}`"
rendered_name += ' **{warning-solid}**' if object[:is_deprecated]
rendered_name
end
# Returns the object description. If the object has been deprecated,
# the deprecation reason will be returned in place of the description.
def render_description(object, owner = nil, context = :block)
owner = Array.wrap(owner)
return render_deprecation(object, owner, context) if object[:is_deprecated]
return if object[:description].blank?
desc = object[:description].strip
desc += '.' unless desc.ends_with?('.')
desc
end
def render_deprecation(object, owner, context)
deprecation = schema_deprecation(owner, object[:name])
return deprecation.markdown(context: context) if deprecation
reason = object[:deprecation_reason] || 'Use of this is deprecated.'
"**Deprecated:** #{reason}"
end
def render_field_type(type)
"[`#{type[:info]}`](##{type[:name].downcase})"
end
# Queries
# returns the deprecation information for a field or argument
# See: Gitlab::Graphql::Deprecation
def schema_deprecation(type_name, field_name)
schema_member(type_name, field_name)&.deprecation
end
# Return a part of the schema.
#
# This queries the Schema by owner and name to find:
#
# - fields (e.g. `schema_member('Query', 'currentUser')`)
# - arguments (e.g. `schema_member(['Query', 'project], 'fullPath')`)
def schema_member(type_name, field_name)
type_name = Array.wrap(type_name)
if type_name.size == 2
arg_name = field_name
type_name, field_name = type_name
else
type_name = type_name.first
arg_name = nil
end
return if type_name.nil? || field_name.nil?
type = schema.types[type_name]
return unless type && type.kind.fields?
field = type.fields[field_name]
return field if arg_name.nil?
args = field.arguments
is_mutation = field.mutation && field.mutation <= ::Mutations::BaseMutation
args = args['input'].type.unwrap.arguments if is_mutation
args[arg_name]
end
end end
end end
end end
......
...@@ -10,17 +10,20 @@ module Gitlab ...@@ -10,17 +10,20 @@ module Gitlab
# It uses graphql-docs helpers and schema parser, more information in https://github.com/gjtorikian/graphql-docs. # It uses graphql-docs helpers and schema parser, more information in https://github.com/gjtorikian/graphql-docs.
# #
# Arguments: # Arguments:
# schema - the GraphQL schema definition. For GitLab should be: GitlabSchema.graphql_definition # schema - the GraphQL schema definition. For GitLab should be: GitlabSchema
# output_dir: The folder where the markdown files will be saved # output_dir: The folder where the markdown files will be saved
# template: The path of the haml template to be parsed # template: The path of the haml template to be parsed
class Renderer class Renderer
include Gitlab::Graphql::Docs::Helper include Gitlab::Graphql::Docs::Helper
attr_reader :schema
def initialize(schema, output_dir:, template:) def initialize(schema, output_dir:, template:)
@output_dir = output_dir @output_dir = output_dir
@template = template @template = template
@layout = Haml::Engine.new(File.read(template)) @layout = Haml::Engine.new(File.read(template))
@parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse @parsed_schema = GraphQLDocs::Parser.new(schema.graphql_definition, {}).parse
@schema = schema
end end
def contents def contents
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
\ \
- sorted_by_name(queries).each do |query| - sorted_by_name(queries).each do |query|
= render_name_and_description(query) = render_name_and_description(query, owner: 'Query')
\ \
= render_return_type(query) = render_return_type(query)
- unless query[:arguments].empty? - unless query[:arguments].empty?
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
~ "| Name | Type | Description |" ~ "| Name | Type | Description |"
~ "| ---- | ---- | ----------- |" ~ "| ---- | ---- | ----------- |"
- sorted_by_name(query[:arguments]).each do |argument| - sorted_by_name(query[:arguments]).each do |argument|
= render_field(argument) = render_field(argument, query[:type][:name])
\ \
:plain :plain
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
~ "| Field | Type | Description |" ~ "| Field | Type | Description |"
~ "| ----- | ---- | ----------- |" ~ "| ----- | ---- | ----------- |"
- sorted_by_name(type[:fields]).each do |field| - sorted_by_name(type[:fields]).each do |field|
= render_field(field) = render_field(field, type[:name])
\ \
:plain :plain
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
~ "| Value | Description |" ~ "| Value | Description |"
~ "| ----- | ----------- |" ~ "| ----- | ----------- |"
- sorted_by_name(enum[:values]).each do |value| - sorted_by_name(enum[:values]).each do |value|
= render_enum_value(value) = render_enum_value(enum, value)
\ \
:plain :plain
...@@ -121,12 +121,12 @@ ...@@ -121,12 +121,12 @@
\ \
- graphql_union_types.each do |type| - graphql_union_types.each do |type|
= render_name_and_description(type, 4) = render_name_and_description(type, level: 4)
\ \
One of: One of:
\ \
- type[:possible_types].each do |type_name| - type[:possible_types].each do |member|
~ "- [`#{type_name}`](##{type_name.downcase})" = render_union_member(member)
\ \
:plain :plain
...@@ -134,7 +134,7 @@ ...@@ -134,7 +134,7 @@
\ \
- graphql_interface_types.each do |type| - graphql_interface_types.each do |type|
= render_name_and_description(type, 4) = render_name_and_description(type, level: 4)
\ \
Implementations: Implementations:
\ \
...@@ -144,5 +144,5 @@ ...@@ -144,5 +144,5 @@
~ "| Field | Type | Description |" ~ "| Field | Type | Description |"
~ "| ----- | ---- | ----------- |" ~ "| ----- | ---- | ----------- |"
- sorted_by_name(type[:fields] + type[:connections]).each do |field| - sorted_by_name(type[:fields] + type[:connections]).each do |field|
= render_field(field) = render_field(field, type[:name])
\ \
...@@ -110,7 +110,7 @@ namespace :gitlab do ...@@ -110,7 +110,7 @@ namespace :gitlab do
desc 'GitLab | GraphQL | Generate GraphQL docs' desc 'GitLab | GraphQL | Generate GraphQL docs'
task compile_docs: [:environment, :enable_feature_flags] do task compile_docs: [:environment, :enable_feature_flags] do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options) renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema, render_options)
renderer.write renderer.write
...@@ -119,7 +119,7 @@ namespace :gitlab do ...@@ -119,7 +119,7 @@ namespace :gitlab do
desc 'GitLab | GraphQL | Check if GraphQL docs are up to date' desc 'GitLab | GraphQL | Check if GraphQL docs are up to date'
task check_docs: [:environment, :enable_feature_flags] do task check_docs: [:environment, :enable_feature_flags] do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options) renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema, render_options)
doc = File.read(Rails.root.join(OUTPUT_DIR, 'index.md')) doc = File.read(Rails.root.join(OUTPUT_DIR, 'index.md'))
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
RSpec.describe Gitlab::Graphql::Docs::Renderer do RSpec.describe Gitlab::Graphql::Docs::Renderer do
describe '#contents' do describe '#contents' do
# Returns a Schema that uses the given `type` let(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') }
def mock_schema(type, field_description)
query_type = Class.new(Types::BaseObject) do
graphql_name 'Query'
field :foo, type, null: true do let(:query_type) do
description field_description Class.new(Types::BaseObject) { graphql_name 'Query' }.tap do |t|
# this keeps type and field_description in scope.
t.field :foo, type, null: true, description: field_description do
argument :id, GraphQL::ID_TYPE, required: false, description: 'ID of the object.' argument :id, GraphQL::ID_TYPE, required: false, description: 'ID of the object.'
end end
end end
end
GraphQL::Schema.define( let(:mock_schema) do
query: query_type, Class.new(GraphQL::Schema) do
resolve_type: ->(obj, ctx) { raise 'Not a real schema' } def resolve_type(obj, ctx)
) raise 'Not a real schema'
end
end
end end
let_it_be(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') }
let(:field_description) { 'List of objects.' } let(:field_description) { 'List of objects.' }
subject(:contents) do subject(:contents) do
mock_schema.query(query_type)
described_class.new( described_class.new(
mock_schema(type, field_description).graphql_definition, mock_schema,
output_dir: nil, output_dir: nil,
template: template template: template
).contents ).contents
...@@ -136,6 +139,22 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do ...@@ -136,6 +139,22 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
null: false, null: false,
deprecated: { reason: 'This is deprecated', milestone: '1.10' }, deprecated: { reason: 'This is deprecated', milestone: '1.10' },
description: 'A description.' description: 'A description.'
field :foo_with_args,
type: GraphQL::STRING_TYPE,
null: false,
deprecated: { reason: 'Do not use', milestone: '1.10' },
description: 'A description.' do
argument :fooity, ::GraphQL::INT_TYPE, required: false, description: 'X'
end
field :bar,
type: GraphQL::STRING_TYPE,
null: false,
description: 'A description.',
deprecated: {
reason: :renamed,
milestone: '1.10',
replacement: 'Query.boom'
}
end end
end end
...@@ -145,7 +164,40 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do ...@@ -145,7 +164,40 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `foo` **{warning-solid}** | [`String!`](#string) | **Deprecated:** This is deprecated. Deprecated in 1.10. | | `bar` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This was renamed. Use: `Query.boom`. |
| `foo` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This is deprecated. |
| `fooWithArgs` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. Do not use. |
DOC
is_expected.to include(expectation)
end
end
context 'when a Query.field is deprecated' do
let(:type) { ::GraphQL::INT_TYPE }
before do
query_type.field(
name: :bar,
type: type,
null: true,
description: 'A bar',
deprecated: { reason: :renamed, milestone: '10.11', replacement: 'Query.foo' }
)
end
it 'includes the deprecation' do
expectation = <<~DOC
### `bar`
A bar.
WARNING:
**Deprecated** in 10.11.
This was renamed.
Use: `Query.foo`.
Returns [`Int`](#int).
DOC DOC
is_expected.to include(expectation) is_expected.to include(expectation)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment