Skip to content
Snippets Groups Projects
Commit ea3272d4 authored by Etienne Baqué's avatar Etienne Baqué
Browse files

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

Linked GraphQL docs output

See merge request gitlab-org/gitlab!55901
parents 587851e5 6512cd4d
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -19,7 +19,14 @@ def ==(other)
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]
Loading
Loading
@@ -46,38 +53,40 @@ def self.[](model_class)
 
@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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
---
title: Link fields to types in GraphQL reference documentation
merge_request: 55901
author:
type: changed
This diff is collapsed.
Loading
Loading
@@ -27,16 +27,18 @@ def auto_generated_comment
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)
Loading
Loading
@@ -46,18 +48,15 @@ def sorted_by_name(objects)
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)
Loading
Loading
@@ -74,27 +73,19 @@ def render_description(object)
"**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|
Loading
Loading
@@ -109,7 +100,7 @@ def queries
# 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
Loading
Loading
Loading
Loading
@@ -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 |"
Loading
Loading
@@ -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|
Loading
Loading
@@ -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)
\
Loading
Loading
@@ -15,10 +15,13 @@ def mock_schema(type, field_description)
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
Loading
Loading
@@ -29,7 +32,23 @@ def mock_schema(type, field_description)
).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'
Loading
Loading
@@ -39,29 +58,33 @@ def mock_schema(type, field_description)
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
 
Loading
Loading
@@ -79,7 +102,7 @@ def mock_schema(type, field_description)
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'
Loading
Loading
@@ -89,49 +112,56 @@ def mock_schema(type, field_description)
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
Loading
Loading
@@ -141,7 +171,7 @@ def mock_schema(type, field_description)
end
end
 
specify do
it 'includes the description of the Enumeration' do
expectation = <<~DOC
### `MyEnum`
 
Loading
Loading
@@ -154,5 +184,129 @@ def mock_schema(type, field_description)
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment