// @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, 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); }