From c1fc750d87c3db14128763aeb8526c9317bb6049 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Sat, 4 Apr 2020 22:36:25 +0100 Subject: [PATCH 1/2] Whoa --- .DS_Store | Bin 0 -> 6148 bytes src/convert.test.ts | 189 +++++++++--------------- src/convert.ts | 222 +++++++++++++++------------- src/index.ts | 1 - src/types.ts | 24 +++ test/.DS_Store | Bin 0 -> 6148 bytes test/github-schema.graphql | 293 ++++++++++++++++++++++++++++++++++++- test/test-schema.graphql | 9 +- tsconfig.json | 2 +- 9 files changed, 511 insertions(+), 229 deletions(-) create mode 100644 .DS_Store create mode 100644 test/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bf2bf7843cf1ffcfd52a4764990f818af5706b8d GIT binary patch literal 6148 zcmeHK!A=4(5N%=Af-!P1kz+63H~TK(|F_! zBMv{cHa53}D2n1v@x~cMQzvngN!)hF zSJ*pqlYwXK^C%iFY}dO8`qOS{?>Gt*HwgPfNI*J#hQRbqe`_!$FuK1fhR+hC?q9Uai%^%2J{ zL=>>`ErBQv+6FU?FapALDxgl~=83^|I{2lDvkhh%bvol}WthjT%*_jhtJT3TRXF3e zMrw%xVqlqpyzZ8;{-1pR{$Ea_9x*@+{3`~y)p0s)*pjKOE1SbwD?xjpC>WP%T%>@Z hN-@M@DK3L50l!28&^DN91P=&Z1QZR_5Cgx;zz5OoP|^SZ literal 0 HcmV?d00001 diff --git a/src/convert.test.ts b/src/convert.test.ts index fcb65d9..535f371 100644 --- a/src/convert.test.ts +++ b/src/convert.test.ts @@ -1,131 +1,78 @@ -// import { arbitraryQuery } from "../output/output"; import * as Codegen from "./convert"; import * as Types from "./types"; -describe("includesOneOf", () => { - it("Makes any sense whatsoever", () => { - expect(Codegen.includesOneOf("dog", ["dog"])).toBeTruthy(); - }); - it("Works with multiple", () => { - expect(Codegen.includesOneOf("dog", ["log", "dog"])).toBeTruthy(); - }); - it("Works with multiple even with nonsense everywhere", () => { - expect(Codegen.includesOneOf("_____dog___", ["log", "dog"])).toBeTruthy(); - }); - it("Doesn't find something that's not there", () => { - expect(Codegen.includesOneOf("dog", ["log"])).toBeFalsy(); - }); -}); +const stable: Types.Output = { + kind: "Object", + name: "Stable" as Types.TypeName, + output: "fc.record({horse: arbitraryHorse })" as Types.Generated, + deps: ["Horse" as Types.TypeName] +}; +const horse: Types.Output = { + kind: "Object", + name: "Horse" as Types.TypeName, + output: "fc.record({saddle: arbitrarySaddle})" as Types.Generated, + deps: ["Saddle" as Types.TypeName] +}; +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: ["Stable" as Types.TypeName, "Saddle" as Types.TypeName] +}; describe("sortASTs", () => { it("Puts mentions later", () => { - 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 - ); - expect(Codegen.sortASTs([stable, horse, saddle, things])).toStrictEqual( - expectedOrder - ); - }); - - it("Does the twitter ones", () => { - 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, - Stat, - Tweet, - Meta, - Notification, - Query, - Mutation, - StatOrNotification, + const expectedOrder = [ + "Saddle" as Types.TypeName, + "Horse" as Types.TypeName, + "Stable" as Types.TypeName, + "Things" as Types.TypeName ]; expect( - Codegen.sortASTs([ - Query, - Tweet, - User, - Stat, - Meta, - Notification, - Mutation, - StatOrNotification, - ]) - ).toStrictEqual(expectedTweetOrder); + Codegen.sortASTs([things, stable, horse, saddle]).map(a => a.name) + ).toStrictEqual(expectedOrder); + expect( + Codegen.sortASTs([stable, horse, saddle, things]).map(a => a.name) + ).toStrictEqual(expectedOrder); + }); +}); + +describe("moveASTs", () => { + it("Moves everything with zero deps", () => { + const used: Types.Output[] = []; + const remaining = [stable, horse, saddle, things]; + const result = Codegen.moveASTs(used, remaining); + if (result._tag === "Right") { + expect(result.payload.used.length).toEqual(1); + expect(result.payload.remaining.length).toEqual(3); + } else { + throw "moveASTs failed"; + } + }); + it("Moves everything with deps that have already been moved across", () => { + const used = [saddle]; + const remaining = [horse, stable, things]; + const result = Codegen.moveASTs(used, remaining); + if (result._tag === "Right") { + expect(result.payload.used.length).toEqual(2); + expect(result.payload.remaining.length).toEqual(2); + } else { + throw "moveASTs failed"; + } + }); + it("Bruce forces it to a satifying conclusion", () => { + const remaining = [stable, horse, saddle, things]; + const result = Codegen.magicSort(remaining, 100); + if (result._tag === "Right") { + expect(result && result.payload.length).toEqual(4); + } else { + throw "fail!"; + } }); }); diff --git a/src/convert.ts b/src/convert.ts index de26fd3..2acd708 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -5,10 +5,20 @@ import { GraphQLNamedType, FieldDefinitionNode, TypeNode, - EnumValueDefinitionNode, + EnumValueDefinitionNode } from "graphql"; import * as fc from "fast-check"; -import { TypeName, Maybe, Output, Kind, Generated } from "./types"; +import { + TypeName, + Maybe, + Output, + Kind, + Generated, + Either, + left, + right, + caseEither +} from "./types"; const nodeName = (node: TypeDefinitionNode): TypeName => node ? (node.name.value as TypeName) : ("no node" as TypeName); @@ -30,7 +40,7 @@ const withFieldType = (typeNode: TypeNode): FieldReturn => { const internalType = withFieldType(typeNode.type); return { output: `fc.array(${internalType.output})` as Generated, - deps: internalType.deps, + deps: internalType.deps }; } else { // therefore typeNode.kind === 'NonNullType' @@ -44,13 +54,13 @@ const withField = (field: FieldDefinitionNode): FieldReturn => { const fieldType = withFieldType(field.type); return { output: `${fieldName}: ${fieldType.output}` as Generated, - deps: fieldType.deps, + deps: fieldType.deps }; }; const withEnum = (values: readonly EnumValueDefinitionNode[]): Generated => { const v = values - .map((value) => { + .map(value => { const val = value.name.value; return `fc.constant("${val}")`; }) @@ -60,15 +70,15 @@ const withEnum = (values: readonly EnumValueDefinitionNode[]): Generated => { const withUnion = (types: readonly NamedTypeNode[]): FieldReturn => { const fieldReturns = types.map(withNamedFieldType); - const output = fieldReturns.map((i) => i.output).join(", "); + const output = fieldReturns.map(i => i.output).join(", "); return { output: `fc.oneof(${output})` as Generated, - deps: flattenDeps(fieldReturns), + deps: flattenDeps(fieldReturns) }; }; const flattenDeps = (fieldReturns: FieldReturn[]): TypeName[] => - fieldReturns.map((i) => i.deps).reduce((as, a) => as.concat(a), []); + fieldReturns.map(i => i.deps).reduce((as, a) => as.concat(a), []); const withAstNode = (name: TypeName, node: TypeDefinitionNode): Output => { switch (node.kind) { @@ -77,35 +87,40 @@ const withAstNode = (name: TypeName, node: TypeDefinitionNode): Output => { kind: "Scalar", name, output: `fc.anything()` as Generated, - deps: [], + deps: [] }; case "EnumTypeDefinition": return { kind: "Enum", name, output: withEnum(node.values || []), - deps: [], + deps: [] }; case "InputObjectTypeDefinition": - // not yet supported - return { kind: "InputObject", name, output: "" as Generated, deps: [] }; + // this is awful + return { + kind: "InputObject", + name, + output: "fc.anything()" as Generated, + deps: [] + }; case "InterfaceTypeDefinition": const iFieldReturns = (node.fields || []).map(withField); - const iFieldOutput = iFieldReturns.map((i) => i.output).join(","); + const iFieldOutput = iFieldReturns.map(i => i.output).join(","); return { kind: "Interface", name, output: `fc.record({${iFieldOutput}})` as Generated, - deps: flattenDeps(iFieldReturns), + deps: flattenDeps(iFieldReturns) }; case "ObjectTypeDefinition": const oFieldReturns = (node.fields || []).map(withField); - const oFieldOutput = oFieldReturns.map((i) => i.output).join(","); + const oFieldOutput = oFieldReturns.map(i => i.output).join(","); return { kind: "Object", name, output: `fc.record({${oFieldOutput}})` as Generated, - deps: flattenDeps(oFieldReturns), + deps: flattenDeps(oFieldReturns) }; case "UnionTypeDefinition": const uFieldReturns = withUnion(node.types || []); @@ -114,7 +129,7 @@ const withAstNode = (name: TypeName, node: TypeDefinitionNode): Output => { kind: "Union", name, output: uFieldReturns.output, - deps: uFieldReturns.deps, + deps: uFieldReturns.deps }; } }; @@ -143,7 +158,7 @@ const getArbitraryName = (typeName: TypeName): 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; } @@ -163,7 +178,7 @@ const withNamedType = (item: GraphQLNamedType): Output | null => { kind: "Primitive", name: item.name as TypeName, output: prim, - deps: [], + deps: [] }; } else { return null; @@ -173,100 +188,107 @@ const withNamedType = (item: GraphQLNamedType): Output | null => { const notNull = (a: A | null): a is A => a !== null; -const byKind = (k: Kind) => (a: Output) => a.kind === k; +const removeKind = (k: Kind) => (a: Output) => a.kind !== k; const render = (val: Output) => { const { name, output } = val; return `export const ${getArbitraryName(name)} = ${output}`; }; -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")) +export const getSchemaDeclarations = (schema: GraphQLSchema): string => + sortASTs(getNamedTypes(schema).map(withNamedType).filter(notNull)) .map(render) - .join("\n"); + .join("\n\n"); - const scalars = namedTypes - .map(withNamedType) - //.map(logThen) - .filter(notNull) - .filter(byKind("Scalar")) - .map(render) - .join("\n"); - - const enums = namedTypes - .map(withNamedType) - //.map(logThen) - .filter(notNull) - .filter(byKind("Enum")) - .map(render) - .join("\n"); - - const unions = namedTypes - .map(withNamedType) - //.map(logThen) - .filter(notNull) - .filter(byKind("Union")); +const filterSplit = ( + as: A[], + f: (a: A) => boolean +): { yes: A[]; no: A[] } => ({ + yes: as.filter(f), + no: as.filter(a => !f(a)) +}); - const interfaces = namedTypes - .map(withNamedType) - //.map(logThen) - .filter(notNull) - .filter(byKind("Interface")); - - const objects = namedTypes - .map(withNamedType) - //.map(logThen) - .filter(notNull) - .filter(byKind("Object")); +export const sortASTs = (as: Output[]): Output[] => { + const limit = 10000; + return caseEither(magicSort(as, limit), { + onRight: payload => payload, + onLeft: err => { + throw err; + return []; + } + }); +}; - const mixed = sortASTs([...unions, ...objects, ...interfaces]) - .map(render) - .join("\n"); +const showProgress = (used: Output[], remaining: Output[]): void => { + const unresolved = [ + ...new Set(remaining.map(a => a.deps).reduce((as, a) => as.concat(a), [])) + ]; + const resolved = used.map(a => a.name); + console.log("RESOLVED", resolved); + console.log("UNRESOLVED", unresolved); +}; - return `${primitives}\n${enums}\n${scalars}\n${mixed}`; +export const magicSort = ( + as: Output[], + startingLimit: number +): Either => { + let limit = startingLimit; + let newRemaining = as; + let newUsed: Output[] = []; + let error = `Could not resolve ordering within ${startingLimit} tries`; + while (newRemaining.length > 0 && limit > 0) { + const succeeded = caseEither(moveASTs(newUsed, newRemaining), { + onRight: payload => { + newUsed = payload.used; + newRemaining = payload.remaining; + limit = limit - 1; + return true; + }, + onLeft: err => { + error = err; + return false; + } + }); + // showProgress(newUsed, newRemaining); + if (!succeeded) { + break; + } + } + if (newRemaining.length > 0) { + return left(error); + } + return right(newUsed); }; -const findableKeys = (key: TypeName) => [ - `${getArbitraryName(key)},`, - `${getArbitraryName(key)})`, - `${getArbitraryName(key)}}`, - `${getArbitraryName(key)} `, -]; +type ASTReturn = Either< + string, + { + used: Output[]; + remaining: Output[]; + } +>; -export const includesOneOf = (val: string, opts: string[]): boolean => - opts - .map((opt) => val.includes(opt)) - .reduce((total, value) => total || value, false); +export const moveASTs = (used: Output[], remaining: Output[]): ASTReturn => { + // remove everything in used from deps list + const usedDeps = used.map(a => a.name); -// if one object mentions another definition, put it after that definition -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; - } + const remainingFiltered = remaining.map(a => ({ + ...a, + deps: a.deps.filter(dep => !usedDeps.includes(dep)) + })); + + // move everything with empty deps list left + const { yes: moved, no: keep } = filterSplit( + remainingFiltered, + a => a.deps.length === 0 ); + + // done + const newUsed = [...used, ...moved]; + if (newUsed.length === used.length) { + return left( + `No changes made, unresolvable. ${newUsed.length} moved, ${keep.length} remaining to move.` + ); + } + return right({ used: newUsed, remaining: keep }); +}; diff --git a/src/index.ts b/src/index.ts index 381d40f..756b829 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ module.exports = { plugin: (schema: GraphQLSchema, documents: any, config: any) => { const declarations = getSchemaDeclarations(schema); const output = `import * as fc from 'fast-check'\n${declarations}`; - return output; return format(output, { parser: "typescript" }); diff --git a/src/types.ts b/src/types.ts index ddb9580..cf0190d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,30 @@ export type Maybe = null | undefined | T; /// +export type Either = + | { _tag: "Left"; error: E } + | { _tag: "Right"; payload: A }; + +export const left = (error: E): Either => ({ + _tag: "Left", + error +}); + +export const right = (payload: A): Either => ({ + _tag: "Right", + payload +}); + +export const caseEither = ( + either: Either, + cases: { onLeft: (e: E) => B; onRight: (a: A) => B } +): B => + either._tag === "Left" + ? cases.onLeft(either.error) + : cases.onRight(either.payload); + +/// + type Nominal = { readonly symbol: A; }; diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Sat, 4 Apr 2020 22:37:38 +0100 Subject: [PATCH 2/2] Nice --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 1 + test/.DS_Store | Bin 6148 -> 0 bytes 3 files changed, 1 insertion(+) delete mode 100644 .DS_Store delete mode 100644 test/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index bf2bf7843cf1ffcfd52a4764990f818af5706b8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!A=4(5N%=Af-!P1kz+63H~TK(|F_! zBMv{cHa53}D2n1v@x~cMQzvngN!)hF zSJ*pqlYwXK^C%iFY}dO8`qOS{?>Gt*HwgPfNI*J#hQRbqe`_!$FuK1fhR+hC?q9Uai%^%2J{ zL=>>`ErBQv+6FU?FapALDxgl~=83^|I{2lDvkhh%bvol}WthjT%*_jhtJT3TRXF3e zMrw%xVqlqpyzZ8;{-1pR{$Ea_9x*@+{3`~y)p0s)*pjKOE1SbwD?xjpC>WP%T%>@Z hN-@M@DK3L50l!28&^DN91P=&Z1QZR_5Cgx;zz5OoP|^SZ diff --git a/.gitignore b/.gitignore index 955ed25..bffcf95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store yarn-error.log dist/ output/*.ts diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0