| // @flow strict |
| |
| import { visit } from './visitor'; |
| import { type ASTNode } from './ast'; |
| import { printBlockString } from './blockString'; |
| |
| /** |
| * Converts an AST into a string, using one set of reasonable |
| * formatting rules. |
| */ |
| export function print(ast: ASTNode): string { |
| return visit(ast, { leave: printDocASTReducer }); |
| } |
| |
| // TODO: provide better type coverage in future |
| const printDocASTReducer: any = { |
| Name: node => node.value, |
| Variable: node => '$' + node.name, |
| |
| // Document |
| |
| Document: node => join(node.definitions, '\n\n') + '\n', |
| |
| OperationDefinition(node) { |
| const op = node.operation; |
| const name = node.name; |
| const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')'); |
| const directives = join(node.directives, ' '); |
| const selectionSet = node.selectionSet; |
| // Anonymous queries with no directives or variable definitions can use |
| // the query short form. |
| return !name && !directives && !varDefs && op === 'query' |
| ? selectionSet |
| : join([op, join([name, varDefs]), directives, selectionSet], ' '); |
| }, |
| |
| VariableDefinition: ({ variable, type, defaultValue, directives }) => |
| variable + |
| ': ' + |
| type + |
| wrap(' = ', defaultValue) + |
| wrap(' ', join(directives, ' ')), |
| SelectionSet: ({ selections }) => block(selections), |
| |
| Field: ({ alias, name, arguments: args, directives, selectionSet }) => |
| join( |
| [ |
| wrap('', alias, ': ') + name + wrap('(', join(args, ', '), ')'), |
| join(directives, ' '), |
| selectionSet, |
| ], |
| ' ', |
| ), |
| |
| Argument: ({ name, value }) => name + ': ' + value, |
| |
| // Fragments |
| |
| FragmentSpread: ({ name, directives }) => |
| '...' + name + wrap(' ', join(directives, ' ')), |
| |
| InlineFragment: ({ typeCondition, directives, selectionSet }) => |
| join( |
| ['...', wrap('on ', typeCondition), join(directives, ' '), selectionSet], |
| ' ', |
| ), |
| |
| FragmentDefinition: ({ |
| name, |
| typeCondition, |
| variableDefinitions, |
| directives, |
| selectionSet, |
| }) => |
| // Note: fragment variable definitions are experimental and may be changed |
| // or removed in the future. |
| `fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` + |
| `on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` + |
| selectionSet, |
| |
| // Value |
| |
| IntValue: ({ value }) => value, |
| FloatValue: ({ value }) => value, |
| StringValue: ({ value, block: isBlockString }, key) => |
| isBlockString |
| ? printBlockString(value, key === 'description' ? '' : ' ') |
| : JSON.stringify(value), |
| BooleanValue: ({ value }) => (value ? 'true' : 'false'), |
| NullValue: () => 'null', |
| EnumValue: ({ value }) => value, |
| ListValue: ({ values }) => '[' + join(values, ', ') + ']', |
| ObjectValue: ({ fields }) => '{' + join(fields, ', ') + '}', |
| ObjectField: ({ name, value }) => name + ': ' + value, |
| |
| // Directive |
| |
| Directive: ({ name, arguments: args }) => |
| '@' + name + wrap('(', join(args, ', '), ')'), |
| |
| // Type |
| |
| NamedType: ({ name }) => name, |
| ListType: ({ type }) => '[' + type + ']', |
| NonNullType: ({ type }) => type + '!', |
| |
| // Type System Definitions |
| |
| SchemaDefinition: addDescription(({ directives, operationTypes }) => |
| join(['schema', join(directives, ' '), block(operationTypes)], ' '), |
| ), |
| |
| OperationTypeDefinition: ({ operation, type }) => operation + ': ' + type, |
| |
| ScalarTypeDefinition: addDescription(({ name, directives }) => |
| join(['scalar', name, join(directives, ' ')], ' '), |
| ), |
| |
| ObjectTypeDefinition: addDescription( |
| ({ name, interfaces, directives, fields }) => |
| join( |
| [ |
| 'type', |
| name, |
| wrap('implements ', join(interfaces, ' & ')), |
| join(directives, ' '), |
| block(fields), |
| ], |
| ' ', |
| ), |
| ), |
| |
| FieldDefinition: addDescription( |
| ({ name, arguments: args, type, directives }) => |
| name + |
| (hasMultilineItems(args) |
| ? wrap('(\n', indent(join(args, '\n')), '\n)') |
| : wrap('(', join(args, ', '), ')')) + |
| ': ' + |
| type + |
| wrap(' ', join(directives, ' ')), |
| ), |
| |
| InputValueDefinition: addDescription( |
| ({ name, type, defaultValue, directives }) => |
| join( |
| [name + ': ' + type, wrap('= ', defaultValue), join(directives, ' ')], |
| ' ', |
| ), |
| ), |
| |
| InterfaceTypeDefinition: addDescription( |
| ({ name, interfaces, directives, fields }) => |
| join( |
| [ |
| 'interface', |
| name, |
| wrap('implements ', join(interfaces, ' & ')), |
| join(directives, ' '), |
| block(fields), |
| ], |
| ' ', |
| ), |
| ), |
| |
| UnionTypeDefinition: addDescription(({ name, directives, types }) => |
| join( |
| [ |
| 'union', |
| name, |
| join(directives, ' '), |
| types && types.length !== 0 ? '= ' + join(types, ' | ') : '', |
| ], |
| ' ', |
| ), |
| ), |
| |
| EnumTypeDefinition: addDescription(({ name, directives, values }) => |
| join(['enum', name, join(directives, ' '), block(values)], ' '), |
| ), |
| |
| EnumValueDefinition: addDescription(({ name, directives }) => |
| join([name, join(directives, ' ')], ' '), |
| ), |
| |
| InputObjectTypeDefinition: addDescription(({ name, directives, fields }) => |
| join(['input', name, join(directives, ' '), block(fields)], ' '), |
| ), |
| |
| DirectiveDefinition: addDescription( |
| ({ name, arguments: args, repeatable, locations }) => |
| 'directive @' + |
| name + |
| (hasMultilineItems(args) |
| ? wrap('(\n', indent(join(args, '\n')), '\n)') |
| : wrap('(', join(args, ', '), ')')) + |
| (repeatable ? ' repeatable' : '') + |
| ' on ' + |
| join(locations, ' | '), |
| ), |
| |
| SchemaExtension: ({ directives, operationTypes }) => |
| join(['extend schema', join(directives, ' '), block(operationTypes)], ' '), |
| |
| ScalarTypeExtension: ({ name, directives }) => |
| join(['extend scalar', name, join(directives, ' ')], ' '), |
| |
| ObjectTypeExtension: ({ name, interfaces, directives, fields }) => |
| join( |
| [ |
| 'extend type', |
| name, |
| wrap('implements ', join(interfaces, ' & ')), |
| join(directives, ' '), |
| block(fields), |
| ], |
| ' ', |
| ), |
| |
| InterfaceTypeExtension: ({ name, interfaces, directives, fields }) => |
| join( |
| [ |
| 'extend interface', |
| name, |
| wrap('implements ', join(interfaces, ' & ')), |
| join(directives, ' '), |
| block(fields), |
| ], |
| ' ', |
| ), |
| |
| UnionTypeExtension: ({ name, directives, types }) => |
| join( |
| [ |
| 'extend union', |
| name, |
| join(directives, ' '), |
| types && types.length !== 0 ? '= ' + join(types, ' | ') : '', |
| ], |
| ' ', |
| ), |
| |
| EnumTypeExtension: ({ name, directives, values }) => |
| join(['extend enum', name, join(directives, ' '), block(values)], ' '), |
| |
| InputObjectTypeExtension: ({ name, directives, fields }) => |
| join(['extend input', name, join(directives, ' '), block(fields)], ' '), |
| }; |
| |
| function addDescription(cb) { |
| return node => join([node.description, cb(node)], '\n'); |
| } |
| |
| /** |
| * Given maybeArray, print an empty string if it is null or empty, otherwise |
| * print all items together separated by separator if provided |
| */ |
| function join(maybeArray: ?Array<string>, separator = '') { |
| return maybeArray?.filter(x => x).join(separator) ?? ''; |
| } |
| |
| /** |
| * Given array, print each item on its own line, wrapped in an |
| * indented "{ }" block. |
| */ |
| function block(array) { |
| return array && array.length !== 0 |
| ? '{\n' + indent(join(array, '\n')) + '\n}' |
| : ''; |
| } |
| |
| /** |
| * If maybeString is not null or empty, then wrap with start and end, otherwise |
| * print an empty string. |
| */ |
| function wrap(start, maybeString, end = '') { |
| return maybeString ? start + maybeString + end : ''; |
| } |
| |
| function indent(maybeString) { |
| return maybeString && ' ' + maybeString.replace(/\n/g, '\n '); |
| } |
| |
| function isMultiline(string) { |
| return string.indexOf('\n') !== -1; |
| } |
| |
| function hasMultilineItems(maybeArray) { |
| return maybeArray && maybeArray.some(isMultiline); |
| } |