Commit ea3272d4 authored by Etienne Baqué's avatar Etienne Baqué

Merge branch 'ajk-linked-graphql-docs' into 'master'

Linked GraphQL docs output

See merge request gitlab-org/gitlab!55901
parents 587851e5 6512cd4d
......@@ -19,7 +19,14 @@ end
module Types
class GlobalIDType < BaseScalar
graphql_name 'GlobalID'
description 'A global identifier'
description <<~DESC
A global identifier.
A global identifier represents an object uniquely across the application.
An example of such an identifier is "gid://gitlab/User/1".
Global identifiers are encoded as strings.
DESC
# @param value [GID]
# @return [String]
......@@ -46,38 +53,40 @@ module Types
@id_types[model_class] ||= Class.new(self) do
graphql_name "#{model_class.name.gsub(/::/, '')}ID"
description "Identifier of #{model_class.name}."
description <<~MD
A `#{graphql_name}` is a global ID. It is encoded as a string.
self.define_singleton_method(:to_s) do
An example `#{graphql_name}` is: `"#{::Gitlab::GlobalId.build(model_name: model_class.name, id: 1)}"`.
MD
define_singleton_method(:to_s) do
graphql_name
end
self.define_singleton_method(:inspect) do
define_singleton_method(:inspect) do
graphql_name
end
self.define_singleton_method(:coerce_result) do |gid, ctx|
define_singleton_method(:coerce_result) do |gid, ctx|
global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_class.name)
if suitable?(global_id)
global_id.to_s
else
raise GraphQL::CoercionError, "Expected a #{model_class.name} ID, got #{global_id}"
end
next global_id.to_s if suitable?(global_id)
raise GraphQL::CoercionError, "Expected a #{model_class.name} ID, got #{global_id}"
end
self.define_singleton_method(:suitable?) do |gid|
define_singleton_method(:suitable?) do |gid|
next false if gid.nil?
gid.model_name.safe_constantize.present? &&
gid.model_class.ancestors.include?(model_class)
end
self.define_singleton_method(:coerce_input) do |string, ctx|
define_singleton_method(:coerce_input) do |string, ctx|
gid = super(string, ctx)
raise GraphQL::CoercionError, "#{string.inspect} does not represent an instance of #{model_class.name}" unless suitable?(gid)
next gid if suitable?(gid)
gid
raise GraphQL::CoercionError, "#{string.inspect} does not represent an instance of #{model_class.name}"
end
end
end
......
......@@ -3,7 +3,13 @@
module Types
class TimeType < BaseScalar
graphql_name 'Time'
description 'Time represented in ISO 8601'
description <<~DESC
Time represented in ISO 8601.
For example: "2021-03-09T14:58:50+00:00".
See `https://www.iso.org/iso-8601-date-and-time-format.html`.
DESC
def self.coerce_input(value, ctx)
Time.parse(value)
......
---
title: Link fields to types in GraphQL reference documentation
merge_request: 55901
author:
type: changed
This diff is collapsed.
......@@ -27,16 +27,18 @@ module Gitlab
MD
end
def render_name_and_description(object)
content = "### `#{object[:name]}`\n"
def render_name_and_description(object, level = 3)
content = []
content << "#{'#' * level} `#{object[:name]}`"
if object[:description].present?
content += "\n#{object[:description]}"
content += '.' unless object[:description].ends_with?('.')
content += "\n"
desc = object[:description].strip
desc += '.' unless desc.ends_with?('.')
content << desc
end
content
content.join("\n\n")
end
def sorted_by_name(objects)
......@@ -46,18 +48,15 @@ module Gitlab
end
def render_field(field)
'| %s | %s | %s |' % [
render_name(field),
render_field_type(field[:type][:info]),
render_description(field)
]
row(render_name(field), render_field_type(field[:type]), render_description(field))
end
def render_enum_value(value)
'| %s | %s |' % [
render_name(value),
render_description(value)
]
row(render_name(value), render_description(value))
end
def row(*values)
"| #{values.join(' | ')} |"
end
def render_name(object)
......@@ -74,27 +73,19 @@ module Gitlab
"**Deprecated:** #{object[:deprecation_reason]}"
end
# Some fields types are arrays of other types and are displayed
# on docs wrapped in square brackets, for example: [String!].
# This makes GitLab docs renderer thinks they are links so here
# we change them to be rendered as: String! => Array.
def render_field_type(type)
array_type = type[/\[(.+)\]/, 1]
"[`#{type[:info]}`](##{type[:name].downcase})"
end
if array_type
"#{array_type} => Array"
else
type
end
def render_return_type(query)
"Returns #{render_field_type(query[:type])}.\n"
end
# We are ignoring connections and built in types for now,
# they should be added when queries are generated.
def objects
object_types = graphql_object_types.select do |object_type|
!object_type[:name]["Connection"] &&
!object_type[:name]["Edge"] &&
!object_type[:name]["__"]
!object_type[:name]["__"]
end
object_types.each do |type|
......@@ -109,7 +100,7 @@ module Gitlab
# We ignore the built-in enum types.
def enums
graphql_enum_types.select do |enum_type|
!enum_type[:name].in?(%w(__DirectiveLocation __TypeKind))
!enum_type[:name].in?(%w[__DirectiveLocation __TypeKind])
end
end
end
......
......@@ -28,6 +28,8 @@
- sorted_by_name(queries).each do |query|
= render_name_and_description(query)
\
= render_return_type(query)
- unless query[:arguments].empty?
~ "#### Arguments\n"
~ "| Name | Type | Description |"
......@@ -52,6 +54,7 @@
- objects.each do |type|
- unless type[:fields].empty?
= render_name_and_description(type)
\
~ "| Field | Type | Description |"
~ "| ----- | ---- | ----------- |"
- sorted_by_name(type[:fields]).each do |field|
......@@ -72,8 +75,74 @@
- enums.each do |enum|
- unless enum[:values].empty?
= render_name_and_description(enum)
\
~ "| Value | Description |"
~ "| ----- | ----------- |"
- sorted_by_name(enum[:values]).each do |value|
= render_enum_value(value)
\
:plain
## Scalar types
Scalar values are atomic values, and do not have fields of their own.
Basic scalars include strings, boolean values, and numbers. This schema also
defines various custom scalar values, such as types for times and dates.
This schema includes custom scalar types for identifiers, with a specific type for
each kind of object.
For more information, read about [Scalar Types](https://graphql.org/learn/schema/#scalar-types) on `graphql.org`.
\
- graphql_scalar_types.each do |type|
= render_name_and_description(type)
\
:plain
## Abstract types
Abstract types (unions and interfaces) are ways the schema can represent
values that may be one of several concrete types.
- A [`Union`](https://graphql.org/learn/schema/#union-types) is a set of possible types.
The types might not have any fields in common.
- An [`Interface`](https://graphql.org/learn/schema/#interfaces) is a defined set of fields.
Types may `implement` an interface, which
guarantees that they have all the fields in the set. A type may implement more than
one interface.
See the [GraphQL documentation](https://graphql.org/learn/) for more information on using
abstract types.
\
:plain
### Unions
\
- graphql_union_types.each do |type|
= render_name_and_description(type, 4)
\
One of:
\
- type[:possible_types].each do |type_name|
~ "- [`#{type_name}`](##{type_name.downcase})"
\
:plain
### Interfaces
\
- graphql_interface_types.each do |type|
= render_name_and_description(type, 4)
\
Implementations:
\
- type[:implemented_by].each do |type_name|
~ "- [`#{type_name}`](##{type_name.downcase})"
\
~ "| Field | Type | Description |"
~ "| ----- | ---- | ----------- |"
- sorted_by_name(type[:fields] + type[:connections]).each do |field|
= render_field(field)
\
......@@ -15,10 +15,13 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
end
end
GraphQL::Schema.define(query: query_type)
GraphQL::Schema.define(
query: query_type,
resolve_type: ->(obj, ctx) { raise 'Not a real schema' }
)
end
let_it_be(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/', 'default.md.haml') }
let_it_be(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') }
let(:field_description) { 'List of objects.' }
subject(:contents) do
......@@ -29,7 +32,23 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
).contents
end
context 'A type with a field with a [Array] return type' do
describe 'headings' do
let(:type) { ::GraphQL::INT_TYPE }
it 'contains the expected sections' do
expect(contents.lines.map(&:chomp)).to include(
'## `Query` type',
'## Object types',
'## Enumeration types',
'## Scalar types',
'## Abstract types',
'### Unions',
'### Interfaces'
)
end
end
context 'when a field has a list type' do
let(:type) do
Class.new(Types::BaseObject) do
graphql_name 'ArrayTest'
......@@ -39,29 +58,33 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
end
specify do
type_name = '[String!]!'
inner_type = 'string'
expectation = <<~DOC
### `ArrayTest`
| Field | Type | Description |
| ----- | ---- | ----------- |
| `foo` | String! => Array | A description. |
| `foo` | [`#{type_name}`](##{inner_type}) | A description. |
DOC
is_expected.to include(expectation)
end
context 'query generation' do
describe 'a top level query field' do
let(:expectation) do
<<~DOC
### `foo`
List of objects.
Returns [`ArrayTest`](#arraytest).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| `id` | ID | ID of the object. |
| `id` | [`ID`](#id) | ID of the object. |
DOC
end
......@@ -79,7 +102,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
end
end
context 'A type with fields defined in reverse alphabetical order' do
describe 'when fields are not defined in alphabetical order' do
let(:type) do
Class.new(Types::BaseObject) do
graphql_name 'OrderingTest'
......@@ -89,49 +112,56 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
end
end
specify do
it 'lists the fields in alphabetical order' do
expectation = <<~DOC
### `OrderingTest`
| Field | Type | Description |
| ----- | ---- | ----------- |
| `bar` | String! | A description of bar field. |
| `foo` | String! | A description of foo field. |
| `bar` | [`String!`](#string) | A description of bar field. |
| `foo` | [`String!`](#string) | A description of foo field. |
DOC
is_expected.to include(expectation)
end
end
context 'A type with a deprecated field' do
context 'when a field is deprecated' do
let(:type) do
Class.new(Types::BaseObject) do
graphql_name 'DeprecatedTest'
field :foo, GraphQL::STRING_TYPE, null: false, deprecated: { reason: 'This is deprecated', milestone: '1.10' }, description: 'A description.'
field :foo,
type: GraphQL::STRING_TYPE,
null: false,
deprecated: { reason: 'This is deprecated', milestone: '1.10' },
description: 'A description.'
end
end
specify do
it 'includes the deprecation' do
expectation = <<~DOC
### `DeprecatedTest`
| Field | Type | Description |
| ----- | ---- | ----------- |
| `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated. Deprecated in 1.10. |
| `foo` **{warning-solid}** | [`String!`](#string) | **Deprecated:** This is deprecated. Deprecated in 1.10. |
DOC
is_expected.to include(expectation)
end
end
context 'A type with an emum field' do
context 'when a field has an Enumeration type' do
let(:type) do
enum_type = Class.new(Types::BaseEnum) do
graphql_name 'MyEnum'
value 'BAZ', description: 'A description of BAZ.'
value 'BAR', description: 'A description of BAR.', deprecated: { reason: 'This is deprecated', milestone: '1.10' }
value 'BAZ',
description: 'A description of BAZ.'
value 'BAR',
description: 'A description of BAR.',
deprecated: { reason: 'This is deprecated', milestone: '1.10' }
end
Class.new(Types::BaseObject) do
......@@ -141,7 +171,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
end
end
specify do
it 'includes the description of the Enumeration' do
expectation = <<~DOC
### `MyEnum`
......@@ -154,5 +184,129 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
is_expected.to include(expectation)
end
end
context 'when a field has a global ID type' do
let(:type) do
Class.new(Types::BaseObject) do
graphql_name 'IDTest'
description 'A test for rendering IDs.'
field :foo, ::Types::GlobalIDType[::User], null: true, description: 'A user foo.'
end
end
it 'includes the field and the description of the ID, so we can link to it' do
type_section = <<~DOC
### `IDTest`
A test for rendering IDs.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `foo` | [`UserID`](#userid) | A user foo. |
DOC
id_section = <<~DOC
### `UserID`
A `UserID` is a global ID. It is encoded as a string.
An example `UserID` is: `"gid://gitlab/User/1"`.
DOC
is_expected.to include(type_section, id_section)
end
end
context 'when there is an interface and a union' do
let(:type) do
user = Class.new(::Types::BaseObject)
user.graphql_name 'User'
user.field :user_field, ::GraphQL::STRING_TYPE, null: true
group = Class.new(::Types::BaseObject)
group.graphql_name 'Group'
group.field :group_field, ::GraphQL::STRING_TYPE, null: true
union = Class.new(::Types::BaseUnion)
union.graphql_name 'UserOrGroup'
union.description 'Either a user or a group.'
union.possible_types user, group
interface = Module.new
interface.include(::Types::BaseInterface)
interface.graphql_name 'Flying'
interface.description 'Something that can fly.'
interface.field :flight_speed, GraphQL::INT_TYPE, null: true, description: 'Speed in mph.'
african_swallow = Class.new(::Types::BaseObject)
african_swallow.graphql_name 'AfricanSwallow'
african_swallow.description 'A swallow from Africa.'
african_swallow.implements interface
interface.orphan_types african_swallow
Class.new(::Types::BaseObject) do
graphql_name 'AbstactTypeTest'
description 'A test for abstract types.'
field :foo, union, null: true, description: 'The foo.'
field :flying, interface, null: true, description: 'A flying thing.'
end
end
it 'lists the fields correctly, and includes descriptions of all the types' do
type_section = <<~DOC
### `AbstactTypeTest`
A test for abstract types.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `flying` | [`Flying`](#flying) | A flying thing. |
| `foo` | [`UserOrGroup`](#userorgroup) | The foo. |
DOC
union_section = <<~DOC
#### `UserOrGroup`
Either a user or a group.
One of:
- [`Group`](#group)
- [`User`](#user)
DOC
interface_section = <<~DOC
#### `Flying`
Something that can fly.
Implementations:
- [`AfricanSwallow`](#africanswallow)
| Field | Type | Description |
| ----- | ---- | ----------- |
| `flightSpeed` | [`Int`](#int) | Speed in mph. |
DOC
implementation_section = <<~DOC
### `AfricanSwallow`
A swallow from Africa.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `flightSpeed` | [`Int`](#int) | Speed in mph. |
DOC
is_expected.to include(
type_section,
union_section,
interface_section,
implementation_section
)
end
end
end
end
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