graphene-codegen
Version:
Generate Graphene Python boilerplate from a GraphQL schema.
314 lines (313 loc) • 13.4 kB
JavaScript
;
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(", ")})`;
}
}