graphene-codegen
Version:
Generate Graphene Python boilerplate from a GraphQL schema.
376 lines (341 loc) • 11.9 kB
text/typescript
import {
parse,
TypeNode,
FieldDefinitionNode,
InputValueDefinitionNode
} from "graphql/language"
const builtInScalars = new Set(["String", "Float", "Int", "Boolean", "ID"])
interface Context {
addGrapheneImport: (importName: string) => void
}
export default function generatePythonStr(schemaStr: string): string {
const schemaASTRoot = parse(schemaStr)
const imports = new Set<string>(["Schema"])
let queryTypeName = "Query"
let mutationTypeName: string | null = null
const classDeclarations: string[] = []
const context = {
addGrapheneImport(importName: string) {
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: string[] = []
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: string[] = []
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: string[] = []
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
}
function objToDictLiteral(obj: { [key: string]: string }): string {
const pairs = Object.keys(obj).map(key => `'${key}': ${obj[key]}`)
return `{${pairs.join(", ")}}`
}
function getFieldArguments(
field: FieldDefinitionNode | InputValueDefinitionNode,
ctx: Context
): string {
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: FieldDefinitionNode | InputValueDefinitionNode,
extraArgsStr: string,
ctx: Context
): string {
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: InputValueDefinitionNode,
ctx: Context
): string {
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: TypeNode, ctx: Context): string {
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: string) {
return str.includes("_")
}
function camelCaseToSnakeCase(str: string) {
return str.replace(/[\w]([A-Z])/g, m => m[0] + "_" + m[1]).toLowerCase()
}
function tupleStr(strs: string[]) {
if (strs.length === 1) {
return `(${strs[0]}, )`
} else {
return `(${strs.join(", ")})`
}
}