graphql: generate Flow types from a schema (#927)
Summary: Generating Flow types from a structured schema is both straightforward and terribly useful. This commit implements it. Test Plan: Inspect the snapshot for correctness manually. Then, copy it into a new file, remove backslash-escapes, and verify that it passes Flow. A subsequent commit will generate types for the actual GitHub schema. wchargin-branch: graphql-generate-flow-types
This commit is contained in:
parent
e787db53c4
commit
04f7e9ea8c
|
@ -0,0 +1,53 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`graphql/generateFlowTypes generateFlowTypes works on a representative schema 1`] = `
|
||||
"// @flow
|
||||
|
||||
// Autogenerated file. Do not edit.
|
||||
|
||||
export type Actor = Human | TalentedChimpanzee;
|
||||
|
||||
export type Color = \\"BLUE\\" | \\"GREEN\\" | \\"RED\\";
|
||||
|
||||
export type DateTime = string;
|
||||
|
||||
export type Dollars = number;
|
||||
|
||||
export type EmptyEnum = empty;
|
||||
|
||||
export type EmptyUnion = empty;
|
||||
|
||||
export type Human = {|
|
||||
+__typename: \\"Human\\",
|
||||
+id: string,
|
||||
+name: null | String,
|
||||
|};
|
||||
|
||||
export type PaintJob = {|
|
||||
+__typename: \\"PaintJob\\",
|
||||
+color: Color,
|
||||
+completed: null | DateTime,
|
||||
+cost: Dollars,
|
||||
+designer: null | Actor,
|
||||
+details: null | {|
|
||||
+description: null | String,
|
||||
+moreMetadata: mixed,
|
||||
+oldColor: Color,
|
||||
+oldPainter: null | Actor,
|
||||
|},
|
||||
+id: string,
|
||||
+metadata: mixed,
|
||||
+painters: $ReadOnlyArray<null | Actor>,
|
||||
+referrer: null | PaintJob,
|
||||
+relatedWork: $ReadOnlyArray<null | PaintJob>,
|
||||
|};
|
||||
|
||||
export type String = string;
|
||||
|
||||
export type TalentedChimpanzee = {|
|
||||
+__typename: \\"TalentedChimpanzee\\",
|
||||
+id: string,
|
||||
+name: null | String,
|
||||
|};
|
||||
"
|
||||
`;
|
|
@ -0,0 +1,118 @@
|
|||
// @flow
|
||||
|
||||
import prettier, {type Options as PrettierOptions} from "prettier";
|
||||
|
||||
import type {
|
||||
ConnectionFieldType,
|
||||
FieldType,
|
||||
IdFieldType,
|
||||
NestedFieldType,
|
||||
NodeFieldType,
|
||||
PrimitiveFieldType,
|
||||
Schema,
|
||||
} from "./schema";
|
||||
|
||||
export default function generateFlowTypes(
|
||||
schema: Schema,
|
||||
prettierOptions: PrettierOptions
|
||||
): string {
|
||||
const typedefs = [];
|
||||
|
||||
function formatField(field: FieldType): string {
|
||||
switch (field.type) {
|
||||
case "ID":
|
||||
return formatIdField(field);
|
||||
case "PRIMITIVE":
|
||||
return formatPrimitiveField(field);
|
||||
case "NODE":
|
||||
return formatNodeField(field);
|
||||
case "CONNECTION":
|
||||
return formatConnectionField(field);
|
||||
case "NESTED":
|
||||
return formatNestedField(field);
|
||||
// istanbul ignore next: unreachable per Flow
|
||||
default:
|
||||
throw new Error((field.type: empty));
|
||||
}
|
||||
}
|
||||
function formatIdField(_unused_field: IdFieldType): string {
|
||||
return "string";
|
||||
}
|
||||
function formatPrimitiveField(field: PrimitiveFieldType): string {
|
||||
if (field.annotation == null) {
|
||||
return "mixed";
|
||||
} else if (field.annotation.nonNull) {
|
||||
return field.annotation.elementType;
|
||||
} else {
|
||||
return "null | " + field.annotation.elementType;
|
||||
}
|
||||
}
|
||||
function formatNodeField(field: NodeFieldType): string {
|
||||
return "null | " + field.elementType;
|
||||
}
|
||||
function formatConnectionField(field: ConnectionFieldType): string {
|
||||
return `$ReadOnlyArray<null | ${field.elementType}>`;
|
||||
}
|
||||
function formatNestedField(field: NestedFieldType): string {
|
||||
const eggs = [];
|
||||
for (const eggName of Object.keys(field.eggs).sort()) {
|
||||
eggs.push({eggName, rhs: formatField(field.eggs[eggName])});
|
||||
}
|
||||
const eggContents = eggs.map((x) => `+${x.eggName}: ${x.rhs}`).join(", ");
|
||||
return `null | {|\n${eggContents}\n|}`;
|
||||
}
|
||||
|
||||
for (const typename of Object.keys(schema).sort()) {
|
||||
const type = schema[typename];
|
||||
switch (type.type) {
|
||||
case "SCALAR":
|
||||
typedefs.push(`export type ${typename} = ${type.representation};`);
|
||||
break;
|
||||
case "ENUM": {
|
||||
const rhs =
|
||||
Object.keys(type.values).length === 0
|
||||
? "empty"
|
||||
: Object.keys(type.values)
|
||||
.sort()
|
||||
.map((x) => JSON.stringify(x))
|
||||
.join(" | ");
|
||||
typedefs.push(`export type ${typename} = ${rhs};`);
|
||||
break;
|
||||
}
|
||||
case "OBJECT": {
|
||||
const fields = [
|
||||
{fieldname: "__typename", rhs: JSON.stringify(typename)},
|
||||
];
|
||||
for (const fieldname of Object.keys(type.fields).sort()) {
|
||||
fields.push({fieldname, rhs: formatField(type.fields[fieldname])});
|
||||
}
|
||||
const fieldContents = fields
|
||||
.map((x) => `+${x.fieldname}: ${x.rhs}`)
|
||||
.join(", ");
|
||||
const rhs = `{|\n${fieldContents}\n|}`;
|
||||
typedefs.push(`export type ${typename} = ${rhs};`);
|
||||
break;
|
||||
}
|
||||
case "UNION": {
|
||||
const rhs =
|
||||
Object.keys(type.clauses).length === 0
|
||||
? "empty"
|
||||
: Object.keys(type.clauses)
|
||||
.sort()
|
||||
.join(" | ");
|
||||
typedefs.push(`export type ${typename} = ${rhs};`);
|
||||
break;
|
||||
}
|
||||
// istanbul ignore next: unreachable per Flow
|
||||
default:
|
||||
throw new Error((type.type: empty));
|
||||
}
|
||||
}
|
||||
|
||||
const rawSource = [
|
||||
"// @flow",
|
||||
"// Autogenerated file. Do not edit.",
|
||||
...typedefs,
|
||||
].join("\n\n");
|
||||
return prettier.format(rawSource, prettierOptions);
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
// @flow
|
||||
|
||||
import generateFlowTypes from "./generateFlowTypes";
|
||||
import * as Schema from "./schema";
|
||||
|
||||
describe("graphql/generateFlowTypes", () => {
|
||||
function run(schema: Schema.Schema): string {
|
||||
return generateFlowTypes(schema, {parser: "babylon", trailingComma: "es5"});
|
||||
}
|
||||
|
||||
describe("generateFlowTypes", () => {
|
||||
it("works on a representative schema", () => {
|
||||
// This schema should be constructed as to singlehandedly achieve
|
||||
// full test coverage.
|
||||
const s = Schema;
|
||||
const schema = s.schema({
|
||||
String: s.scalar("string"),
|
||||
DateTime: s.scalar("string"),
|
||||
Dollars: s.scalar("number"),
|
||||
Color: s.enum(["RED", "GREEN", "BLUE"]),
|
||||
PaintJob: s.object({
|
||||
id: s.id(),
|
||||
metadata: s.primitive(/* unannotated */),
|
||||
cost: s.primitive(s.nonNull("Dollars")),
|
||||
completed: s.primitive(s.nullable("DateTime")),
|
||||
color: s.primitive(s.nonNull("Color")),
|
||||
designer: s.node("Actor"),
|
||||
painters: s.connection("Actor"),
|
||||
referrer: s.node("PaintJob"),
|
||||
relatedWork: s.connection("PaintJob"),
|
||||
details: s.nested({
|
||||
moreMetadata: s.primitive(),
|
||||
description: s.primitive(s.nullable("String")),
|
||||
oldColor: s.primitive(s.nonNull("Color")),
|
||||
oldPainter: s.node("Actor"),
|
||||
}),
|
||||
}),
|
||||
Actor: s.union(["Human", "TalentedChimpanzee"]),
|
||||
Human: s.object({
|
||||
id: s.id(),
|
||||
name: s.primitive(s.nullable("String")),
|
||||
}),
|
||||
TalentedChimpanzee: s.object({
|
||||
id: s.id(),
|
||||
name: s.primitive(s.nullable("String")),
|
||||
}),
|
||||
EmptyEnum: s.enum([]),
|
||||
EmptyUnion: s.union([]),
|
||||
});
|
||||
expect(run(schema)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("respects the Prettier options", () => {
|
||||
const s = Schema;
|
||||
const schema = s.schema({
|
||||
E: s.enum(["ONE", "TWO"]),
|
||||
});
|
||||
const options1 = {parser: "babylon", singleQuote: false};
|
||||
const options2 = {parser: "babylon", singleQuote: true};
|
||||
const output1 = generateFlowTypes(schema, options1);
|
||||
const output2 = generateFlowTypes(schema, options2);
|
||||
expect(output1).not.toEqual(output2);
|
||||
});
|
||||
|
||||
it("is invariant with respect to type definition order", () => {
|
||||
const s = Schema;
|
||||
const s1 = s.schema({
|
||||
A: s.object({id: s.id()}),
|
||||
B: s.object({id: s.id()}),
|
||||
});
|
||||
const s2 = s.schema({
|
||||
B: s.object({id: s.id()}),
|
||||
A: s.object({id: s.id()}),
|
||||
});
|
||||
expect(run(s1)).toEqual(run(s2));
|
||||
});
|
||||
|
||||
it("is invariant with respect to enum clause order", () => {
|
||||
const s = Schema;
|
||||
const s1 = s.schema({
|
||||
E: s.enum(["ONE", "TWO"]),
|
||||
});
|
||||
const s2 = s.schema({
|
||||
E: s.enum(["TWO", "ONE"]),
|
||||
});
|
||||
expect(run(s1)).toEqual(run(s2));
|
||||
});
|
||||
|
||||
it("is invariant with respect to object field order", () => {
|
||||
const s = Schema;
|
||||
const s1 = s.schema({
|
||||
O: s.object({
|
||||
id: s.id(),
|
||||
x: s.primitive(),
|
||||
y: s.primitive(),
|
||||
}),
|
||||
});
|
||||
const s2 = s.schema({
|
||||
O: s.object({
|
||||
id: s.id(),
|
||||
y: s.primitive(),
|
||||
x: s.primitive(),
|
||||
}),
|
||||
});
|
||||
expect(run(s1)).toEqual(run(s2));
|
||||
});
|
||||
|
||||
it("is invariant with respect to nested egg order", () => {
|
||||
const s = Schema;
|
||||
const s1 = s.schema({
|
||||
O: s.object({
|
||||
id: s.id(),
|
||||
nest: s.nested({
|
||||
x: s.primitive(),
|
||||
y: s.primitive(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const s2 = s.schema({
|
||||
O: s.object({
|
||||
id: s.id(),
|
||||
nest: s.nested({
|
||||
y: s.primitive(),
|
||||
x: s.primitive(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
expect(run(s1)).toEqual(run(s2));
|
||||
});
|
||||
|
||||
it("is invariant with respect to union clause order", () => {
|
||||
const s = Schema;
|
||||
const s1 = s.schema({
|
||||
A: s.object({id: s.id()}),
|
||||
B: s.object({id: s.id()}),
|
||||
U: s.union(["A", "B"]),
|
||||
});
|
||||
const s2 = s.schema({
|
||||
A: s.object({id: s.id()}),
|
||||
B: s.object({id: s.id()}),
|
||||
U: s.union(["B", "A"]),
|
||||
});
|
||||
expect(run(s1)).toEqual(run(s2));
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue