UNPKG

@graphql-codegen/java-apollo-android

Version:

GraphQL Code Generator plugin for generating Java classes for Apollo-Android

813 lines (805 loc) • 37.8 kB
import { createHash } from 'crypto'; import { camelCase, pascalCase } from 'change-case-all'; import { GraphQLString, isEnumType, isInputObjectType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType, isUnionType, Kind, print, } from 'graphql'; import pluralize from 'pluralize'; import { buildPackageNameFromPath, JavaDeclarationBlock } from '@graphql-codegen/java-common'; import { getBaseType } from '@graphql-codegen/plugin-helpers'; import { getBaseTypeNode, indent, indentMultiline, } from '@graphql-codegen/visitor-plugin-common'; import { BaseJavaVisitor, SCALAR_TO_WRITER_METHOD } from './base-java-visitor.js'; import { visitFieldArguments } from './field-arguments.js'; import { Imports } from './imports.js'; export class OperationVisitor extends BaseJavaVisitor { constructor(_schema, rawConfig, _availableFragments) { super(_schema, rawConfig, { package: rawConfig.package || buildPackageNameFromPath(process.cwd()), fragmentPackage: rawConfig.fragmentPackage || 'fragment', typePackage: rawConfig.typePackage || 'type', }); this._availableFragments = _availableFragments; this.visitingFragment = false; } printDocument(node) { return print(node) .replace(/\r?\n|\r/g, ' ') .replace(/"/g, '\\"') .trim(); } getPackage() { return this.visitingFragment ? this.config.fragmentPackage : this.config.package; } addCtor(className, node, cls) { const variables = node.variableDefinitions || []; const hasVariables = variables.length > 0; const nonNullVariables = variables .filter(v => v.type.kind === Kind.NON_NULL_TYPE) .map(v => { this._imports.add(Imports.Utils); return `Utils.checkNotNull(${v.variable.name.value}, "${v.variable.name.value} == null");`; }); const impl = [ ...nonNullVariables, `this.variables = ${!hasVariables ? 'Operation.EMPTY_VARIABLES' : `new ${className}.Variables(${variables.map(v => v.variable.name.value).join(', ')})`};`, ].join('\n'); cls.addClassMethod(className, null, impl, node.variableDefinitions.map(varDec => { const outputType = getBaseTypeNode(varDec.type).name.value; const schemaType = this._schema.getType(outputType); const javaClass = this.getJavaClass(schemaType); const typeToUse = this.getListTypeNodeWrapped(javaClass, varDec.type); const isNonNull = varDec.type.kind === Kind.NON_NULL_TYPE; return { name: varDec.variable.name.value, type: typeToUse, annotations: [isNonNull ? 'Nonnull' : 'Nullable'], }; }), null, 'public'); } getRootType(operation) { if (operation === 'query') { return this._schema.getQueryType(); } if (operation === 'mutation') { return this._schema.getMutationType(); } if (operation === 'subscription') { return this._schema.getSubscriptionType(); } return null; } createUniqueClassName(inUse, name, count = 0) { const possibleNewName = count === 0 ? name : `${name}${count}`; if (inUse.includes(possibleNewName)) { return this.createUniqueClassName(inUse, name, count + 1); } return possibleNewName; } transformSelectionSet(options, isRoot = true) { if (!options.result) { options.result = {}; } if (!isObjectType(options.schemaType) && !isInterfaceType(options.schemaType)) { return options.result; } const className = this.createUniqueClassName(Object.keys(options.result), options.className); const cls = new JavaDeclarationBlock() .access('public') .asKind('class') .withName(className) .implements(options.implements || []); if (!options.nonStaticClass) { cls.static(); } options.result[className] = cls; const fields = options.schemaType.getFields(); const childFields = [...(options.additionalFields || [])]; const childInlineFragments = []; const childFragmentSpread = [...(options.additionalFragments || [])]; const selections = [...(options.selectionSet || [])]; const responseFieldArr = []; for (const selection of selections) { if (selection.kind === Kind.FIELD) { this._imports.add(Imports.ResponseField); const field = fields[selection.name.value]; const isObject = selection.selectionSet && selection.selectionSet.selections && selection.selectionSet.selections.length > 0; const isNonNull = isNonNullType(field.type); const fieldAnnotation = isNonNull ? 'Nonnull' : 'Nullable'; this._imports.add(Imports[fieldAnnotation]); const baseType = getBaseType(field.type); const isList = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType)); if (isObject) { let childClsName = this.convertName(field.name); if (isList && pluralize.isPlural(childClsName)) { childClsName = pluralize.singular(childClsName); } this.transformSelectionSet({ className: childClsName, result: options.result, selectionSet: selection.selectionSet.selections, schemaType: baseType, }, false); childFields.push({ rawType: field.type, isObject: true, isList, isFragment: false, type: baseType, isNonNull, annotation: fieldAnnotation, className: childClsName, fieldName: field.name, }); } else { const javaClass = this.getJavaClass(baseType); childFields.push({ rawType: field.type, isObject: false, isFragment: false, isList, type: baseType, isNonNull, annotation: fieldAnnotation, className: javaClass, fieldName: field.name, }); } this._imports.add(Imports.ResponseField); this._imports.add(Imports.Collections); const operationArgs = visitFieldArguments(selection, this._imports); const responseFieldMethod = this._resolveResponseFieldMethodForBaseType(field.type); responseFieldArr.push(`ResponseField.${responseFieldMethod.fn}("${selection.alias ? selection.alias.value : selection.name.value}", "${selection.name.value}", ${operationArgs}, ${!isNonNullType(field.type)},${responseFieldMethod.custom ? ` CustomType.${baseType.name},` : ''} Collections.<ResponseField.Condition>emptyList())`); } else if (selection.kind === Kind.INLINE_FRAGMENT) { if (isUnionType(options.schemaType) || isInterfaceType(options.schemaType)) { childInlineFragments.push({ onType: selection.typeCondition.name.value, node: selection, }); } else { selections.push(...selection.selectionSet.selections); } } else if (selection.kind === Kind.FRAGMENT_SPREAD) { const fragment = this._availableFragments.find(f => f.name === selection.name.value); if (fragment) { childFragmentSpread.push(fragment); this._imports.add(`${this.config.fragmentPackage}.${fragment.name}`); } else { throw new Error(`Fragment with name ${selection.name.value} was not loaded as document!`); } } } if (childInlineFragments.length > 0) { const childFieldsBase = [...childFields]; childFields.push(...childInlineFragments.map(inlineFragment => { const cls = `As${inlineFragment.onType}`; const schemaType = this._schema.getType(inlineFragment.onType); this.transformSelectionSet({ additionalFields: childFieldsBase, additionalFragments: childFragmentSpread, className: cls, result: options.result, selectionSet: inlineFragment.node.selectionSet.selections, schemaType, }, false); this._imports.add(Imports.Nullable); return { isFragment: false, rawType: schemaType, isObject: true, isList: false, type: schemaType, isNonNull: false, annotation: 'Nullable', className: cls, fieldName: `as${inlineFragment.onType}`, }; })); responseFieldArr.push(...childInlineFragments.map(f => { this._imports.add(Imports.Arrays); return `ResponseField.forInlineFragment("__typename", "__typename", Arrays.asList("${f.onType}"))`; })); } if (childFragmentSpread.length > 0) { responseFieldArr.push(`ResponseField.forFragment("__typename", "__typename", Arrays.asList(${childFragmentSpread .map(f => `"${f.onType}"`) .join(', ')}))`); this._imports.add(Imports.ResponseField); this._imports.add(Imports.Nonnull); this._imports.add(Imports.Arrays); const fragmentsClassName = 'Fragments'; childFields.push({ isObject: true, isList: false, isFragment: true, rawType: options.schemaType, type: options.schemaType, isNonNull: true, annotation: 'Nonnull', className: fragmentsClassName, fieldName: 'fragments', }); const fragmentsClass = new JavaDeclarationBlock() .withName(fragmentsClassName) .access('public') .static() .final() .asKind('class'); const fragmentMapperClass = new JavaDeclarationBlock() .withName('Mapper') .access('public') .static() .final() .implements([`FragmentResponseFieldMapper<${fragmentsClassName}>`]) .asKind('class'); fragmentsClass.addClassMethod(fragmentsClassName, null, childFragmentSpread .map(spread => { const varName = camelCase(spread.name); this._imports.add(Imports.Utils); return `this.${varName} = Utils.checkNotNull(${varName}, "${varName} == null");`; }) .join('\n'), childFragmentSpread.map(spread => ({ name: camelCase(spread.name), type: spread.name, annotations: ['Nonnull'], })), [], 'public'); for (const spread of childFragmentSpread) { const fragmentVarName = camelCase(spread.name); fragmentsClass.addClassMember(fragmentVarName, spread.name, null, ['Nonnull'], 'private', { final: true, }); fragmentsClass.addClassMethod(fragmentVarName, spread.name, `return this.${fragmentVarName};`, [], ['Nonnull'], 'public', {}, []); fragmentMapperClass.addClassMember(`${fragmentVarName}FieldMapper`, `${spread.name}.Mapper`, `new ${spread.name}.Mapper()`, [], 'private', { final: true }); } fragmentMapperClass.addClassMethod('map', fragmentsClassName, ` ${childFragmentSpread .map(spread => { const fragmentVarName = camelCase(spread.name); return `${spread.name} ${fragmentVarName} = null; if (${spread.name}.POSSIBLE_TYPES.contains(conditionalType)) { ${fragmentVarName} = ${fragmentVarName}FieldMapper.map(reader); }`; }) .join('\n')} return new Fragments(${childFragmentSpread .map(spread => { const fragmentVarName = camelCase(spread.name); return `Utils.checkNotNull(${fragmentVarName}, "${fragmentVarName} == null")`; }) .join(', ')}); `, [ { name: 'reader', type: 'ResponseReader', }, { name: 'conditionalType', type: 'String', annotations: ['Nonnull'], }, ], ['Nonnull'], 'public', {}, ['Override']); this._imports.add(Imports.String); this._imports.add(Imports.ResponseReader); this._imports.add(Imports.ResponseFieldMarshaller); this._imports.add(Imports.ResponseWriter); fragmentsClass.addClassMethod('marshaller', 'ResponseFieldMarshaller', `return new ResponseFieldMarshaller() { @Override public void marshal(ResponseWriter writer) { ${childFragmentSpread .map(spread => { const fragmentVarName = camelCase(spread.name); return indentMultiline(`final ${spread.name} $${fragmentVarName} = ${fragmentVarName};\nif ($${fragmentVarName} != null) { $${fragmentVarName}.marshaller().marshal(writer); }`, 2); }) .join('\n')} } }; `, [], [], 'public'); fragmentsClass.addClassMember('$toString', 'String', null, [], 'private', { volatile: true }); fragmentsClass.addClassMember('$hashCode', 'int', null, [], 'private', { volatile: true }); fragmentsClass.addClassMember('$hashCodeMemoized', 'boolean', null, [], 'private', { volatile: true, }); fragmentsClass.addClassMethod('toString', 'String', `if ($toString == null) { $toString = "${fragmentsClassName}{" ${childFragmentSpread .map(spread => { const varName = camelCase(spread.name); return indent(`+ "${varName}=" + ${varName} + ", "`, 2); }) .join('\n')} + "}"; } return $toString;`, [], [], 'public', {}, ['Override']); // Add equals fragmentsClass.addClassMethod('equals', 'boolean', `if (o == this) { return true; } if (o instanceof ${fragmentsClassName}) { ${fragmentsClassName} that = (${fragmentsClassName}) o; return ${childFragmentSpread .map(spread => { const varName = camelCase(spread.name); return `this.${varName}.equals(that.${varName})`; }) .join(' && ')}; } return false;`, [{ name: 'o', type: 'Object' }], [], 'public', {}, ['Override']); // hashCode fragmentsClass.addClassMethod('hashCode', 'int', `if (!$hashCodeMemoized) { int h = 1; ${childFragmentSpread .map(spread => { const varName = camelCase(spread.name); return indentMultiline(`h *= 1000003;\nh ^= ${varName}.hashCode();`, 1); }) .join('\n')} $hashCode = h; $hashCodeMemoized = true; } return $hashCode;`, [], [], 'public', {}, ['Override']); this._imports.add(Imports.FragmentResponseFieldMapper); fragmentsClass.nestedClass(fragmentMapperClass); cls.nestedClass(fragmentsClass); } if (responseFieldArr.length > 0 && !isRoot) { responseFieldArr.unshift(`ResponseField.forString("__typename", "__typename", null, false, Collections.<ResponseField.Condition>emptyList())`); } if (!isRoot) { this._imports.add(Imports.Nonnull); childFields.unshift({ isObject: false, isFragment: false, isList: false, type: GraphQLString, rawType: GraphQLString, isNonNull: true, annotation: 'Nonnull', className: 'String', fieldName: '__typename', }); } // Add members childFields.forEach(c => { cls.addClassMember(c.fieldName, this.getListTypeWrapped(c.className, c.rawType), null, [c.annotation], 'private', { final: true }); }); // Add $toString, $hashCode, $hashCodeMemoized cls.addClassMember('$toString', 'String', null, [], 'private', { volatile: true }); cls.addClassMember('$hashCode', 'int', null, [], 'private', { volatile: true }); cls.addClassMember('$hashCodeMemoized', 'boolean', null, [], 'private', { volatile: true }); // Add responseFields for all fields cls.addClassMember('$responseFields', 'ResponseField[]', `{\n${indentMultiline(responseFieldArr.join(',\n'), 2) + '\n }'}`, [], null, { static: true, final: true }); // Add Ctor this._imports.add(Imports.Utils); cls.addClassMethod(className, null, childFields .map(c => `this.${c.fieldName} = ${c.isNonNull ? `Utils.checkNotNull(${c.fieldName}, "${c.fieldName} == null")` : c.fieldName};`) .join('\n'), childFields.map(c => ({ name: c.fieldName, type: this.getListTypeWrapped(c.className, c.rawType), annotations: [c.annotation], })), null, 'public'); // Add getters for all members childFields.forEach(c => { cls.addClassMethod(c.fieldName, this.getListTypeWrapped(c.className, c.rawType), `return this.${c.fieldName};`, [], [c.annotation], 'public', {}); }); // Add .toString() cls.addClassMethod('toString', 'String', `if ($toString == null) { $toString = "${className}{" ${childFields.map(c => indent(`+ "${c.fieldName}=" + ${c.fieldName} + ", "`, 2)).join('\n')} + "}"; } return $toString;`, [], [], 'public', {}, ['Override']); // Add equals cls.addClassMethod('equals', 'boolean', `if (o == this) { return true; } if (o instanceof ${className}) { ${className} that = (${className}) o; return ${childFields .map(c => c.isNonNull ? `this.${c.fieldName}.equals(that.${c.fieldName})` : `((this.${c.fieldName} == null) ? (that.${c.fieldName} == null) : this.${c.fieldName}.equals(that.${c.fieldName}))`) .join(' && ')}; } return false;`, [{ name: 'o', type: 'Object' }], [], 'public', {}, ['Override']); // hashCode cls.addClassMethod('hashCode', 'int', `if (!$hashCodeMemoized) { int h = 1; ${childFields .map(f => indentMultiline(`h *= 1000003;\nh ^= ${!f.isNonNull ? `(${f.fieldName} == null) ? 0 : ` : ''}${f.fieldName}.hashCode();`, 1)) .join('\n')} $hashCode = h; $hashCodeMemoized = true; } return $hashCode;`, [], [], 'public', {}, ['Override']); this._imports.add(Imports.ResponseReader); this._imports.add(Imports.ResponseFieldMarshaller); this._imports.add(Imports.ResponseWriter); // marshaller cls.addClassMethod('marshaller', 'ResponseFieldMarshaller', `return new ResponseFieldMarshaller() { @Override public void marshal(ResponseWriter writer) { ${childFields .map((f, index) => { const writerMethod = this._getWriterMethodByType(f.type); if (f.isList) { return indentMultiline(`writer.writeList($responseFields[${index}], ${f.fieldName}, new ResponseWriter.ListWriter() { @Override public void write(Object value, ResponseWriter.ListItemWriter listItemWriter) { listItemWriter.${writerMethod.name}(((${f.className}) value)${writerMethod.useMarshaller ? '.marshaller()' : ''}); } });`, 2); } let fValue = `${f.fieldName}${writerMethod.useMarshaller ? '.marshaller()' : ''}`; if (writerMethod.checkNull || !f.isNonNull) { fValue = `${f.fieldName} != null ? ${fValue} : null`; } return indent(`writer.${writerMethod.name}(${writerMethod.castTo ? `(${writerMethod.castTo}) ` : ''}$responseFields[${index}], ${fValue});`, 2); }) .join('\n')} } };`, [], [], 'public'); cls.nestedClass(this.buildMapperClass(className, childFields)); return options.result; } getReaderFn(baseType) { if (isScalarType(baseType)) { if (baseType.name === 'String') { return { fn: `readString` }; } if (baseType.name === 'Int') { return { fn: `readInt` }; } if (baseType.name === 'Float') { return { fn: `readDouble` }; } if (baseType.name === 'Boolean') { return { fn: `readBoolean` }; } return { fn: `readCustomType`, custom: true }; } if (isEnumType(baseType)) { return { fn: `readString` }; } return { fn: `readObject`, object: baseType.name }; } buildMapperClass(parentClassName, childFields) { const wrapList = (childField, rawType, edgeStr) => { if (isNonNullType(rawType)) { return wrapList(childField, rawType.ofType, edgeStr); } if (isListType(rawType)) { const typeStr = this.getListTypeWrapped(childField.className, rawType.ofType); const innerContent = wrapList(childField, rawType.ofType, edgeStr); const inner = isListType(rawType.ofType) ? `return listItemReader.readList(${innerContent});` : innerContent; return `new ResponseReader.ListReader<${typeStr}>() { @Override public ${typeStr} read(ResponseReader.ListItemReader listItemReader) { ${indentMultiline(inner, 2)} } }`; } return edgeStr; }; this._imports.add(Imports.ResponseReader); const mapperBody = childFields.map((f, index) => { const varDec = `final ${this.getListTypeWrapped(f.className, f.rawType)} ${f.fieldName} =`; const readerFn = this.getReaderFn(f.type); if (f.isFragment) { return `${varDec} reader.readConditional($responseFields[${index}], new ResponseReader.ConditionalTypeReader<${f.className}>() { @Override public ${f.className} read(String conditionalType, ResponseReader reader) { return fragmentsFieldMapper.map(reader, conditionalType); } });`; } if (f.isList) { const listReader = readerFn.object ? `return listItemReader.${readerFn.fn}(new ResponseReader.ObjectReader<Item>() { @Override public Item read(ResponseReader reader) { return ${f.fieldName}FieldMapper.map(reader); } });` : `return listItemReader.${readerFn.fn}();`; const wrappedList = wrapList(f, f.rawType, listReader); return `${varDec} reader.readList($responseFields[${index}], ${wrappedList});`; } if (readerFn.object) { return `${varDec} reader.readObject($responseFields[${index}], new ResponseReader.ObjectReader<${f.className}>() { @Override public ${f.className} read(ResponseReader reader) { return ${f.fieldName}FieldMapper.map(reader); } });`; } return `${varDec} reader.${readerFn.fn}(${readerFn.custom ? '(ResponseField.CustomTypeField) ' : ''}$responseFields[${index}]);`; }); const mapperImpl = [ ...mapperBody, `return new ${parentClassName}(${childFields.map(f => f.fieldName).join(', ')});`, ].join('\n'); const cls = new JavaDeclarationBlock() .access('public') .static() .final() .asKind('class') .withName('Mapper') .implements([`ResponseFieldMapper<${parentClassName}>`]) .addClassMethod('map', parentClassName, mapperImpl, [ { name: 'reader', type: 'ResponseReader', }, ], [], 'public', {}, ['Override']); childFields .filter(c => c.isObject) .forEach(childField => { cls.addClassMember(`${childField.fieldName}FieldMapper`, `${childField.className}.Mapper`, `new ${childField.className}.Mapper()`, [], 'private', { final: true }); }); return cls; } _resolveResponseFieldMethodForBaseType(baseType) { if (isListType(baseType)) { return { fn: `forList` }; } if (isNonNullType(baseType)) { return this._resolveResponseFieldMethodForBaseType(baseType.ofType); } if (isScalarType(baseType)) { if (baseType.name === 'String') { return { fn: `forString` }; } if (baseType.name === 'Int') { return { fn: `forInt` }; } if (baseType.name === 'Float') { return { fn: `forDouble` }; } if (baseType.name === 'Boolean') { return { fn: `forBoolean` }; } this._imports.add(`${this.config.typePackage}.CustomType`); return { fn: `forCustomType`, custom: true }; } if (isEnumType(baseType)) { return { fn: `forEnum` }; } return { fn: `forObject` }; } FragmentDefinition(node) { this.visitingFragment = true; const className = node.name.value; const schemaType = this._schema.getType(node.typeCondition.name.value); this._imports.add(Imports.Arrays); this._imports.add(Imports.GraphqlFragment); this._imports.add(Imports.List); this._imports.add(Imports.String); this._imports.add(Imports.Collections); this._imports.add(Imports.Override); this._imports.add(Imports.Generated); this._imports.add(Imports.ResponseFieldMapper); const dataClasses = this.transformSelectionSet({ className, nonStaticClass: true, implements: ['GraphqlFragment'], selectionSet: node.selectionSet && node.selectionSet.selections ? node.selectionSet.selections : [], result: {}, schemaType, }, false); const rootCls = dataClasses[className]; const printed = this.printDocument(node); rootCls.addClassMember('FRAGMENT_DEFINITION', 'String', `"${printed}"`, [], 'public', { static: true, final: true, }); const possibleTypes = isObjectType(schemaType) ? [schemaType.name] : this.getImplementingTypes(schemaType); rootCls.addClassMember('POSSIBLE_TYPES', 'List<String>', `Collections.unmodifiableList(Arrays.asList(${possibleTypes.map(t => `"${t}"`).join(', ')}))`, [], 'public', { static: true, final: true }); Object.keys(dataClasses) .filter(name => name !== className) .forEach(clsName => { rootCls.nestedClass(dataClasses[clsName]); }); return rootCls.string; } OperationDefinition(node) { this.visitingFragment = false; const operationType = pascalCase(node.operation); const operationSchemaType = this.getRootType(node.operation); const className = node.name.value.endsWith(operationType) ? operationType : `${node.name.value}${operationType}`; this._imports.add(Imports[operationType]); this._imports.add(Imports.String); this._imports.add(Imports.Override); this._imports.add(Imports.Generated); this._imports.add(Imports.OperationName); this._imports.add(Imports.Operation); this._imports.add(Imports.ResponseFieldMapper); const cls = new JavaDeclarationBlock() .annotate([`Generated("Apollo GraphQL")`]) .access('public') .final() .asKind('class') .withName(className); const printed = this.printDocument(node); cls.implements([ `${operationType}<${className}.Data, ${className}.Data, ${node.variableDefinitions.length === 0 ? 'Operation' : className}.Variables>`, ]); cls.addClassMember('OPERATION_DEFINITION', 'String', `"${printed}"`, [], 'public', { static: true, final: true, }); cls.addClassMember('QUERY_DOCUMENT', 'String', 'OPERATION_DEFINITION', [], 'public', { static: true, final: true, }); cls.addClassMember('OPERATION_NAME', 'OperationName', `new OperationName() { @Override public String name() { return "${node.name.value}"; } }`, [], 'public', { static: true, final: true }); cls.addClassMember('variables', `${node.variableDefinitions.length === 0 ? 'Operation' : className}.Variables`, null, [], 'private', { final: true }); cls.addClassMethod('queryDocument', `String`, `return QUERY_DOCUMENT;`, [], [], 'public', {}, [ 'Override', ]); cls.addClassMethod('wrapData', `${className}.Data`, `return data;`, [ { name: 'data', type: `${className}.Data`, }, ], [], 'public', {}, ['Override']); cls.addClassMethod('variables', `${node.variableDefinitions.length === 0 ? 'Operation' : className}.Variables`, `return variables;`, [], [], 'public', {}, ['Override']); cls.addClassMethod('responseFieldMapper', `ResponseFieldMapper<${className}.Data>`, `return new Data.Mapper();`, [], [], 'public', {}, ['Override']); cls.addClassMethod('builder', `Builder`, `return new Builder();`, [], [], 'public', { static: true }, []); cls.addClassMethod('name', `OperationName`, `return OPERATION_NAME;`, [], [], 'public', {}, [ 'Override', ]); cls.addClassMethod('operationId', `String`, `return "${createHash('md5').update(printed).digest('hex')}";`, [], [], 'public', {}, []); this.addCtor(className, node, cls); this._imports.add(Imports.Operation); const dataClasses = this.transformSelectionSet({ className: 'Data', implements: ['Operation.Data'], selectionSet: node.selectionSet && node.selectionSet.selections ? node.selectionSet.selections : [], result: {}, schemaType: operationSchemaType, }); Object.keys(dataClasses).forEach(className => { cls.nestedClass(dataClasses[className]); }); cls.nestedClass(this.createBuilderClass(className, node.variableDefinitions || [])); cls.nestedClass(this.createVariablesClass(className, node.variableDefinitions || [])); return cls.string; } createVariablesClass(parentClassName, variables) { const className = 'Variables'; const cls = new JavaDeclarationBlock() .static() .access('public') .final() .asKind('class') .extends(['Operation.Variables']) .withName(className); const ctorImpl = []; const ctorArgs = []; variables.forEach(variable => { ctorImpl.push(`this.${variable.variable.name.value} = ${variable.variable.name.value};`); ctorImpl.push(`this.valueMap.put("${variable.variable.name.value}", ${variable.variable.name.value});`); const baseTypeNode = getBaseTypeNode(variable.type); const schemaType = this._schema.getType(baseTypeNode.name.value); const javaClass = this.getJavaClass(schemaType); const annotation = isNonNullType(variable.type) ? 'Nullable' : 'Nonnull'; this._imports.add(Imports[annotation]); ctorArgs.push({ name: variable.variable.name.value, type: javaClass, annotations: [annotation], }); cls.addClassMember(variable.variable.name.value, javaClass, null, [annotation], 'private'); cls.addClassMethod(variable.variable.name.value, javaClass, `return ${variable.variable.name.value};`, [], [], 'public'); }); this._imports.add(Imports.LinkedHashMap); this._imports.add(Imports.Map); cls.addClassMethod(className, null, ctorImpl.join('\n'), ctorArgs, [], 'public'); cls.addClassMember('valueMap', 'Map<String, Object>', 'new LinkedHashMap<>()', [], 'private', { final: true, transient: true, }); cls.addClassMethod('valueMap', 'Map<String, Object>', 'return Collections.unmodifiableMap(valueMap);', [], [], 'public', {}, ['Override']); const marshallerImpl = `return new InputFieldMarshaller() { @Override public void marshal(InputFieldWriter writer) throws IOException { ${variables .map(v => { const baseTypeNode = getBaseTypeNode(v.type); const schemaType = this._schema.getType(baseTypeNode.name.value); const writerMethod = this._getWriterMethodByType(schemaType, true); return indent(`writer.${writerMethod.name}("${v.variable.name.value}", ${writerMethod.checkNull ? `${v.variable.name.value} != null ? ${v.variable.name.value}${writerMethod.useMarshaller ? '.marshaller()' : ''} : null` : v.variable.name.value});`, 2); }) .join('\n')} } };`; this._imports.add(Imports.InputFieldMarshaller); this._imports.add(Imports.InputFieldWriter); this._imports.add(Imports.IOException); cls.addClassMethod('marshaller', 'InputFieldMarshaller', marshallerImpl, [], [], 'public', {}, [ 'Override', ]); return cls; } _getWriterMethodByType(schemaType, idAsString = false) { if (isScalarType(schemaType)) { if (SCALAR_TO_WRITER_METHOD[schemaType.name] && (idAsString || schemaType.name !== 'ID')) { return { name: SCALAR_TO_WRITER_METHOD[schemaType.name], checkNull: false, useMarshaller: false, }; } return { name: 'writeCustom', checkNull: false, useMarshaller: false, castTo: 'ResponseField.CustomTypeField', }; } if (isInputObjectType(schemaType)) { return { name: 'writeObject', checkNull: true, useMarshaller: true }; } if (isEnumType(schemaType)) { return { name: 'writeString', checkNull: false, useMarshaller: false }; } if (isObjectType(schemaType) || isInterfaceType(schemaType)) { return { name: 'writeObject', checkNull: true, useMarshaller: true }; } return { name: 'writeString', useMarshaller: false, checkNull: false }; } createBuilderClass(parentClassName, variables) { const builderClassName = 'Builder'; const cls = new JavaDeclarationBlock() .static() .final() .access('public') .asKind('class') .withName(builderClassName) .addClassMethod(builderClassName, null, ''); variables.forEach(variable => { const baseTypeNode = getBaseTypeNode(variable.type); const schemaType = this._schema.getType(baseTypeNode.name.value); const javaClass = this.getJavaClass(schemaType); const annotation = isNonNullType(variable.type) ? 'Nonnull' : 'Nullable'; this._imports.add(Imports[annotation]); cls.addClassMember(variable.variable.name.value, javaClass, null, [annotation], 'private'); cls.addClassMethod(variable.variable.name.value, builderClassName, `this.${variable.variable.name.value} = ${variable.variable.name.value};\nreturn this;`, [ { name: variable.variable.name.value, type: javaClass, annotations: [annotation], }, ], [], 'public'); }); this._imports.add(Imports.Utils); const nonNullChecks = variables .filter(f => isNonNullType(f)) .map(f => `Utils.checkNotNull(${f.variable.name.value}, "${f.variable.name.value} == null");`); const returnStatement = `return new ${parentClassName}(${variables .map(v => v.variable.name.value) .join(', ')});`; cls.addClassMethod('build', parentClassName, `${[...nonNullChecks, returnStatement].join('\n')}`, [], [], 'public'); return cls; } }