UNPKG

graphene-codegen

Version:

Generate Graphene Python boilerplate from a GraphQL schema.

314 lines (313 loc) 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const language_1 = require("graphql/language"); const builtInScalars = new Set(["String", "Float", "Int", "Boolean", "ID"]); function generatePythonStr(schemaStr) { const schemaASTRoot = language_1.parse(schemaStr); const imports = new Set(["Schema"]); let queryTypeName = "Query"; let mutationTypeName = null; const classDeclarations = []; const context = { addGrapheneImport(importName) { imports.add(importName); } }; for (const definition of schemaASTRoot.definitions) { switch (definition.kind) { case "SchemaDefinition": { for (const operationType of definition.operationTypes) { switch (operationType.operation) { case "query": { queryTypeName = operationType.type.name.value; break; } case "mutation": { mutationTypeName = operationType.type.name.value; break; } } } break; } case "ObjectTypeDefinition": { context.addGrapheneImport("ObjectType"); if (definition.name.value === "Mutation" && mutationTypeName === null) { mutationTypeName = "Mutation"; } let classStr = `class ${definition.name.value}(ObjectType):\n`; let isEmptyClass = true; if (definition.description && definition.description.value.length) { classStr += ` '''${definition.description.value}'''\n`; } if (definition.interfaces && definition.interfaces.length) { isEmptyClass = false; classStr += " class Meta:\n"; const interfaceNames = definition.interfaces.map(iface => iface.name.value); classStr += ` interfaces = ${tupleStr(interfaceNames)}\n`; } if (definition.fields && definition.fields.length) { if (!isEmptyClass) classStr += "\n"; isEmptyClass = false; const fieldStrs = []; for (const field of definition.fields) { const fieldName = camelCaseToSnakeCase(field.name.value); const fieldArguments = getFieldArguments(field, context); const fieldType = getFieldTypeDeclaration(field, fieldArguments, context); fieldStrs.push(` ${fieldName} = ${fieldType}`); } classStr += fieldStrs.join("\n") + "\n"; } if (isEmptyClass) { classStr += " pass\n"; } classDeclarations.push(classStr); break; } case "InputObjectTypeDefinition": { context.addGrapheneImport("InputObjectType"); let classStr = `class ${definition.name.value}(InputObjectType):\n`; if (definition.description && definition.description.value.length) { classStr += ` '''${definition.description.value}'''\n`; } if (definition.fields && definition.fields.length) { const fieldStrs = []; for (const field of definition.fields) { const fieldName = camelCaseToSnakeCase(field.name.value); const fieldArguments = getFieldArguments(field, context); const fieldType = getFieldTypeDeclaration(field, fieldArguments, context); fieldStrs.push(` ${fieldName} = ${fieldType}`); } classStr += fieldStrs.join("\n") + "\n"; } else { classStr += " pass\n"; } classDeclarations.push(classStr); break; } case "InterfaceTypeDefinition": { context.addGrapheneImport("Interface"); let classStr = `class ${definition.name.value}(Interface):\n`; if (definition.description && definition.description.value.length) { classStr += ` '''${definition.description.value}'''\n`; } if (definition.fields && definition.fields.length) { const fieldStrs = []; for (const field of definition.fields) { const fieldName = camelCaseToSnakeCase(field.name.value); const fieldArguments = getFieldArguments(field, context); const fieldType = getFieldTypeDeclaration(field, fieldArguments, context); fieldStrs.push(` ${fieldName} = ${fieldType}`); } classStr += fieldStrs.join("\n") + "\n"; } else { classStr += " pass\n"; } classDeclarations.push(classStr); break; } case "ScalarTypeDefinition": { context.addGrapheneImport("Scalar"); let classStr = `class ${definition.name.value}(Scalar):\n`; if (definition.description && definition.description.value.length) { classStr += ` '''${definition.description.value}'''\n`; } classStr += " pass\n"; classDeclarations.push(classStr); break; } case "EnumTypeDefinition": { context.addGrapheneImport("Enum"); let classStr = `class ${definition.name.value}(Enum):\n`; if (definition.description && definition.description.value.length) { classStr += ` '''${definition.description.value}'''\n`; } if (definition.values) { for (let i = 0; i < definition.values.length; i++) { classStr += ` ${definition.values[i].name.value} = ${i}\n`; } } else { classStr += " pass\n"; } classDeclarations.push(classStr); break; } case "UnionTypeDefinition": { context.addGrapheneImport("Union"); let classStr = `class ${definition.name.value}(Union):\n`; if (definition.description && definition.description.value.length) { classStr += ` '''${definition.description.value}'''\n`; } if (definition.types && definition.types.length) { classStr += " class Meta:\n"; const unionTypeNames = definition.types.map(type => type.name.value); classStr += ` types = ${tupleStr(unionTypeNames)}\n`; } else { classStr += " pass\n"; } classDeclarations.push(classStr); break; } } } let outStr = ""; if (classDeclarations.length) { if (imports.size) { outStr += `from graphene import ${Array.from(imports).join(", ")}\n\n`; } outStr += classDeclarations.join("\n"); const mutationArg = mutationTypeName !== null ? `, mutation=${mutationTypeName}` : ""; outStr += `\nschema = Schema(query=${queryTypeName}${mutationArg})\n`; } return outStr; } exports.default = generatePythonStr; function objToDictLiteral(obj) { const pairs = Object.keys(obj).map(key => `'${key}': ${obj[key]}`); return `{${pairs.join(", ")}}`; } function getFieldArguments(field, ctx) { const reservedArgNames = new Set(); const extraArgs = []; // special case arguments - these need to work for both Fields and InputFields if (isSnakeCase(field.name.value)) { extraArgs.push(`name='${field.name.value}'`); reservedArgNames.add("name"); } if (field.description) { extraArgs.push(`description='${field.description.value}'`); reservedArgNames.add("description"); } // input fields don't have arguments if ("arguments" in field && field.arguments) { // these are the args that will make up the "args" parameter let collisionArgs = null; for (const arg of field.arguments) { const argName = arg.name.value; if (reservedArgNames.has(argName)) { let descriptionStr = ""; if (arg.description) { descriptionStr = `, description=${arg.description.value}`; } ctx.addGrapheneImport("Argument"); const typeName = `Argument(${getNestedTypeDeclaration(arg.type, ctx)}${descriptionStr})`; if (!collisionArgs) { collisionArgs = { [argName]: typeName }; } else { collisionArgs[argName] = typeName; } } else { const typeName = getArgumentTypeDeclaration(arg, ctx); extraArgs.push(`${argName}=${typeName}`); } } if (collisionArgs) { extraArgs.push(`args=${objToDictLiteral(collisionArgs)}`); } } return extraArgs.join(", "); } function getFieldTypeDeclaration(fieldNode, extraArgsStr, ctx) { const typeNode = fieldNode.type; switch (typeNode.kind) { case "NonNullType": { if (extraArgsStr !== "") extraArgsStr = ", " + extraArgsStr; ctx.addGrapheneImport("NonNull"); return `NonNull(${getNestedTypeDeclaration(typeNode.type, ctx)}${extraArgsStr})`; } case "ListType": { if (extraArgsStr !== "") extraArgsStr = ", " + extraArgsStr; ctx.addGrapheneImport("List"); return `List(${getNestedTypeDeclaration(typeNode.type, ctx)}${extraArgsStr})`; } case "NamedType": { if (builtInScalars.has(typeNode.name.value)) { ctx.addGrapheneImport(typeNode.name.value); return `${typeNode.name.value}(${extraArgsStr})`; } if (extraArgsStr !== "") extraArgsStr = ", " + extraArgsStr; const fieldClassName = fieldNode.kind === "FieldDefinition" ? "Field" : "InputField"; ctx.addGrapheneImport(fieldClassName); return `${fieldClassName}(${typeNode.name.value}${extraArgsStr})`; } } // @ts-ignore should never reach this line throw new Error(`Expected type node but node was ${typeNode.kind}`); } function getArgumentTypeDeclaration(argNode, ctx) { let argsStr = ""; if (argNode.description) { argsStr = `description='${argNode.description.value}'`; } const typeNode = argNode.type; switch (typeNode.kind) { case "NonNullType": { ctx.addGrapheneImport("NonNull"); if (argsStr !== "") argsStr = ", " + argsStr; return `NonNull(${getNestedTypeDeclaration(typeNode.type, ctx)}${argsStr})`; } case "ListType": { ctx.addGrapheneImport("List"); if (argsStr !== "") argsStr = ", " + argsStr; return `List(${getNestedTypeDeclaration(typeNode.type, ctx)}${argsStr})`; } case "NamedType": { if (builtInScalars.has(typeNode.name.value)) { ctx.addGrapheneImport(typeNode.name.value); return `${typeNode.name.value}(${argsStr})`; } else { if (argsStr !== "") argsStr = ", " + argsStr; ctx.addGrapheneImport("Argument"); return `Argument(${typeNode.name.value}${argsStr})`; } } } } function getNestedTypeDeclaration(typeNode, ctx) { switch (typeNode.kind) { case "NonNullType": { ctx.addGrapheneImport("NonNull"); return `NonNull(${getNestedTypeDeclaration(typeNode.type, ctx)})`; } case "ListType": { ctx.addGrapheneImport("List"); return `List(${getNestedTypeDeclaration(typeNode.type, ctx)})`; } case "NamedType": { if (builtInScalars.has(typeNode.name.value)) { ctx.addGrapheneImport(typeNode.name.value); } return typeNode.name.value; } } // @ts-ignore should never reach this line throw new Error(`Expected type node but node was ${typeNode.kind}`); } function isSnakeCase(str) { return str.includes("_"); } function camelCaseToSnakeCase(str) { return str.replace(/[\w]([A-Z])/g, m => m[0] + "_" + m[1]).toLowerCase(); } function tupleStr(strs) { if (strs.length === 1) { return `(${strs[0]}, )`; } else { return `(${strs.join(", ")})`; } }