diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index 7771a35187..4b3fbaa154 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -8,6 +8,10 @@ schema { mutation: MutationType } +""" +This is a description +of the `Foo` type. +""" type Foo implements Bar { one: Type two(argument: InputType!): Type diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 14266e5bbc..a59f9a94a5 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -94,6 +94,55 @@ type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); + it('parses type with description string', () => { + const doc = parse(` +"Description" +type Hello { + world: String +}`); + expect(doc).to.containSubset({ + kind: 'Document', + definitions: [ + { + kind: 'ObjectTypeDefinition', + name: nameNode('Hello', { start: 20, end: 25 }), + description: { + kind: 'StringValue', + value: 'Description', + loc: { start: 1, end: 14 }, + } + } + ], + loc: { start: 0, end: 45 }, + }); + }); + + it('parses type with description multi-line string', () => { + const doc = parse(` +""" +Description +""" +# Even with comments between them +type Hello { + world: String +}`); + expect(doc).to.containSubset({ + kind: 'Document', + definitions: [ + { + kind: 'ObjectTypeDefinition', + name: nameNode('Hello', { start: 60, end: 65 }), + description: { + kind: 'StringValue', + value: 'Description', + loc: { start: 1, end: 20 }, + } + } + ], + loc: { start: 0, end: 85 }, + }); + }); + it('Simple extension', () => { const body = ` extend type Hello { @@ -128,6 +177,15 @@ extend type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); + it('Extension do not include descriptions', () => { + expect(() => parse(` + "Description" + extend type Hello { + world: String + } + `)).to.throw('Syntax Error GraphQL request (2:7)'); + }); + it('Simple non-null type', () => { const body = ` type Hello { diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index a8748b402e..b2f11d3822 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -54,6 +54,10 @@ describe('Printer', () => { mutation: MutationType } +""" +This is a description +of the \`Foo\` type. +""" type Foo implements Bar { one: Type two(argument: InputType!): Type diff --git a/src/language/ast.js b/src/language/ast.js index 64b4a5a6db..44b43892de 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -396,6 +396,7 @@ export type TypeDefinitionNode = export type ScalarTypeDefinitionNode = { kind: 'ScalarTypeDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; directives?: ?Array; }; @@ -403,6 +404,7 @@ export type ScalarTypeDefinitionNode = { export type ObjectTypeDefinitionNode = { kind: 'ObjectTypeDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; interfaces?: ?Array; directives?: ?Array; @@ -412,6 +414,7 @@ export type ObjectTypeDefinitionNode = { export type FieldDefinitionNode = { kind: 'FieldDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; arguments: Array; type: TypeNode; @@ -421,6 +424,7 @@ export type FieldDefinitionNode = { export type InputValueDefinitionNode = { kind: 'InputValueDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; type: TypeNode; defaultValue?: ?ValueNode; @@ -430,6 +434,7 @@ export type InputValueDefinitionNode = { export type InterfaceTypeDefinitionNode = { kind: 'InterfaceTypeDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; directives?: ?Array; fields: Array; @@ -438,6 +443,7 @@ export type InterfaceTypeDefinitionNode = { export type UnionTypeDefinitionNode = { kind: 'UnionTypeDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; directives?: ?Array; types: Array; @@ -446,6 +452,7 @@ export type UnionTypeDefinitionNode = { export type EnumTypeDefinitionNode = { kind: 'EnumTypeDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; directives?: ?Array; values: Array; @@ -454,6 +461,7 @@ export type EnumTypeDefinitionNode = { export type EnumValueDefinitionNode = { kind: 'EnumValueDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; directives?: ?Array; }; @@ -461,6 +469,7 @@ export type EnumValueDefinitionNode = { export type InputObjectTypeDefinitionNode = { kind: 'InputObjectTypeDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; directives?: ?Array; fields: Array; @@ -475,6 +484,7 @@ export type TypeExtensionDefinitionNode = { export type DirectiveDefinitionNode = { kind: 'DirectiveDefinition'; loc?: Location; + description?: ?StringValueNode; name: NameNode; arguments?: ?Array; locations: Array; diff --git a/src/language/lexer.js b/src/language/lexer.js index 2199c09e94..9bb9fffa18 100644 --- a/src/language/lexer.js +++ b/src/language/lexer.js @@ -32,18 +32,24 @@ export function createLexer( token: startOfFileToken, line: 1, lineStart: 0, - advance: advanceLexer + advance: advanceLexer, + lookahead }; return lexer; } function advanceLexer() { - let token = this.lastToken = this.token; + this.lastToken = this.token; + const token = this.token = this.lookahead(); + return token; +} + +function lookahead() { + let token = this.token; if (token.kind !== EOF) { do { - token = token.next = readToken(this, token); + token = token.next || (token.next = readToken(this, token)); } while (token.kind === COMMENT); - this.token = token; } return token; } @@ -79,6 +85,12 @@ export type Lexer = { * Advances the token stream to the next non-ignored token. */ advance(): Token; + + /** + * Looks ahead and returns the next non-ignored token, but does not change + * the Lexer's state. + */ + lookahead(): Token; }; // Each kind of token. diff --git a/src/language/parser.js b/src/language/parser.js index b8c8cc9094..88f56804a5 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -38,6 +38,7 @@ import type { FragmentDefinitionNode, ValueNode, + StringValueNode, ListValueNode, ObjectValueNode, ObjectFieldNode, @@ -243,7 +244,7 @@ function parseDefinition(lexer: Lexer<*>): DefinitionNode { case 'fragment': return parseFragmentDefinition(lexer); - // Note: the Type System IDL is an experimental non-spec addition. + // Note: The schema definition language is an experimental addition. case 'schema': case 'scalar': case 'type': @@ -256,6 +257,11 @@ function parseDefinition(lexer: Lexer<*>): DefinitionNode { } } + // Note: The schema definition language is an experimental addition. + if (peekDescription(lexer)) { + return parseTypeSystemDefinition(lexer); + } + throw unexpected(lexer); } @@ -541,13 +547,7 @@ function parseValueLiteral(lexer: Lexer<*>, isConst: boolean): ValueNode { }; case TokenKind.STRING: case TokenKind.BLOCK_STRING: - lexer.advance(); - return { - kind: (STRING: 'StringValue'), - value: ((token.value: any): string), - block: token.kind === TokenKind.BLOCK_STRING, - loc: loc(lexer, token) - }; + return parseStringLiteral(lexer); case TokenKind.NAME: if (token.value === 'true' || token.value === 'false') { lexer.advance(); @@ -578,6 +578,17 @@ function parseValueLiteral(lexer: Lexer<*>, isConst: boolean): ValueNode { throw unexpected(lexer); } +function parseStringLiteral(lexer: Lexer<*>): StringValueNode { + const token = lexer.token; + lexer.advance(); + return { + kind: (STRING: 'StringValue'), + value: ((token.value: any): string), + block: token.kind === TokenKind.BLOCK_STRING, + loc: loc(lexer, token) + }; +} + export function parseConstValue(lexer: Lexer<*>): ValueNode { return parseValueLiteral(lexer, true); } @@ -726,8 +737,14 @@ export function parseNamedType(lexer: Lexer<*>): NamedTypeNode { * - InputObjectTypeDefinition */ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode { - if (peek(lexer, TokenKind.NAME)) { - switch (lexer.token.value) { + // Many definitions begin with a description and require a lookahead. + const keywordToken = + peekDescription(lexer) ? + lexer.lookahead() : + lexer.token; + + if (keywordToken.kind === TokenKind.NAME) { + switch (keywordToken.value) { case 'schema': return parseSchemaDefinition(lexer); case 'scalar': return parseScalarTypeDefinition(lexer); case 'type': return parseObjectTypeDefinition(lexer); @@ -743,10 +760,21 @@ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode { throw unexpected(lexer); } +function peekDescription(lexer: Lexer<*>): boolean { + return peek(lexer, TokenKind.STRING) || peek(lexer, TokenKind.BLOCK_STRING); +} + +/** + * Description : StringValue + */ +function parseDescription(lexer: Lexer<*>): void | StringValueNode { + if (peekDescription(lexer)) { + return parseStringLiteral(lexer); + } +} + /** * SchemaDefinition : schema Directives? { OperationTypeDefinition+ } - * - * OperationTypeDefinition : OperationType : NamedType */ function parseSchemaDefinition(lexer: Lexer<*>): SchemaDefinitionNode { const start = lexer.token; @@ -766,6 +794,9 @@ function parseSchemaDefinition(lexer: Lexer<*>): SchemaDefinitionNode { }; } +/** + * OperationTypeDefinition : OperationType : NamedType + */ function parseOperationTypeDefinition( lexer: Lexer<*> ): OperationTypeDefinitionNode { @@ -782,15 +813,17 @@ function parseOperationTypeDefinition( } /** - * ScalarTypeDefinition : scalar Name Directives? + * ScalarTypeDefinition : Description? scalar Name Directives? */ function parseScalarTypeDefinition(lexer: Lexer<*>): ScalarTypeDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'scalar'); const name = parseName(lexer); const directives = parseDirectives(lexer); return { kind: SCALAR_TYPE_DEFINITION, + description, name, directives, loc: loc(lexer, start), @@ -799,10 +832,11 @@ function parseScalarTypeDefinition(lexer: Lexer<*>): ScalarTypeDefinitionNode { /** * ObjectTypeDefinition : - * - type Name ImplementsInterfaces? Directives? { FieldDefinition+ } + * Description? type Name ImplementsInterfaces? Directives? { FieldDefinition+ } */ function parseObjectTypeDefinition(lexer: Lexer<*>): ObjectTypeDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'type'); const name = parseName(lexer); const interfaces = parseImplementsInterfaces(lexer); @@ -815,6 +849,7 @@ function parseObjectTypeDefinition(lexer: Lexer<*>): ObjectTypeDefinitionNode { ); return { kind: OBJECT_TYPE_DEFINITION, + description, name, interfaces, directives, @@ -838,10 +873,11 @@ function parseImplementsInterfaces(lexer: Lexer<*>): Array { } /** - * FieldDefinition : Name ArgumentsDefinition? : Type Directives? + * FieldDefinition : Description? Name ArgumentsDefinition? : Type Directives? */ function parseFieldDefinition(lexer: Lexer<*>): FieldDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); const name = parseName(lexer); const args = parseArgumentDefs(lexer); expect(lexer, TokenKind.COLON); @@ -849,6 +885,7 @@ function parseFieldDefinition(lexer: Lexer<*>): FieldDefinitionNode { const directives = parseDirectives(lexer); return { kind: FIELD_DEFINITION, + description, name, arguments: args, type, @@ -868,10 +905,11 @@ function parseArgumentDefs(lexer: Lexer<*>): Array { } /** - * InputValueDefinition : Name : Type DefaultValue? Directives? + * InputValueDefinition : Description? Name : Type DefaultValue? Directives? */ function parseInputValueDef(lexer: Lexer<*>): InputValueDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); const name = parseName(lexer); expect(lexer, TokenKind.COLON); const type = parseTypeReference(lexer); @@ -882,6 +920,7 @@ function parseInputValueDef(lexer: Lexer<*>): InputValueDefinitionNode { const directives = parseDirectives(lexer); return { kind: INPUT_VALUE_DEFINITION, + description, name, type, defaultValue, @@ -891,12 +930,14 @@ function parseInputValueDef(lexer: Lexer<*>): InputValueDefinitionNode { } /** - * InterfaceTypeDefinition : interface Name Directives? { FieldDefinition+ } + * InterfaceTypeDefinition : + * - Description? interface Name Directives? { FieldDefinition+ } */ function parseInterfaceTypeDefinition( lexer: Lexer<*> ): InterfaceTypeDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'interface'); const name = parseName(lexer); const directives = parseDirectives(lexer); @@ -908,6 +949,7 @@ function parseInterfaceTypeDefinition( ); return { kind: INTERFACE_TYPE_DEFINITION, + description, name, directives, fields, @@ -916,10 +958,11 @@ function parseInterfaceTypeDefinition( } /** - * UnionTypeDefinition : union Name Directives? = UnionMembers + * UnionTypeDefinition : Description? union Name Directives? = UnionMembers */ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'union'); const name = parseName(lexer); const directives = parseDirectives(lexer); @@ -927,6 +970,7 @@ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode { const types = parseUnionMembers(lexer); return { kind: UNION_TYPE_DEFINITION, + description, name, directives, types, @@ -950,10 +994,12 @@ function parseUnionMembers(lexer: Lexer<*>): Array { } /** - * EnumTypeDefinition : enum Name Directives? { EnumValueDefinition+ } + * EnumTypeDefinition : + * - Description? enum Name Directives? { EnumValueDefinition+ } */ function parseEnumTypeDefinition(lexer: Lexer<*>): EnumTypeDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'enum'); const name = parseName(lexer); const directives = parseDirectives(lexer); @@ -965,6 +1011,7 @@ function parseEnumTypeDefinition(lexer: Lexer<*>): EnumTypeDefinitionNode { ); return { kind: ENUM_TYPE_DEFINITION, + description, name, directives, values, @@ -973,16 +1020,18 @@ function parseEnumTypeDefinition(lexer: Lexer<*>): EnumTypeDefinitionNode { } /** - * EnumValueDefinition : EnumValue Directives? + * EnumValueDefinition : Description? EnumValue Directives? * * EnumValue : Name */ function parseEnumValueDefinition(lexer: Lexer<*>): EnumValueDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); const name = parseName(lexer); const directives = parseDirectives(lexer); return { kind: ENUM_VALUE_DEFINITION, + description, name, directives, loc: loc(lexer, start), @@ -990,12 +1039,14 @@ function parseEnumValueDefinition(lexer: Lexer<*>): EnumValueDefinitionNode { } /** - * InputObjectTypeDefinition : input Name Directives? { InputValueDefinition+ } + * InputObjectTypeDefinition : + * - Description? input Name Directives? { InputValueDefinition+ } */ function parseInputObjectTypeDefinition( lexer: Lexer<*> ): InputObjectTypeDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'input'); const name = parseName(lexer); const directives = parseDirectives(lexer); @@ -1007,6 +1058,7 @@ function parseInputObjectTypeDefinition( ); return { kind: INPUT_OBJECT_TYPE_DEFINITION, + description, name, directives, fields, @@ -1032,10 +1084,11 @@ function parseTypeExtensionDefinition( /** * DirectiveDefinition : - * - directive @ Name ArgumentsDefinition? on DirectiveLocations + * - Description? directive @ Name ArgumentsDefinition? on DirectiveLocations */ function parseDirectiveDefinition(lexer: Lexer<*>): DirectiveDefinitionNode { const start = lexer.token; + const description = parseDescription(lexer); expectKeyword(lexer, 'directive'); expect(lexer, TokenKind.AT); const name = parseName(lexer); @@ -1044,6 +1097,7 @@ function parseDirectiveDefinition(lexer: Lexer<*>): DirectiveDefinitionNode { const locations = parseDirectiveLocations(lexer); return { kind: DIRECTIVE_DEFINITION, + description, name, arguments: args, locations, diff --git a/src/language/printer.js b/src/language/printer.js index a16787bfd9..5b85a3661e 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -72,9 +72,9 @@ const printDocASTReducer = { IntValue: ({ value }) => value, FloatValue: ({ value }) => value, - StringValue: ({ value, block: isBlockString }) => + StringValue: ({ value, block: isBlockString }, key) => isBlockString ? - printBlockString(value) : + printBlockString(value, key === 'description') : JSON.stringify(value), BooleanValue: ({ value }) => JSON.stringify(value), NullValue: () => 'null', @@ -106,71 +106,109 @@ const printDocASTReducer = { OperationTypeDefinition: ({ operation, type }) => operation + ': ' + type, - ScalarTypeDefinition: ({ name, directives }) => - join([ 'scalar', name, join(directives, ' ') ], ' '), - - ObjectTypeDefinition: ({ name, interfaces, directives, fields }) => + ScalarTypeDefinition: ({ description, name, directives }) => join([ - 'type', - name, - wrap('implements ', join(interfaces, ', ')), - join(directives, ' '), - block(fields) - ], ' '), - - FieldDefinition: ({ name, arguments: args, type, directives }) => - name + - wrap('(', join(args, ', '), ')') + - ': ' + type + - wrap(' ', join(directives, ' ')), - - InputValueDefinition: ({ name, type, defaultValue, directives }) => + description, + join([ 'scalar', name, join(directives, ' ') ], ' ') + ], '\n'), + + ObjectTypeDefinition: ({ + description, + name, + interfaces, + directives, + fields + }) => join([ - name + ': ' + type, - wrap('= ', defaultValue), - join(directives, ' ') - ], ' '), - - InterfaceTypeDefinition: ({ name, directives, fields }) => + description, + join([ + 'type', + name, + wrap('implements ', join(interfaces, ', ')), + join(directives, ' '), + block(fields) + ], ' ') + ], '\n'), + + FieldDefinition: + ({ description, name, arguments: args, type, directives }) => + join([ + description, + name + + wrap('(', join(args, ', '), ')') + + ': ' + type + + wrap(' ', join(directives, ' ')), + ], '\n'), + + InputValueDefinition: + ({ description, name, type, defaultValue, directives }) => + join([ + description, + join([ + name + ': ' + type, + wrap('= ', defaultValue), + join(directives, ' ') + ], ' '), + ], '\n'), + + InterfaceTypeDefinition: ({ description, name, directives, fields }) => join([ - 'interface', - name, - join(directives, ' '), - block(fields) - ], ' '), - - UnionTypeDefinition: ({ name, directives, types }) => + description, + join([ + 'interface', + name, + join(directives, ' '), + block(fields) + ], ' '), + ], '\n'), + + UnionTypeDefinition: ({ description, name, directives, types }) => join([ - 'union', - name, - join(directives, ' '), - '= ' + join(types, ' | ') - ], ' '), - - EnumTypeDefinition: ({ name, directives, values }) => + description, + join([ + 'union', + name, + join(directives, ' '), + '= ' + join(types, ' | ') + ], ' '), + ], '\n'), + + EnumTypeDefinition: ({ description, name, directives, values }) => join([ - 'enum', - name, - join(directives, ' '), - block(values) - ], ' '), - - EnumValueDefinition: ({ name, directives }) => - join([ name, join(directives, ' ') ], ' '), + description, + join([ + 'enum', + name, + join(directives, ' '), + block(values) + ], ' '), + ], '\n'), + + EnumValueDefinition: ({ description, name, directives }) => + join([ + description, + join([ name, join(directives, ' ') ], ' '), + ], '\n'), - InputObjectTypeDefinition: ({ name, directives, fields }) => + InputObjectTypeDefinition: ({ description, name, directives, fields }) => join([ - 'input', - name, - join(directives, ' '), - block(fields) - ], ' '), + description, + join([ + 'input', + name, + join(directives, ' '), + block(fields) + ], ' '), + ], '\n'), TypeExtensionDefinition: ({ definition }) => `extend ${definition}`, - DirectiveDefinition: ({ name, arguments: args, locations }) => - 'directive @' + name + wrap('(', join(args, ', '), ')') + - ' on ' + join(locations, ' | '), + DirectiveDefinition: ({ description, name, arguments: args, locations }) => + join([ + description, + 'directive @' + name + wrap('(', join(args, ', '), ')') + + ' on ' + join(locations, ' | '), + ], '\n'), }; /** @@ -210,8 +248,10 @@ function indent(maybeString) { * trailing blank line. However, if a block string starts with whitespace and is * a single-line, adding a leading blank line would strip that whitespace. */ -function printBlockString(value) { +function printBlockString(value, isDescription) { return (value[0] === ' ' || value[0] === '\t') && value.indexOf('\n') === -1 ? `"""${value.replace(/"""/g, '\\"""')}"""` : - indent('"""\n' + value.replace(/"""/g, '\\"""')) + '\n"""'; + isDescription ? + '"""\n' + value.replace(/"""/g, '\\"""') + '\n"""' : + indent('"""\n' + value.replace(/"""/g, '\\"""')) + '\n"""'; } diff --git a/src/language/visitor.js b/src/language/visitor.js index 52f94c35be..9e6ac0063c 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -40,19 +40,21 @@ export const QueryDocumentKeys = { SchemaDefinition: [ 'directives', 'operationTypes' ], OperationTypeDefinition: [ 'type' ], - ScalarTypeDefinition: [ 'name', 'directives' ], - ObjectTypeDefinition: [ 'name', 'interfaces', 'directives', 'fields' ], - FieldDefinition: [ 'name', 'arguments', 'type', 'directives' ], - InputValueDefinition: [ 'name', 'type', 'defaultValue', 'directives' ], - InterfaceTypeDefinition: [ 'name', 'directives', 'fields' ], - UnionTypeDefinition: [ 'name', 'directives', 'types' ], - EnumTypeDefinition: [ 'name', 'directives', 'values' ], - EnumValueDefinition: [ 'name', 'directives' ], - InputObjectTypeDefinition: [ 'name', 'directives', 'fields' ], + ScalarTypeDefinition: [ 'description', 'name', 'directives' ], + ObjectTypeDefinition: + [ 'description', 'name', 'interfaces', 'directives', 'fields' ], + FieldDefinition: [ 'description', 'name', 'arguments', 'type', 'directives' ], + InputValueDefinition: + [ 'description', 'name', 'type', 'defaultValue', 'directives' ], + InterfaceTypeDefinition: [ 'description', 'name', 'directives', 'fields' ], + UnionTypeDefinition: [ 'description', 'name', 'directives', 'types' ], + EnumTypeDefinition: [ 'description', 'name', 'directives', 'values' ], + EnumValueDefinition: [ 'description', 'name', 'directives' ], + InputObjectTypeDefinition: [ 'description', 'name', 'directives', 'fields' ], TypeExtensionDefinition: [ 'definition' ], - DirectiveDefinition: [ 'name', 'arguments', 'locations' ], + DirectiveDefinition: [ 'description', 'name', 'arguments', 'locations' ], }; export const BREAK = {}; diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 808e1e2ae9..912f717f66 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -25,10 +25,10 @@ import { * into an in-memory GraphQLSchema, and then finally * printing that GraphQL into the DSL */ -function cycleOutput(body) { +function cycleOutput(body, options) { const ast = parse(body); - const schema = buildASTSchema(ast); - return printSchema(schema); + const schema = buildASTSchema(ast, options); + return printSchema(schema, options); } describe('Schema Builder', () => { @@ -96,6 +96,37 @@ describe('Schema Builder', () => { }); it('Supports descriptions', () => { + const body = dedent` + schema { + query: Hello + } + + """This is a directive""" + directive @foo( + """It has an argument""" + arg: Int + ) on FIELD + + """With an enum""" + enum Color { + RED + + """Not a creative color""" + GREEN + BLUE + } + + """What a great type""" + type Hello { + """And a field to boot""" + str: String + } + `; + const output = cycleOutput(body); + expect(output).to.equal(body); + }); + + it('Supports option for comment descriptions', () => { const body = dedent` schema { query: Hello @@ -122,7 +153,7 @@ describe('Schema Builder', () => { str: String } `; - const output = cycleOutput(body); + const output = cycleOutput(body, { commentDescriptions: true }); expect(output).to.equal(body); }); diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index dfd92c3128..2b2d7e53af 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -135,7 +135,7 @@ describe('extendSchema', () => { it('can describe the extended fields', async () => { const ast = parse(` extend type Query { - # New field description. + "New field description." newField: String } `); @@ -146,6 +146,39 @@ describe('extendSchema', () => { ).to.equal('New field description.'); }); + it('can describe the extended fields with legacy comments', async () => { + const ast = parse(` + extend type Query { + # New field description. + newField: String + } + `); + const extendedSchema = extendSchema(testSchema, ast, { + commentDescriptions: true + }); + + expect( + extendedSchema.getType('Query').getFields().newField.description + ).to.equal('New field description.'); + }); + + it('describes extended fields with strings when present', async () => { + const ast = parse(` + extend type Query { + # New field description. + "Actually use this description." + newField: String + } + `); + const extendedSchema = extendSchema(testSchema, ast, { + commentDescriptions: true + }); + + expect( + extendedSchema.getType('Query').getFields().newField.description + ).to.equal('Actually use this description.'); + }); + it('extends objects by adding new fields', () => { const ast = parse(` extend type Foo { @@ -836,7 +869,9 @@ describe('extendSchema', () => { it('sets correct description when extending with a new directive', () => { const ast = parse(` - # new directive + """ + new directive + """ directive @new on QUERY `); @@ -845,6 +880,19 @@ describe('extendSchema', () => { expect(newDirective.description).to.equal('new directive'); }); + it('sets correct description using legacy comments', () => { + const ast = parse(` + # new directive + directive @new on QUERY + `); + + const extendedSchema = extendSchema(testSchema, ast, { + commentDescriptions: true + }); + const newDirective = extendedSchema.getDirective('new'); + expect(newDirective.description).to.equal('new directive'); + }); + it('may extend directives with new complex directive', () => { const ast = parse(` directive @profile(enable: Boolean! tag: String) on QUERY | FIELD diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 6aab7f33c1..3c7050d111 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -600,6 +600,251 @@ describe('Type System Printer', () => { query: Root } + """ + Directs the executor to include this field or fragment only when the \`if\` argument is true. + """ + directive @include( + """Included when true.""" + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + """ + Directs the executor to skip this field or fragment when the \`if\` argument is true. + """ + directive @skip( + """Skipped when true.""" + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + """Marks an element of a GraphQL schema as no longer supported.""" + directive @deprecated( + """ + Explains why this element was deprecated, usually also including a suggestion + for how to access supported similar data. Formatted in + [Markdown](https://daringfireball.net/projects/markdown/). + """ + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + + """ + A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + + In some cases, you need to provide options to alter GraphQL's execution behavior + in ways field arguments will not suffice, such as conditionally including or + skipping a field. Directives provide this by describing additional information + to the executor. + """ + type __Directive { + name: String! + description: String + locations: [__DirectiveLocation!]! + args: [__InputValue!]! + onOperation: Boolean! @deprecated(reason: "Use \`locations\`.") + onFragment: Boolean! @deprecated(reason: "Use \`locations\`.") + onField: Boolean! @deprecated(reason: "Use \`locations\`.") + } + + """ + A Directive can be adjacent to many parts of the GraphQL language, a + __DirectiveLocation describes one such possible adjacencies. + """ + enum __DirectiveLocation { + """Location adjacent to a query operation.""" + QUERY + + """Location adjacent to a mutation operation.""" + MUTATION + + """Location adjacent to a subscription operation.""" + SUBSCRIPTION + + """Location adjacent to a field.""" + FIELD + + """Location adjacent to a fragment definition.""" + FRAGMENT_DEFINITION + + """Location adjacent to a fragment spread.""" + FRAGMENT_SPREAD + + """Location adjacent to an inline fragment.""" + INLINE_FRAGMENT + + """Location adjacent to a schema definition.""" + SCHEMA + + """Location adjacent to a scalar definition.""" + SCALAR + + """Location adjacent to an object type definition.""" + OBJECT + + """Location adjacent to a field definition.""" + FIELD_DEFINITION + + """Location adjacent to an argument definition.""" + ARGUMENT_DEFINITION + + """Location adjacent to an interface definition.""" + INTERFACE + + """Location adjacent to a union definition.""" + UNION + + """Location adjacent to an enum definition.""" + ENUM + + """Location adjacent to an enum value definition.""" + ENUM_VALUE + + """Location adjacent to an input object type definition.""" + INPUT_OBJECT + + """Location adjacent to an input object field definition.""" + INPUT_FIELD_DEFINITION + } + + """ + One possible value for a given Enum. Enum values are unique values, not a + placeholder for a string or numeric value. However an Enum value is returned in + a JSON response as a string. + """ + type __EnumValue { + name: String! + description: String + isDeprecated: Boolean! + deprecationReason: String + } + + """ + Object and Interface types are described by a list of Fields, each of which has + a name, potentially a list of arguments, and a return type. + """ + type __Field { + name: String! + description: String + args: [__InputValue!]! + type: __Type! + isDeprecated: Boolean! + deprecationReason: String + } + + """ + Arguments provided to Fields or Directives and the input fields of an + InputObject are represented as Input Values which describe their type and + optionally a default value. + """ + type __InputValue { + name: String! + description: String + type: __Type! + + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + } + + """ + A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all + available types and directives on the server, as well as the entry points for + query, mutation, and subscription operations. + """ + type __Schema { + """A list of all types supported by this server.""" + types: [__Type!]! + + """The type that query operations will be rooted at.""" + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """A list of all directives supported by this server.""" + directives: [__Directive!]! + } + + """ + The fundamental unit of any GraphQL Schema is the type. There are many kinds of + types in GraphQL as represented by the \`__TypeKind\` enum. + + Depending on the kind of a type, certain fields describe information about that + type. Scalar types provide no information beyond a name and description, while + Enum types provide their values. Object and Interface types provide the fields + they describe. Abstract types, Union and Interface, provide the Object types + possible at runtime. List and NonNull types compose other types. + """ + type __Type { + kind: __TypeKind! + name: String + description: String + fields(includeDeprecated: Boolean = false): [__Field!] + interfaces: [__Type!] + possibleTypes: [__Type!] + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + inputFields: [__InputValue!] + ofType: __Type + } + + """An enum describing what kind of type a given \`__Type\` is.""" + enum __TypeKind { + """Indicates this type is a scalar.""" + SCALAR + + """ + Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. \`fields\` and \`possibleTypes\` are valid fields. + """ + INTERFACE + + """Indicates this type is a union. \`possibleTypes\` is a valid field.""" + UNION + + """Indicates this type is an enum. \`enumValues\` is a valid field.""" + ENUM + + """ + Indicates this type is an input object. \`inputFields\` is a valid field. + """ + INPUT_OBJECT + + """Indicates this type is a list. \`ofType\` is a valid field.""" + LIST + + """Indicates this type is a non-null. \`ofType\` is a valid field.""" + NON_NULL + } + `; + expect(output).to.equal(introspectionSchema); + }); + + it('Print Introspection Schema with comment descriptions', () => { + const Root = new GraphQLObjectType({ + name: 'Root', + fields: { + onlyField: { type: GraphQLString } + }, + }); + const Schema = new GraphQLSchema({ query: Root }); + const output = printIntrospectionSchema(Schema, { + commentDescriptions: true + }); + const introspectionSchema = dedent` + schema { + query: Root + } + # Directs the executor to include this field or fragment only when the \`if\` argument is true. directive @include( # Included when true. diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 51c1e3ad79..5888a48f2c 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -11,6 +11,7 @@ import invariant from '../jsutils/invariant'; import keyValMap from '../jsutils/keyValMap'; import type {ObjMap} from '../jsutils/ObjMap'; import { valueFromAST } from './valueFromAST'; +import blockStringValue from '../language/blockStringValue'; import { TokenKind } from '../language/lexer'; import { parse } from '../language/parser'; import type { Source } from '../language/source'; @@ -21,6 +22,7 @@ import * as Kind from '../language/kinds'; import type { Location, DocumentNode, + StringValueNode, TypeNode, NamedTypeNode, SchemaDefinitionNode, @@ -89,6 +91,7 @@ import { __TypeKind, } from '../type/introspection'; +type Options = {| commentDescriptions?: boolean |}; function buildWrappedType( innerType: GraphQLType, @@ -125,8 +128,17 @@ function getNamedTypeNode(typeNode: TypeNode): NamedTypeNode { * * Given that AST it constructs a GraphQLSchema. The resulting schema * has no resolve methods, so execution will use default resolvers. + * + * Accepts options as a second argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. + * */ -export function buildASTSchema(ast: DocumentNode): GraphQLSchema { +export function buildASTSchema( + ast: DocumentNode, + options?: Options, +): GraphQLSchema { if (!ast || ast.kind !== Kind.DOCUMENT) { throw new Error('Must provide a document ast.'); } @@ -271,7 +283,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { ): GraphQLDirective { return new GraphQLDirective({ name: directiveNode.name.value, - description: getDescription(directiveNode), + description: getDescription(directiveNode, options), locations: directiveNode.locations.map( node => ((node.value: any): DirectiveLocationEnum) ), @@ -348,7 +360,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { const typeName = def.name.value; return new GraphQLObjectType({ name: typeName, - description: getDescription(def), + description: getDescription(def, options), fields: () => makeFieldDefMap(def), interfaces: () => makeImplementedInterfaces(def), astNode: def, @@ -363,7 +375,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { field => field.name.value, field => ({ type: produceOutputType(field.type), - description: getDescription(field), + description: getDescription(field, options), args: makeInputValues(field.arguments), deprecationReason: getDeprecationReason(field), astNode: field, @@ -384,7 +396,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { const type = produceInputType(value.type); return { type, - description: getDescription(value), + description: getDescription(value, options), defaultValue: valueFromAST(value.defaultValue, type), astNode: value, }; @@ -395,7 +407,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { function makeInterfaceDef(def: InterfaceTypeDefinitionNode) { return new GraphQLInterfaceType({ name: def.name.value, - description: getDescription(def), + description: getDescription(def, options), fields: () => makeFieldDefMap(def), astNode: def, resolveType: cannotExecuteSchema, @@ -405,12 +417,12 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { function makeEnumDef(def: EnumTypeDefinitionNode) { return new GraphQLEnumType({ name: def.name.value, - description: getDescription(def), + description: getDescription(def, options), values: keyValMap( def.values, enumValue => enumValue.name.value, enumValue => ({ - description: getDescription(enumValue), + description: getDescription(enumValue, options), deprecationReason: getDeprecationReason(enumValue), astNode: enumValue, }) @@ -422,7 +434,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { function makeUnionDef(def: UnionTypeDefinitionNode) { return new GraphQLUnionType({ name: def.name.value, - description: getDescription(def), + description: getDescription(def, options), types: def.types.map(t => produceObjectType(t)), resolveType: cannotExecuteSchema, astNode: def, @@ -432,7 +444,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { function makeScalarDef(def: ScalarTypeDefinitionNode) { return new GraphQLScalarType({ name: def.name.value, - description: getDescription(def), + description: getDescription(def, options), astNode: def, serialize: () => null, // Note: validation calls the parse functions to determine if a @@ -447,7 +459,7 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema { function makeInputObjectDef(def: InputObjectTypeDefinitionNode) { return new GraphQLInputObjectType({ name: def.name.value, - description: getDescription(def), + description: getDescription(def, options), fields: () => makeInputValues(def.fields), astNode: def, }); @@ -466,16 +478,35 @@ export function getDeprecationReason( } /** - * Given an ast node, returns its string description based on a contiguous - * block full-line of comments preceding it. + * Given an ast node, returns its string description. + * + * Accepts options as a second argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. + * */ -export function getDescription(node: { loc?: Location }): ?string { +export function getDescription( + node: { loc?: Location, description?: ?StringValueNode }, + options?: Options, +): void | string { + if (node.description) { + return node.description.value; + } + if (options && options.commentDescriptions) { + const rawValue = getLeadingCommentBlock(node); + if (rawValue !== undefined) { + return blockStringValue('\n' + rawValue); + } + } +} + +function getLeadingCommentBlock(node: { loc?: Location }): void | string { const loc = node.loc; if (!loc) { return; } const comments = []; - let minSpaces; let token = loc.startToken.prev; while ( token && @@ -485,17 +516,10 @@ export function getDescription(node: { loc?: Location }): ?string { token.line !== token.prev.line ) { const value = String(token.value); - const spaces = leadingSpaces(value); - if (minSpaces === undefined || spaces < minSpaces) { - minSpaces = spaces; - } comments.push(value); token = token.prev; } - return comments - .reverse() - .map(comment => comment.slice(minSpaces)) - .join('\n'); + return comments.reverse().join('\n'); } /** @@ -506,17 +530,6 @@ export function buildSchema(source: string | Source): GraphQLSchema { return buildASTSchema(parse(source)); } -// Count the number of spaces on the starting side of a string. -function leadingSpaces(str) { - let i = 0; - for (; i < str.length; i++) { - if (str[i] !== ' ') { - break; - } - } - return i; -} - function cannotExecuteSchema() { throw new Error( 'Generated Schema cannot use Interface or Union types for execution.' diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index f40e057ff3..19eb2dba26 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -82,6 +82,7 @@ import type { DirectiveDefinitionNode, } from '../language/ast'; +type Options = {| commentDescriptions?: boolean |}; /** * Produces a new schema given an existing schema and a document which may @@ -94,10 +95,17 @@ import type { * * This algorithm copies the provided schema, applying extensions while * producing the copy. The original schema remains unaltered. + * + * Accepts options as a third argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. + * */ export function extendSchema( schema: GraphQLSchema, - documentAST: DocumentNode + documentAST: DocumentNode, + options?: Options, ): GraphQLSchema { invariant( schema instanceof GraphQLSchema, @@ -426,7 +434,7 @@ export function extendSchema( ); } newFieldMap[fieldName] = { - description: getDescription(field), + description: getDescription(field, options), type: buildOutputFieldType(field.type), args: buildInputValues(field.arguments), deprecationReason: getDeprecationReason(field), @@ -465,7 +473,7 @@ export function extendSchema( function buildObjectType(typeNode: ObjectTypeDefinitionNode) { return new GraphQLObjectType({ name: typeNode.name.value, - description: getDescription(typeNode), + description: getDescription(typeNode, options), interfaces: () => buildImplementedInterfaces(typeNode), fields: () => buildFieldMap(typeNode), astNode: typeNode, @@ -475,7 +483,7 @@ export function extendSchema( function buildInterfaceType(typeNode: InterfaceTypeDefinitionNode) { return new GraphQLInterfaceType({ name: typeNode.name.value, - description: getDescription(typeNode), + description: getDescription(typeNode, options), fields: () => buildFieldMap(typeNode), astNode: typeNode, resolveType: cannotExecuteExtendedSchema, @@ -485,7 +493,7 @@ export function extendSchema( function buildUnionType(typeNode: UnionTypeDefinitionNode) { return new GraphQLUnionType({ name: typeNode.name.value, - description: getDescription(typeNode), + description: getDescription(typeNode, options), types: typeNode.types.map(getObjectTypeFromAST), astNode: typeNode, resolveType: cannotExecuteExtendedSchema, @@ -495,7 +503,7 @@ export function extendSchema( function buildScalarType(typeNode: ScalarTypeDefinitionNode) { return new GraphQLScalarType({ name: typeNode.name.value, - description: getDescription(typeNode), + description: getDescription(typeNode, options), astNode: typeNode, serialize: id => id, // Note: validation calls the parse functions to determine if a @@ -510,12 +518,12 @@ export function extendSchema( function buildEnumType(typeNode: EnumTypeDefinitionNode) { return new GraphQLEnumType({ name: typeNode.name.value, - description: getDescription(typeNode), + description: getDescription(typeNode, options), values: keyValMap( typeNode.values, enumValue => enumValue.name.value, enumValue => ({ - description: getDescription(enumValue), + description: getDescription(enumValue, options), deprecationReason: getDeprecationReason(enumValue), astNode: enumValue, }), @@ -527,7 +535,7 @@ export function extendSchema( function buildInputObjectType(typeNode: InputObjectTypeDefinitionNode) { return new GraphQLInputObjectType({ name: typeNode.name.value, - description: getDescription(typeNode), + description: getDescription(typeNode, options), fields: () => buildInputValues(typeNode.fields), astNode: typeNode, }); @@ -538,7 +546,7 @@ export function extendSchema( ): GraphQLDirective { return new GraphQLDirective({ name: directiveNode.name.value, - description: getDescription(directiveNode), + description: getDescription(directiveNode, options), locations: directiveNode.locations.map( node => ((node.value: any): DirectiveLocationEnum) ), @@ -559,7 +567,7 @@ export function extendSchema( field => field.name.value, field => ({ type: buildOutputFieldType(field.type), - description: getDescription(field), + description: getDescription(field, options), args: buildInputValues(field.arguments), deprecationReason: getDeprecationReason(field), astNode: field, @@ -575,7 +583,7 @@ export function extendSchema( const type = buildInputFieldType(value.type); return { type, - description: getDescription(value), + description: getDescription(value, options), defaultValue: valueFromAST(value.defaultValue, type), astNode: value, }; diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index eecf626395..99774179d3 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -25,13 +25,34 @@ import { import { GraphQLString } from '../type/scalars'; import { DEFAULT_DEPRECATION_REASON } from '../type/directives'; +type Options = {| commentDescriptions?: boolean |}; -export function printSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, n => !isSpecDirective(n), isDefinedType); +/** + * Accepts options as a second argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. + * + */ +export function printSchema(schema: GraphQLSchema, options?: Options): string { + return printFilteredSchema( + schema, + n => !isSpecDirective(n), + isDefinedType, + options + ); } -export function printIntrospectionSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, isSpecDirective, isIntrospectionType); +export function printIntrospectionSchema( + schema: GraphQLSchema, + options?: Options, +): string { + return printFilteredSchema( + schema, + isSpecDirective, + isIntrospectionType, + options + ); } function isSpecDirective(directiveName: string): boolean { @@ -63,7 +84,8 @@ function isBuiltInScalar(typename: string): boolean { function printFilteredSchema( schema: GraphQLSchema, directiveFilter: (type: string) => boolean, - typeFilter: (type: string) => boolean + typeFilter: (type: string) => boolean, + options ): string { const directives = schema.getDirectives() .filter(directive => directiveFilter(directive.name)); @@ -74,8 +96,8 @@ function printFilteredSchema( .map(typeName => typeMap[typeName]); return [ printSchemaDefinition(schema) ].concat( - directives.map(printDirective), - types.map(printType) + directives.map(directive => printDirective(directive, options)), + types.map(type => printType(type, options)) ).filter(Boolean).join('\n\n') + '\n'; } @@ -135,85 +157,88 @@ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { return true; } -export function printType(type: GraphQLType): string { +export function printType( + type: GraphQLType, + options?: Options, +): string { if (type instanceof GraphQLScalarType) { - return printScalar(type); + return printScalar(type, options); } else if (type instanceof GraphQLObjectType) { - return printObject(type); + return printObject(type, options); } else if (type instanceof GraphQLInterfaceType) { - return printInterface(type); + return printInterface(type, options); } else if (type instanceof GraphQLUnionType) { - return printUnion(type); + return printUnion(type, options); } else if (type instanceof GraphQLEnumType) { - return printEnum(type); + return printEnum(type, options); } invariant(type instanceof GraphQLInputObjectType); - return printInputObject(type); + return printInputObject(type, options); } -function printScalar(type: GraphQLScalarType): string { - return printDescription(type) + +function printScalar(type: GraphQLScalarType, options): string { + return printDescription(options, type) + `scalar ${type.name}`; } -function printObject(type: GraphQLObjectType): string { +function printObject(type: GraphQLObjectType, options): string { const interfaces = type.getInterfaces(); const implementedInterfaces = interfaces.length ? ' implements ' + interfaces.map(i => i.name).join(', ') : ''; - return printDescription(type) + + return printDescription(options, type) + `type ${type.name}${implementedInterfaces} {\n` + - printFields(type) + '\n' + + printFields(options, type) + '\n' + '}'; } -function printInterface(type: GraphQLInterfaceType): string { - return printDescription(type) + +function printInterface(type: GraphQLInterfaceType, options): string { + return printDescription(options, type) + `interface ${type.name} {\n` + - printFields(type) + '\n' + + printFields(options, type) + '\n' + '}'; } -function printUnion(type: GraphQLUnionType): string { - return printDescription(type) + +function printUnion(type: GraphQLUnionType, options): string { + return printDescription(options, type) + `union ${type.name} = ${type.getTypes().join(' | ')}`; } -function printEnum(type: GraphQLEnumType): string { - return printDescription(type) + +function printEnum(type: GraphQLEnumType, options): string { + return printDescription(options, type) + `enum ${type.name} {\n` + - printEnumValues(type.getValues()) + '\n' + + printEnumValues(type.getValues(), options) + '\n' + '}'; } -function printEnumValues(values): string { +function printEnumValues(values, options): string { return values.map((value, i) => - printDescription(value, ' ', !i) + ' ' + + printDescription(options, value, ' ', !i) + ' ' + value.name + printDeprecated(value) ).join('\n'); } -function printInputObject(type: GraphQLInputObjectType): string { +function printInputObject(type: GraphQLInputObjectType, options): string { const fieldMap = type.getFields(); const fields = Object.keys(fieldMap).map(fieldName => fieldMap[fieldName]); - return printDescription(type) + + return printDescription(options, type) + `input ${type.name} {\n` + fields.map((f, i) => - printDescription(f, ' ', !i) + ' ' + printInputValue(f) + printDescription(options, f, ' ', !i) + ' ' + printInputValue(f) ).join('\n') + '\n' + '}'; } -function printFields(type) { +function printFields(options, type) { const fieldMap = type.getFields(); const fields = Object.keys(fieldMap).map(fieldName => fieldMap[fieldName]); return fields.map((f, i) => - printDescription(f, ' ', !i) + ' ' + - f.name + printArgs(f.args, ' ') + ': ' + + printDescription(options, f, ' ', !i) + ' ' + + f.name + printArgs(options, f.args, ' ') + ': ' + String(f.type) + printDeprecated(f) ).join('\n'); } -function printArgs(args, indentation = '') { +function printArgs(options, args, indentation = '') { if (args.length === 0) { return ''; } @@ -224,7 +249,8 @@ function printArgs(args, indentation = '') { } return '(\n' + args.map((arg, i) => - printDescription(arg, ' ' + indentation, !i) + ' ' + indentation + + printDescription(options, arg, ' ' + indentation, !i) + + ' ' + indentation + printInputValue(arg) ).join('\n') + '\n' + indentation + ')'; } @@ -237,9 +263,9 @@ function printInputValue(arg) { return argDecl; } -function printDirective(directive) { - return printDescription(directive) + - 'directive @' + directive.name + printArgs(directive.args) + +function printDirective(directive, options) { + return printDescription(options, directive) + + 'directive @' + directive.name + printArgs(options, directive.args) + ' on ' + directive.locations.join(' | '); } @@ -258,32 +284,74 @@ function printDeprecated(fieldOrEnumVal) { print(astFromValue(reason, GraphQLString)) + ')'; } -function printDescription(def, indentation = '', firstInBlock = true): string { +function printDescription( + options, + def, + indentation = '', + firstInBlock = true +): string { if (!def.description) { return ''; } - const lines = def.description.split('\n'); + + const lines = descriptionLines(def.description, 120 - indentation.length); + if (options && options.commentDescriptions) { + return printDescriptionWithComments(lines, indentation, firstInBlock); + } + + let description = indentation && !firstInBlock ? '\n' : ''; + if (lines.length === 1 && lines[0].length < 70) { + description += indentation + '"""' + escapeQuote(lines[0]) + '"""\n'; + return description; + } + + description += indentation + '"""\n'; + for (let i = 0; i < lines.length; i++) { + description += indentation + escapeQuote(lines[i]) + '\n'; + } + description += indentation + '"""\n'; + return description; +} + +function escapeQuote(line) { + return line.replace(/"""/g, '\\"""'); +} + +function printDescriptionWithComments(lines, indentation, firstInBlock) { let description = indentation && !firstInBlock ? '\n' : ''; for (let i = 0; i < lines.length; i++) { if (lines[i] === '') { description += indentation + '#\n'; + } else { + description += indentation + '# ' + lines[i] + '\n'; + } + } + return description; +} + +function descriptionLines(description: string, maxLen: number): Array { + const lines = []; + const rawLines = description.split('\n'); + for (let i = 0; i < rawLines.length; i++) { + if (rawLines[i] === '') { + lines.push(rawLines[i]); } else { // For > 120 character long lines, cut at space boundaries into sublines // of ~80 chars. - const sublines = breakLine(lines[i], 120 - indentation.length); + const sublines = breakLine(rawLines[i], maxLen); for (let j = 0; j < sublines.length; j++) { - description += indentation + '# ' + sublines[j] + '\n'; + lines.push(sublines[j]); } } } - return description; + return lines; } -function breakLine(line: string, len: number): Array { - if (line.length < len + 5) { +function breakLine(line: string, maxLen: number): Array { + if (line.length < maxLen + 5) { return [ line ]; } - const parts = line.split(new RegExp(`((?: |^).{15,${len - 40}}(?= |$))`)); + const parts = line.split(new RegExp(`((?: |^).{15,${maxLen - 40}}(?= |$))`)); if (parts.length < 4) { return [ line ]; }