From 9e6f334d27c801e33fcface749dbe0866efd5e6a Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Sat, 4 Apr 2020 12:33:59 +0100 Subject: [PATCH] Track deps --- src/convert.test.ts | 140 +++++++++++++++++-------------- src/convert.ts | 200 +++++++++++++++++++++++++++----------------- src/types.ts | 34 ++++++++ 3 files changed, 236 insertions(+), 138 deletions(-) create mode 100644 src/types.ts diff --git a/src/convert.test.ts b/src/convert.test.ts index ae805f5..fcb65d9 100644 --- a/src/convert.test.ts +++ b/src/convert.test.ts @@ -1,5 +1,6 @@ // import { arbitraryQuery } from "../output/output"; import * as Codegen from "./convert"; +import * as Types from "./types"; describe("includesOneOf", () => { it("Makes any sense whatsoever", () => { @@ -18,26 +19,30 @@ describe("includesOneOf", () => { describe("sortASTs", () => { it("Puts mentions later", () => { - const stable: Codegen.NamedType = [ - "Object", - "Stable" as Codegen.TypeName, - "fc.record({horse: arbitraryHorse })" - ]; - const horse: Codegen.NamedType = [ - "Object", - "Horse" as Codegen.TypeName, - "fc.record({saddle: arbitrarySaddle})" - ]; - const saddle: Codegen.NamedType = [ - "Object", - "Saddle" as Codegen.TypeName, - "fc.record({chair: arbitraryChair})" - ]; - const things: Codegen.NamedType = [ - "Union", - "Things" as Codegen.TypeName, - "fc.oneof(arbitraryStable, arbitrarySaddle)" - ]; + const stable: Types.Output = { + kind: "Object", + name: "Stable" as Types.TypeName, + output: "fc.record({horse: arbitraryHorse })" as Types.Generated, + deps: [], + }; + const horse: Types.Output = { + kind: "Object", + name: "Horse" as Types.TypeName, + output: "fc.record({saddle: arbitrarySaddle})" as Types.Generated, + deps: [], + }; + const saddle: Types.Output = { + kind: "Object", + name: "Saddle" as Types.TypeName, + output: "fc.record({chair: arbitraryChair})" as Types.Generated, + deps: [], + }; + const things: Types.Output = { + kind: "Union", + name: "Things" as Types.TypeName, + output: "fc.oneof(arbitraryStable, arbitrarySaddle)" as Types.Generated, + deps: [], + }; const expectedOrder = [saddle, horse, stable, things]; expect(Codegen.sortASTs([things, stable, horse, saddle])).toStrictEqual( expectedOrder @@ -48,46 +53,57 @@ describe("sortASTs", () => { }); it("Does the twitter ones", () => { - const Query: Codegen.NamedType = [ - "Object", - "Query" as Codegen.TypeName, - `fc.record({Tweet: arbitraryTweet,Tweets: fc.array(arbitraryTweet),TweetsMeta: arbitraryMeta,User: arbitraryUser,Notifications: fc.array(arbitraryNotification),NotificationsMeta: arbitraryMeta})` - ]; - const Tweet: Codegen.NamedType = [ - "Object", - "Tweet" as Codegen.TypeName, - `fc.record({id: arbitraryID,body: arbitraryString,date: arbitraryDate,Author: arbitraryUser,Stats: arbitraryStat})` - ]; - const User: Codegen.NamedType = [ - "Object", - "User" as Codegen.TypeName, - `fc.record({id: arbitraryID,username: arbitraryString,first_name: arbitraryString,last_name: arbitraryString,full_name: arbitraryString,name: arbitraryString,avatar_url: arbitraryUrl})` - ]; - const Stat: Codegen.NamedType = [ - "Object", - "Stat" as Codegen.TypeName, - `fc.record({views: arbitraryInt,likes: arbitraryInt,retweets: arbitraryInt,responses: arbitraryInt})` - ]; - const Meta: Codegen.NamedType = [ - "Object", - "Meta" as Codegen.TypeName, - `fc.record({count: arbitraryInt})` - ]; - const Notification: Codegen.NamedType = [ - "Object", - "Notification" as Codegen.TypeName, - `fc.record({id: arbitraryID,date: arbitraryDate,type: arbitraryString})` - ]; - const Mutation: Codegen.NamedType = [ - "Object", - "Mutation" as Codegen.TypeName, - `fc.record({createTweet: arbitraryTweet,deleteTweet: arbitraryTweet,markTweetRead: arbitraryBoolean})` - ]; - const StatOrNotification: Codegen.NamedType = [ - "Union", - "StatOrNotification" as Codegen.TypeName, - `fc.oneof(arbitraryStat, arbitraryNotification)` - ]; + const Query: Types.Output = { + kind: "Object", + name: "Query" as Types.TypeName, + output: `fc.record({Tweet: arbitraryTweet,Tweets: fc.array(arbitraryTweet),TweetsMeta: arbitraryMeta,User: arbitraryUser,Notifications: fc.array(arbitraryNotification),NotificationsMeta: arbitraryMeta})` as Types.Generated, + deps: [], + }; + const Tweet: Types.Output = { + kind: "Object", + name: "Tweet" as Types.TypeName, + output: `fc.record({id: arbitraryID,body: arbitraryString,date: arbitraryDate,Author: arbitraryUser,Stats: arbitraryStat})` as Types.Generated, + deps: [], + }; + const User: Types.Output = { + kind: "Object", + name: "User" as Types.TypeName, + output: `fc.record({id: arbitraryID,username: arbitraryString,first_name: arbitraryString,last_name: arbitraryString,full_name: arbitraryString,name: arbitraryString,avatar_url: arbitraryUrl})` as Types.Generated, + deps: [], + }; + const Stat: Types.Output = { + kind: "Object", + name: "Stat" as Types.TypeName, + output: `fc.record({views: arbitraryInt,likes: arbitraryInt,retweets: arbitraryInt,responses: arbitraryInt})` as Types.Generated, + deps: [], + }; + const Meta: Types.Output = { + kind: "Object", + name: "Meta" as Types.TypeName, + output: `fc.record({count: arbitraryInt})` as Types.Generated, + deps: [], + }; + + const Notification: Types.Output = { + kind: "Object", + name: "Notification" as Types.TypeName, + output: `fc.record({id: arbitraryID,date: arbitraryDate,type: arbitraryString})` as Types.Generated, + deps: [], + }; + + const Mutation: Types.Output = { + kind: "Object", + name: "Mutation" as Types.TypeName, + output: `fc.record({createTweet: arbitraryTweet,deleteTweet: arbitraryTweet,markTweetRead: arbitraryBoolean})` as Types.Generated, + deps: [], + }; + + const StatOrNotification: Types.Output = { + kind: "Union", + name: "StatOrNotification" as Types.TypeName, + output: `fc.oneof(arbitraryStat, arbitraryNotification)` as Types.Generated, + deps: [], + }; const expectedTweetOrder = [ User, @@ -97,7 +113,7 @@ describe("sortASTs", () => { Notification, Query, Mutation, - StatOrNotification + StatOrNotification, ]; expect( Codegen.sortASTs([ @@ -108,7 +124,7 @@ describe("sortASTs", () => { Meta, Notification, Mutation, - StatOrNotification + StatOrNotification, ]) ).toStrictEqual(expectedTweetOrder); }); diff --git a/src/convert.ts b/src/convert.ts index f0859ad..de26fd3 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -5,33 +5,33 @@ import { GraphQLNamedType, FieldDefinitionNode, TypeNode, - EnumValueDefinitionNode + EnumValueDefinitionNode, } from "graphql"; import * as fc from "fast-check"; - -type Maybe = null | undefined | T; - -type Nominal = { - readonly symbol: A; -}; - -type Newtype = Nominal & A; - -export type TypeName = Newtype<"Typename", string>; +import { TypeName, Maybe, Output, Kind, Generated } from "./types"; const nodeName = (node: TypeDefinitionNode): TypeName => node ? (node.name.value as TypeName) : ("no node" as TypeName); -const withNamedFieldType = (namedTypeNode: NamedTypeNode): string => { - return getArbitraryName(namedTypeNode.name.value as TypeName); +type FieldReturn = { + output: Generated; + deps: TypeName[]; +}; + +const withNamedFieldType = (namedTypeNode: NamedTypeNode): FieldReturn => { + const name = namedTypeNode.name.value as TypeName; + return { output: getArbitraryName(name), deps: [name] }; }; -const withFieldType = (typeNode: TypeNode): string => { +const withFieldType = (typeNode: TypeNode): FieldReturn => { if (typeNode.kind === "NamedType") { return withNamedFieldType(typeNode); } else if (typeNode.kind === "ListType") { const internalType = withFieldType(typeNode.type); - return `fc.array(${internalType})`; + return { + output: `fc.array(${internalType.output})` as Generated, + deps: internalType.deps, + }; } else { // therefore typeNode.kind === 'NonNullType' // probably need to make a custom Arbitrary for filtering out null values (and add them to all the primitive values by default, ffs) @@ -39,81 +39,111 @@ const withFieldType = (typeNode: TypeNode): string => { } }; -const withField = (field: FieldDefinitionNode): string => { +const withField = (field: FieldDefinitionNode): FieldReturn => { const fieldName = field.name.value; const fieldType = withFieldType(field.type); - return `${fieldName}: ${fieldType}`; + return { + output: `${fieldName}: ${fieldType.output}` as Generated, + deps: fieldType.deps, + }; }; -const withEnum = (values: readonly EnumValueDefinitionNode[]) => { +const withEnum = (values: readonly EnumValueDefinitionNode[]): Generated => { const v = values - .map(value => { + .map((value) => { const val = value.name.value; return `fc.constant("${val}")`; }) .join(", "); - return `fc.oneof(${v})`; + return `fc.oneof(${v})` as Generated; }; -const withUnion = (types: readonly NamedTypeNode[]): string => { - const nodes = types.map(withNamedFieldType).join(", "); - return `fc.oneof(${nodes})`; +const withUnion = (types: readonly NamedTypeNode[]): FieldReturn => { + const fieldReturns = types.map(withNamedFieldType); + const output = fieldReturns.map((i) => i.output).join(", "); + return { + output: `fc.oneof(${output})` as Generated, + deps: flattenDeps(fieldReturns), + }; }; -const withAstNode = (node: TypeDefinitionNode): [Kind, string] => { +const flattenDeps = (fieldReturns: FieldReturn[]): TypeName[] => + fieldReturns.map((i) => i.deps).reduce((as, a) => as.concat(a), []); + +const withAstNode = (name: TypeName, node: TypeDefinitionNode): Output => { switch (node.kind) { case "ScalarTypeDefinition": - return ["Scalar", `fc.anything()`]; + return { + kind: "Scalar", + name, + output: `fc.anything()` as Generated, + deps: [], + }; case "EnumTypeDefinition": - return ["Enum", withEnum(node.values || [])]; + return { + kind: "Enum", + name, + output: withEnum(node.values || []), + deps: [], + }; case "InputObjectTypeDefinition": // not yet supported - return ["InputObject", ""]; + return { kind: "InputObject", name, output: "" as Generated, deps: [] }; case "InterfaceTypeDefinition": - const iFields = (node.fields || []).map(withField); - return ["Interface", `fc.record({${iFields.join(",")}})`]; + const iFieldReturns = (node.fields || []).map(withField); + const iFieldOutput = iFieldReturns.map((i) => i.output).join(","); + return { + kind: "Interface", + name, + output: `fc.record({${iFieldOutput}})` as Generated, + deps: flattenDeps(iFieldReturns), + }; case "ObjectTypeDefinition": - const oFields = (node.fields || []).map(withField); - return ["Object", `fc.record({${oFields.join(",")}})`]; + const oFieldReturns = (node.fields || []).map(withField); + const oFieldOutput = oFieldReturns.map((i) => i.output).join(","); + return { + kind: "Object", + name, + output: `fc.record({${oFieldOutput}})` as Generated, + deps: flattenDeps(oFieldReturns), + }; case "UnionTypeDefinition": - return ["Union", withUnion(node.types || [])]; + const uFieldReturns = withUnion(node.types || []); + + return { + kind: "Union", + name, + output: uFieldReturns.output, + deps: uFieldReturns.deps, + }; } }; -const withPrimitive = (node: GraphQLNamedType): string | null => { +const withPrimitive = (node: GraphQLNamedType): Generated | null => { switch (node.name) { case "Int": - return `fc.integer()`; + return `fc.integer()` as Generated; case "Boolean": - return `fc.boolean()`; + return `fc.boolean()` as Generated; case "String": - return "fc.string()"; + return "fc.string()" as Generated; case "ID": - return `fc.string()`; + return `fc.string()` as Generated; case "Float": - return `fc.float()`; + return `fc.float()` as Generated; } // console.log(`Primitive ${node.name} not found`); return null; }; -const getArbitraryName = (typeName: TypeName): string => `arbitrary${typeName}`; - -export type Kind = - | "Object" - | "Scalar" - | "Primitive" - | "Enum" - | "Union" - | "InputObject" - | "Interface"; -export type NamedType = [Kind, TypeName, string]; +const getArbitraryName = (typeName: TypeName): Generated => + `arbitrary${typeName}` as Generated; const getNamedTypes = (schema: GraphQLSchema): GraphQLNamedType[] => { const typesMap = schema.getTypeMap(); const namedTypes: Maybe[] = Object.keys(typesMap).map( - key => { + (key) => { const item = schema.getType(key); return item || null; } @@ -123,14 +153,18 @@ const getNamedTypes = (schema: GraphQLSchema): GraphQLNamedType[] => { ) as GraphQLNamedType[]; }; -const withNamedType = (item: GraphQLNamedType): NamedType | null => { +const withNamedType = (item: GraphQLNamedType): Output | null => { if (item.astNode) { - const [nodeKind, str] = withAstNode(item.astNode); - return [nodeKind, nodeName(item.astNode), str]; + return withAstNode(nodeName(item.astNode), item.astNode); } else { const prim = withPrimitive(item); if (prim) { - return ["Primitive", item.name as TypeName, prim]; + return { + kind: "Primitive", + name: item.name as TypeName, + output: prim, + deps: [], + }; } else { return null; } @@ -139,17 +173,24 @@ const withNamedType = (item: GraphQLNamedType): NamedType | null => { const notNull = (a: A | null): a is A => a !== null; -const byKind = (k: Kind) => (a: NamedType) => a[0] === k; +const byKind = (k: Kind) => (a: Output) => a.kind === k; + +const render = (val: Output) => { + const { name, output } = val; + return `export const ${getArbitraryName(name)} = ${output}`; +}; -const render = (val: NamedType) => { - const [_, variable, definition] = val; - return `export const ${getArbitraryName(variable)} = ${definition}`; +const logThen = (a: A): A => { + console.log(a); + return a; }; export const getSchemaDeclarations = (schema: GraphQLSchema): string => { const namedTypes = getNamedTypes(schema); + const primitives = namedTypes .map(withNamedType) + //.map(logThen) .filter(notNull) .filter(byKind("Primitive")) .map(render) @@ -157,6 +198,7 @@ export const getSchemaDeclarations = (schema: GraphQLSchema): string => { const scalars = namedTypes .map(withNamedType) + //.map(logThen) .filter(notNull) .filter(byKind("Scalar")) .map(render) @@ -164,6 +206,7 @@ export const getSchemaDeclarations = (schema: GraphQLSchema): string => { const enums = namedTypes .map(withNamedType) + //.map(logThen) .filter(notNull) .filter(byKind("Enum")) .map(render) @@ -171,16 +214,19 @@ export const getSchemaDeclarations = (schema: GraphQLSchema): string => { const unions = namedTypes .map(withNamedType) + //.map(logThen) .filter(notNull) .filter(byKind("Union")); const interfaces = namedTypes .map(withNamedType) + //.map(logThen) .filter(notNull) .filter(byKind("Interface")); const objects = namedTypes .map(withNamedType) + //.map(logThen) .filter(notNull) .filter(byKind("Object")); @@ -195,30 +241,32 @@ const findableKeys = (key: TypeName) => [ `${getArbitraryName(key)},`, `${getArbitraryName(key)})`, `${getArbitraryName(key)}}`, - `${getArbitraryName(key)} ` + `${getArbitraryName(key)} `, ]; export const includesOneOf = (val: string, opts: string[]): boolean => opts - .map(opt => val.includes(opt)) + .map((opt) => val.includes(opt)) .reduce((total, value) => total || value, false); // if one object mentions another definition, put it after that definition -export const sortASTs = (as: NamedType[]): NamedType[] => - [...as].sort(([_a, key, val], [_b, newKey, newVal]) => { - // console.log(`${_a}: ${key}: ${val}`); - // console.log(`${_b}: ${newKey}: ${newVal}`); - const leftMentionsRight = includesOneOf(newVal, findableKeys(key)); - const rightMentionsLeft = includesOneOf(val, findableKeys(newKey)); - if (leftMentionsRight) { - // console.log(`moving ${key} left to avoid ${newKey}:${newVal}`); - return -1; // move left before right - } else if (rightMentionsLeft) { - // console.log(`${newKey} moves left to go before ${key}:${val}`); - return 1; // move right before left - } - /*console.log( +export const sortASTs = (as: Output[]): Output[] => + [...as].sort( + ({ name: key, output: val }, { name: newKey, output: newVal }) => { + // console.log(`${_a}: ${key}: ${val}`); + // console.log(`${_b}: ${newKey}: ${newVal}`); + const leftMentionsRight = includesOneOf(newVal, findableKeys(key)); + const rightMentionsLeft = includesOneOf(val, findableKeys(newKey)); + if (leftMentionsRight) { + // console.log(`moving ${key} left to avoid ${newKey}:${newVal}`); + return -1; // move left before right + } else if (rightMentionsLeft) { + // console.log(`${newKey} moves left to go before ${key}:${val}`); + return 1; // move right before left + } + /*console.log( `Not moving ${key} because it's not found in ${newKey}:${newVal}` );*/ - return 0; - }); + return 0; + } + ); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ddb9580 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,34 @@ +export type Maybe = null | undefined | T; + +/// + +type Nominal = { + readonly symbol: A; +}; + +type Newtype = Nominal & A; + +/// + +export type TypeName = Newtype<"Typename", string>; +export type Generated = Newtype<"Generated", string>; + +//// + +export type Kind = + | "Object" + | "Scalar" + | "Primitive" + | "Enum" + | "Union" + | "InputObject" + | "Interface"; + +/// + +export type Output = { + kind: Kind; + name: TypeName; + output: Generated; + deps: TypeName[]; +};