diff --git a/.changeset/cruel-geese-buy.md b/.changeset/cruel-geese-buy.md new file mode 100644 index 00000000..ba12aac2 --- /dev/null +++ b/.changeset/cruel-geese-buy.md @@ -0,0 +1,7 @@ +--- +"@traversable/graphql-types": patch +"@traversable/graphql-test": patch +"@traversable/graphql": patch +--- + +feat(graphql): adds `graphql` to `dependencies` diff --git a/.changeset/fresh-years-lay.md b/.changeset/fresh-years-lay.md new file mode 100644 index 00000000..98008b9a --- /dev/null +++ b/.changeset/fresh-years-lay.md @@ -0,0 +1,7 @@ +--- +"@traversable/graphql-types": patch +"@traversable/graphql-test": patch +"@traversable/graphql": patch +--- + +initializes `@traversable/graphql`, `@traverable/graphql-test`, `@traversable/graphql-types` packages diff --git a/bin/workspace.ts b/bin/workspace.ts index c554938f..1a80850c 100755 --- a/bin/workspace.ts +++ b/bin/workspace.ts @@ -430,6 +430,7 @@ namespace write { ($) => pipe( [ `export * from './exports.js'`, + null ].join('\n'), $.dryRun ? tap(`\n\n[CREATE #10]: workspaceIndex\n`, globalThis.String) : fs.writeString(path.join(PATH.packages, $.pkgName, 'src', 'index.ts')), @@ -443,7 +444,8 @@ namespace write { export const workspaceSrcExports = defineEffect( ($) => pipe( [ - `export * from './version.js'`, + `export { VERSION } from './version.js'`, + null ].join('\n'), $.dryRun ? tap(`\n\n[CREATE #11]: workspaceSrcExports\n`, globalThis.String) : fs.writeString(path.join(PATH.packages, $.pkgName, 'src', 'exports.ts')), diff --git a/config/__generated__/package-list.ts b/config/__generated__/package-list.ts index 9aee010d..dee58135 100644 --- a/config/__generated__/package-list.ts +++ b/config/__generated__/package-list.ts @@ -2,6 +2,9 @@ export const PACKAGES = [ "packages/arktype", "packages/arktype-test", "packages/arktype-types", + "packages/graphql", + "packages/graphql-test", + "packages/graphql-types", "packages/json", "packages/json-schema", "packages/json-schema-test", diff --git a/package.json b/package.json index 00aee449..915f0336 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "fast-check": "catalog:", "madge": "catalog:", "tinybench": "catalog:", - "typedoc": "^0.28.9", + "typedoc": "catalog:", "typescript": "catalog:", "vitest": "catalog:" }, diff --git a/packages/arktype-test/src/generator-bounds.ts b/packages/arktype-test/src/generator-bounds.ts index ea2049bf..b5ef548e 100644 --- a/packages/arktype-test/src/generator-bounds.ts +++ b/packages/arktype-test/src/generator-bounds.ts @@ -1,6 +1,5 @@ import * as fc from 'fast-check' -import type { newtype } from '@traversable/registry' import { fn, Number_isFinite, Number_isNatural, Object_is } from '@traversable/registry' /** @internal */ @@ -17,8 +16,8 @@ export const defaultDoubleConstraints = { // const defaultIntBounds = [-0x1000, +0x1000, null] satisfies Bounds_int const defaultBigIntBounds = [-0x1000000n, 0x1000000n, null] satisfies Bounds_bigint const defaultNumberBounds = [-0x10000, +0x10000, null, false, false] satisfies Bounds_number -const defaultStringBounds = [0, +0x40] satisfies Bounds_string -const defaultArrayBounds = [0, +0x10] satisfies Bounds_array +const defaultStringBounds = [0, +0x40, null] satisfies Bounds_string +const defaultArrayBounds = [0, +0x10, null] satisfies Bounds_array export const defaults = { // int: defaultIntBounds, @@ -68,11 +67,11 @@ const clampArrayMax = clampMax(defaults.array[0], defaults.array[1], Number_isNa export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: model, maximum: model }) // export { Bounds_int as int } -// interface Bounds_int extends newtype<[ +// type Bounds_int = [ // minimum: number | null, // maximum: number | null, // multipleOf: number | null, -// ]> {} +// ] // const Bounds_int // : (model: fc.Arbitrary) => fc.Arbitrary @@ -84,11 +83,11 @@ export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: mo // ]) export { Bounds_bigint as bigint } -interface Bounds_bigint extends newtype<[ +type Bounds_bigint = [ minimum: bigint | null, maximum: bigint | null, multipleOf: bigint | null, -]> {} +] const Bounds_bigint : (model: fc.Arbitrary) => fc.Arbitrary @@ -99,28 +98,28 @@ const Bounds_bigint ]) export { Bounds_string as string } -interface Bounds_string extends newtype<[ +type Bounds_string = [ minLength: number | null, maxLength: number | null, -]> {} + exactLength: number | null, +] const Bounds_string : (model: fc.Arbitrary) => fc.Arbitrary = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map( ([x, y, length]) => Number_isNatural(length) - ? [null, null] satisfies [any, any] - // [clampString(length), clampString(length)] - : [clampStringMin(x, y), clampStringMax(y, x)] + ? [null, null, null] satisfies Bounds_string + : [clampStringMin(x, y), clampStringMax(y, x), null] satisfies Bounds_string ) export { Bounds_number as number } -interface Bounds_number extends newtype<[ +type Bounds_number = [ minimum: number | null, maximum: number | null, multipleOf: number | null, exclusiveMinimum: boolean, exclusiveMaximum: boolean, -]> {} +] const deltaIsSubEpsilon = (x: number, y: number) => Math.abs(x - y) < Number.EPSILON @@ -153,10 +152,11 @@ const Bounds_number ) export { Bounds_array as array } -interface Bounds_array extends newtype<[ +type Bounds_array = [ minLength: number | null, maxLength: number | null, -]> {} + exactLength: number | null, +] const Bounds_array : (model: fc.Arbitrary) => fc.Arbitrary @@ -166,8 +166,8 @@ const Bounds_array fc.constant(null) ).map(([x, y, exactLength]) => Number_isNatural(exactLength) - ? [null, null] - : [clampArrayMin(x, y), clampArrayMax(y, x)] + ? [null, null, exactLength] satisfies Bounds_array + : [clampArrayMin(x, y), clampArrayMax(y, x), null] satisfies Bounds_array ) @@ -184,7 +184,7 @@ const Bounds_array export const bigintBoundsToBigIntConstraints : (bounds?: Bounds_bigint) => fc.BigIntConstraints = (bounds = defaultBigIntBounds) => { - const [min, max, multipleOf] = bounds + const [min, max, _multipleOf] = bounds return { max: max ?? void 0, min: min ?? void 0, @@ -207,7 +207,7 @@ export const numberBoundsToDoubleConstraints export const stringBoundsToStringConstraints : (bounds?: Bounds_string) => fc.StringConstraints - = ([minLength, maxLength] = defaultStringBounds) => ({ + = ([minLength, maxLength, exactLength] = defaultStringBounds) => ({ minLength: minLength ?? void 0, maxLength: maxLength ?? void 0 }) satisfies fc.StringConstraints diff --git a/packages/arktype-test/src/generator.ts b/packages/arktype-test/src/generator.ts index 58c260e3..caabe698 100644 --- a/packages/arktype-test/src/generator.ts +++ b/packages/arktype-test/src/generator.ts @@ -1,7 +1,7 @@ import { type } from 'arktype' import * as fc from 'fast-check' -import type { newtype, inline } from '@traversable/registry' +import type { inline } from '@traversable/registry' import { Array_isArray, fn, @@ -120,7 +120,7 @@ export interface SeedBuilder { (tie: fc.LetrecTypedTie, $: Config.byTypeName[K]): fc.Arbitrary } -export interface SeedMap extends newtype<{ [K in keyof Seed]: SeedBuilder }> {} +export type SeedMap = { [K in keyof Seed]: SeedBuilder } export const SeedMap = { ...TerminalMap, ...BoundableMap, @@ -262,7 +262,8 @@ export declare namespace Gen { type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary } type Values = never | T[Exclude] type InferArb = S extends fc.Arbitrary ? T : never - interface Builder extends newtype { ['*']: fc.Arbitrary>> } + type Builder = T & BuilderStar + interface BuilderStar { ['*']: fc.Arbitrary>> } type BuildBuilder, Out extends {} = BuilderBase> = never | Builder type BuilderBase, $ extends ParseOptions = ParseOptions> = never | & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) diff --git a/packages/graphql-test/README.md b/packages/graphql-test/README.md new file mode 100644 index 00000000..fcbae809 --- /dev/null +++ b/packages/graphql-test/README.md @@ -0,0 +1,40 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝗴𝗿𝗮𝗽𝗵𝗾𝗹-𝘁𝗲𝘀𝘁

+
+ +

+ TODO: write me +

+ +
+ NPM Version +   + TypeScript +   + License +   + npm +   +
+ +
+ + Static Badge +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
diff --git a/packages/graphql-test/package.json b/packages/graphql-test/package.json new file mode 100644 index 00000000..b424e938 --- /dev/null +++ b/packages/graphql-test/package.json @@ -0,0 +1,65 @@ +{ + "name": "@traversable/graphql-test", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/graphql-test" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": [ + "**/*.ts" + ] + }, + "generateIndex": { + "include": [ + "**/*.ts" + ] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/graphql-types": "workspace:^", + "@traversable/json": "workspace:^", + "@traversable/registry": "workspace:^", + "graphql": ">= 16" + }, + "peerDependenciesMeta": { + "@traversable/graphql-types": { "optional": false }, + "@traversable/json": { "optional": false }, + "@traversable/registry": { "optional": false }, + "graphql": { "optional": true } + }, + "devDependencies": { + "@prettier/sync": "catalog:", + "@traversable/graphql-types": "workspace:^", + "@traversable/json": "workspace:^", + "@traversable/registry": "workspace:^", + "graphql": "catalog:" + } +} diff --git a/packages/graphql-test/src/__generated__/__manifest__.ts b/packages/graphql-test/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..823f5593 --- /dev/null +++ b/packages/graphql-test/src/__generated__/__manifest__.ts @@ -0,0 +1,61 @@ +export default { + "name": "@traversable/graphql-test", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/graphql-test" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": ["**/*.ts"] + }, + "generateIndex": { + "include": ["**/*.ts"] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/graphql-types": "workspace:^", + "@traversable/json": "workspace:^", + "@traversable/registry": "workspace:^", + "graphql": ">= 16" + }, + "peerDependenciesMeta": { + "@traversable/graphql-types": { "optional": false }, + "@traversable/json": { "optional": false }, + "@traversable/registry": { "optional": false }, + "graphql": { "optional": true } + }, + "devDependencies": { + "@prettier/sync": "catalog:", + "@traversable/graphql-types": "workspace:^", + "@traversable/json": "workspace:^", + "@traversable/registry": "workspace:^", + "graphql": "catalog:" + } +} as const \ No newline at end of file diff --git a/packages/graphql-test/src/exports.ts b/packages/graphql-test/src/exports.ts new file mode 100644 index 00000000..7e30d4e3 --- /dev/null +++ b/packages/graphql-test/src/exports.ts @@ -0,0 +1,11 @@ +export { VERSION } from './version.js' + +export { Config } from './generator-options.js' +export * from './generator-seed.js' +export { + Builder, + Gen, + SeedGenerator, + pickAndSortNodes, + seedToSchema, +} from './generator.js' diff --git a/packages/graphql-test/src/functor.ts b/packages/graphql-test/src/functor.ts new file mode 100644 index 00000000..defa9654 --- /dev/null +++ b/packages/graphql-test/src/functor.ts @@ -0,0 +1,118 @@ +import type * as T from '@traversable/registry' +import { fn } from '@traversable/registry' + +import type { Seed } from './generator-seed.js' +import { byTag } from './generator-seed.js' + +const isNull = (x: unknown) => x === null + +export interface Index {} + +export const defaultIndex = Object.create(null) satisfies Index + +export const Functor: T.Functor.Ix = { + map(g) { + return (x) => { + switch (true) { + default: return x satisfies never + case x[0] === byTag.Name: return x + case x[0] === byTag.Boolean: return x + case x[0] === byTag.BooleanValue: return x + case x[0] === byTag.EnumValue: return x + case x[0] === byTag.EnumValueDefinition: return x + case x[0] === byTag.Float: return x + case x[0] === byTag.FloatValue: return x + case x[0] === byTag.ID: return x + case x[0] === byTag.Int: return x + case x[0] === byTag.IntValue: return x + case x[0] === byTag.NamedType: return x + case x[0] === byTag.Null: return x + case x[0] === byTag.NullValue: return x + case x[0] === byTag.Number: return x + case x[0] === byTag.ScalarTypeDefinition: return x + case x[0] === byTag.String: return x + case x[0] === byTag.StringValue: return x + case x[0] === byTag.ListValue: return x + case x[0] === byTag.ObjectValue: return x + case x[0] === byTag.ObjectField: return x + case x[0] === byTag.Variable: return x + case x[0] === byTag.OperationTypeDefinition: return x + case x[0] === byTag.ListType: return [x[0], g(x[1])] + case x[0] === byTag.NonNullType: return [x[0], g(x[1])] + case x[0] === byTag.SelectionSet: return [x[0], fn.map(x[1], g)] + case x[0] === byTag.Argument: return [x[0], x[1], g(x[2])] + case x[0] === byTag.Directive: return [x[0], x[1], fn.map(x[2], g)] + case x[0] === byTag.DirectiveDefinition: return [x[0], x[1], x[2], x[3], x[4], fn.map(x[5], g)] + case x[0] === byTag.Document: return [x[0], fn.map(x[1], g)] + case x[0] === byTag.EnumTypeDefinition: return [x[0], x[1], x[2], x[3], fn.map(x[4], g)] + case x[0] === byTag.Field: return [x[0], x[1], x[2], isNull(x[3]) ? x[3] : g(x[3]), fn.map(x[4], g), fn.map(x[5], g)] + case x[0] === byTag.FieldDefinition: return [x[0], x[1], x[2], g(x[3]), fn.map(x[4], g), fn.map(x[5], g)] + case x[0] === byTag.FragmentDefinition: return [x[0], x[1], x[2], g(x[3]), fn.map(x[4], g)] + case x[0] === byTag.FragmentSpread: return [x[0], x[1], fn.map(x[2], g)] + case x[0] === byTag.InlineFragment: return [x[0], x[1], g(x[2]), fn.map(x[3], g)] + case x[0] === byTag.InputValueDefinition: return [x[0], x[1], x[2], g(x[3]), g(x[4]), fn.map(x[5], g)] + case x[0] === byTag.InputObjectTypeDefinition: return [x[0], x[1], x[2], fn.map(x[3], g), fn.map(x[4], g)] + case x[0] === byTag.InterfaceTypeDefinition: return [x[0], x[1], x[2], fn.map(x[3], g), fn.map(x[4], g), fn.map(x[5], g)] + case x[0] === byTag.ObjectTypeDefinition: return [x[0], x[1], x[2], fn.map(x[3], g), fn.map(x[4], g), fn.map(x[5], g)] + case x[0] === byTag.OperationDefinition: return [x[0], x[1], x[2], g(x[3]!), fn.map(x[4], g), fn.map(x[5], g)] + case x[0] === byTag.SchemaDefinition: return [x[0], x[1], x[2], x[3], fn.map(x[4], g)] + case x[0] === byTag.UnionTypeDefinition: return [x[0], x[1], fn.map(x[2], g), fn.map(x[3], g)] + case x[0] === byTag.VariableDefinition: return [x[0], x[1], g(x[2]), g(x[3]), fn.map(x[4], g)] + // case x[0] === byTag.SchemaExtension: return [x[0], fn.map(x[1], g), fn.map(x[2], g)] + } + } + }, + mapWithIndex(g) { + return (x, ix) => { + switch (true) { + default: return x satisfies never + case x[0] === byTag.Name: return x + case x[0] === byTag.Boolean: return x + case x[0] === byTag.BooleanValue: return x + case x[0] === byTag.EnumValue: return x + case x[0] === byTag.EnumValueDefinition: return x + case x[0] === byTag.Float: return x + case x[0] === byTag.FloatValue: return x + case x[0] === byTag.ID: return x + case x[0] === byTag.Int: return x + case x[0] === byTag.IntValue: return x + case x[0] === byTag.NamedType: return x + case x[0] === byTag.Null: return x + case x[0] === byTag.NullValue: return x + case x[0] === byTag.Number: return x + case x[0] === byTag.ScalarTypeDefinition: return x + case x[0] === byTag.String: return x + case x[0] === byTag.StringValue: return x + case x[0] === byTag.ListValue: return x + case x[0] === byTag.ObjectValue: return x + case x[0] === byTag.ObjectField: return x + case x[0] === byTag.Variable: return x + case x[0] === byTag.OperationTypeDefinition: return x + case x[0] === byTag.ListType: return [x[0], g(x[1], ix, x)] + case x[0] === byTag.NonNullType: return [x[0], g(x[1], ix, x)] + case x[0] === byTag.SelectionSet: return [x[0], fn.map(x[1], (_) => g(_, ix, x))] + case x[0] === byTag.Argument: return [x[0], x[1], g(x[2], ix, x)] + case x[0] === byTag.Directive: return [x[0], x[1], fn.map(x[2], (_) => g(_, ix, x))] + case x[0] === byTag.DirectiveDefinition: return [x[0], x[1], x[2], x[3], x[4], fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.Document: return [x[0], fn.map(x[1], (_) => g(_, ix, x))] + case x[0] === byTag.EnumTypeDefinition: return [x[0], x[1], x[2], x[3], fn.map(x[4], (_) => g(_, ix, x))] + case x[0] === byTag.Field: return [x[0], x[1], x[2], isNull(x[3]) ? x[3] : g(x[3], ix, x), fn.map(x[4], (_) => g(_, ix, x)), fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.FieldDefinition: return [x[0], x[1], x[2], g(x[3], ix, x), fn.map(x[4], (_) => g(_, ix, x)), fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.FragmentDefinition: return [x[0], x[1], x[2], g(x[3], ix, x), fn.map(x[4], (_) => g(_, ix, x))] + case x[0] === byTag.FragmentSpread: return [x[0], x[1], fn.map(x[2], (_) => g(_, ix, x))] + case x[0] === byTag.InlineFragment: return [x[0], x[1], g(x[2], ix, x), fn.map(x[3], (_) => g(_, ix, x))] + case x[0] === byTag.InputValueDefinition: return [x[0], x[1], x[2], g(x[3], ix, x), g(x[4], ix, x), fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.InputObjectTypeDefinition: return [x[0], x[1], x[2], fn.map(x[3], (_) => g(_, ix, x)), fn.map(x[4], (_) => g(_, ix, x))] + case x[0] === byTag.InterfaceTypeDefinition: return [x[0], x[1], x[2], fn.map(x[3], (_) => g(_, ix, x)), fn.map(x[4], (_) => g(_, ix, x)), fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.ObjectTypeDefinition: return [x[0], x[1], x[2], fn.map(x[3], (_) => g(_, ix, x)), fn.map(x[4], (_) => g(_, ix, x)), fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.OperationDefinition: return [x[0], x[1], x[2], g(x[3], ix, x), fn.map(x[4], (_) => g(_, ix, x)), fn.map(x[5], (_) => g(_, ix, x))] + case x[0] === byTag.SchemaDefinition: return [x[0], x[1], x[2], x[3], fn.map(x[4], (_) => g(_, ix, x))] + case x[0] === byTag.UnionTypeDefinition: return [x[0], x[1], fn.map(x[2], (_) => g(_, ix, x)), fn.map(x[3], (_) => g(_, ix, x))] + case x[0] === byTag.VariableDefinition: return [x[0], x[1], g(x[2], ix, x), g(x[3], ix, x), fn.map(x[4], (_) => g(_, ix, x))] + // case x[0] === byTag.SchemaExtension: return [x[0], fn.map(x[1], (_) => g(_, ix, x)), fn.map(x[2], (_) => g(_, ix, x))] + } + } + } +} + +export const fold = fn.catamorphism(Functor, defaultIndex) diff --git a/packages/graphql-test/src/generator-bounds.ts b/packages/graphql-test/src/generator-bounds.ts new file mode 100644 index 00000000..15bda141 --- /dev/null +++ b/packages/graphql-test/src/generator-bounds.ts @@ -0,0 +1,224 @@ +import * as fc from 'fast-check' +import { + fn, + Number_isFinite, + Number_isNatural, + Number_isSafeInteger, + Object_is, +} from '@traversable/registry' + +/** @internal */ +const nullable = (model: fc.Arbitrary) => fc.oneof(fc.constant(null), fc.constant(null), model) + +/** @internal */ +const isBigInt = (x: unknown) => typeof x === 'bigint' + +export const defaultDoubleConstraints = { + noNaN: true, + noDefaultInfinity: true, +} satisfies fc.DoubleConstraints + +const defaultIntBounds = [-0x1000, +0x1000, null] satisfies Bounds_int +const defaultBigIntBounds = [-0x1000000n, 0x1000000n, null] satisfies Bounds_bigint +const defaultNumberBounds = [-0x10000, +0x10000, null, false, false] satisfies Bounds_number +const defaultStringBounds = [0, +0x40] satisfies Bounds_string +const defaultArrayBounds = [0, +0x10] satisfies Bounds_array + +export const defaults = { + int: defaultIntBounds, + number: defaultNumberBounds, + bigint: defaultBigIntBounds, + string: defaultStringBounds, + array: defaultArrayBounds, +} + +const clampMin + : (min: T, max: T, predicate: (x: T | null) => x is T) => (x: T | null, y: T | null) => T | null + = (min, max, predicate) => (x, y) => { + if (!predicate(x)) { + return null + } else if (!predicate(y)) { + return x < min ? min : max < x ? max : x + } else { + const z = x < y ? x : y + return z < min ? min : max < z ? max : z + } + } + +const clampMax + : (min: T, max: T, predicate: (x: T | null) => x is T) => (x: T | null, y: T | null) => T | null + = (min, max, predicate) => (x, y) => { + if (!predicate(x)) { + return null + } else if (!predicate(y)) { + return x < min ? min : max < x ? max : x + } else { + const z = x > y ? x : y + return z < min ? min : max < z ? max : z + } + } + +const clampIntMin = clampMin(defaults.int[0], defaults.int[1], Number_isSafeInteger) +const clampIntMax = clampMax(defaults.int[0], defaults.int[1], Number_isSafeInteger) +const clampBigIntMin = clampMin(defaults.bigint[0], defaults.bigint[1], isBigInt) +const clampBigIntMax = clampMax(defaults.bigint[0], defaults.bigint[1], isBigInt) +const clampNumberMin = clampMin(defaults.number[0], defaults.number[1], Number_isFinite) +const clampNumberMax = clampMin(defaults.number[0], defaults.number[1], Number_isFinite) +const clampStringMin = clampMin(defaults.string[0], defaults.string[1], Number_isNatural) +const clampStringMax = clampMax(defaults.string[0], defaults.string[1], Number_isNatural) +const clampArrayMin = clampMin(defaults.array[0], defaults.array[1], Number_isNatural) +const clampArrayMax = clampMax(defaults.array[0], defaults.array[1], Number_isNatural) + +export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: model, maximum: model }) + +export { Bounds_int as int } +type Bounds_int = [ + minimum: number | null, + maximum: number | null, + multipleOf: number | null, +] + +const Bounds_int + : (model: fc.Arbitrary) => fc.Arbitrary + = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map(([x, y, multipleOf]) => [ + clampIntMin(x, y), + clampIntMax(y, x), + multipleOf, + // clampInt(multipleOf), + ]) + +export { Bounds_bigint as bigint } +type Bounds_bigint = [ + minimum: bigint | null, + maximum: bigint | null, + multipleOf: bigint | null, +] + +const Bounds_bigint + : (model: fc.Arbitrary) => fc.Arbitrary + = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map(([x, y, multipleOf]) => [ + clampBigIntMin(x, y), + clampBigIntMax(y, x), + multipleOf, // clampBigInt(multipleOf), + ]) + +export { Bounds_string as string } +type Bounds_string = [ + minLength: number | null, + maxLength: number | null, +] + +const Bounds_string + : (model: fc.Arbitrary) => fc.Arbitrary + = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map( + ([x, y, length]) => Number_isNatural(length) + ? [null, null] satisfies [any, any] + // [clampString(length), clampString(length)] + : [clampStringMin(x, y), clampStringMax(y, x)] + ) + +export { Bounds_number as number } +type Bounds_number = [ + minimum: number | null, + maximum: number | null, + multipleOf: number | null, + exclusiveMinimum: boolean, + exclusiveMaximum: boolean, +] + +const deltaIsSubEpsilon = (x: number, y: number) => Math.abs(x - y) < Number.EPSILON + +const Bounds_number + : (model: fc.Arbitrary) => fc.Arbitrary + = (model) => fc.tuple( + nullable(model), + nullable(model), + nullable(model), + fc.boolean(), + fc.boolean() + ).map( + fn.flow( + ([x, y, multipleOf, minExcluded, maxExcluded]): Bounds_number => [ + clampNumberMin(x, y), + clampNumberMax(y, x), + // clampNumber(multipleOf), + multipleOf, + minExcluded, + maxExcluded, + ], + ([min, max, multipleOf, minExcluded, maxExcluded]): Bounds_number => [ + min, + ((minExcluded || maxExcluded) && min != null && max != null && deltaIsSubEpsilon(min, max)) ? max + 1 : max, + multipleOf, + minExcluded, + maxExcluded + ], + ) + ) + +export { Bounds_array as array } +type Bounds_array = [ + minLength: number | null, + maxLength: number | null, +] + +const Bounds_array + : (model: fc.Arbitrary) => fc.Arbitrary + = (model) => fc.tuple( + nullable(model), + nullable(model), + fc.constant(null) + ).map(([x, y, exactLength]) => + Number_isNatural(exactLength) + ? [null, null] + : [clampArrayMin(x, y), clampArrayMax(y, x)] + ) + + +export const intBoundsToIntegerConstraints + : (bounds?: Bounds_int) => fc.IntegerConstraints + = (bounds = defaultIntBounds) => { + const [min, max, multipleOf] = bounds + return { + max: max ?? void 0, + min: min ?? void 0, + } satisfies fc.IntegerConstraints + } + +export const bigintBoundsToBigIntConstraints + : (bounds?: Bounds_bigint) => fc.BigIntConstraints + = (bounds = defaultBigIntBounds) => { + const [min, max, multipleOf] = bounds + return { + max: max ?? void 0, + min: min ?? void 0, + } satisfies fc.BigIntConstraints + } + +export const numberBoundsToDoubleConstraints + : (bounds?: Bounds_number) => fc.DoubleConstraints + = (bounds = defaultNumberBounds) => { + let [min, max, multipleOf, minExcluded, maxExcluded] = bounds + if (minExcluded && Object_is(min, -0)) min = +0 + if (maxExcluded && Object_is(max, +0)) max = -0 + return { + ...defaultDoubleConstraints, max: max ?? void 0, + min: min ?? void 0, + minExcluded, + maxExcluded, + } satisfies fc.DoubleConstraints + } + +export const stringBoundsToStringConstraints + : (bounds?: Bounds_string) => fc.StringConstraints + = ([minLength, maxLength] = defaultStringBounds) => ({ + minLength: minLength ?? void 0, + maxLength: maxLength ?? void 0 + }) satisfies fc.StringConstraints + +export const arrayBoundsToArrayConstraints + : (bounds?: Bounds_array) => fc.ArrayConstraints + = ([minLength, maxLength] = defaultArrayBounds) => ({ + minLength: minLength ?? void 0, + maxLength: maxLength ?? void 0 + }) satisfies fc.ArrayConstraints diff --git a/packages/graphql-test/src/generator-options.ts b/packages/graphql-test/src/generator-options.ts new file mode 100644 index 00000000..baffa051 --- /dev/null +++ b/packages/graphql-test/src/generator-options.ts @@ -0,0 +1,341 @@ +import type * as fc from 'fast-check' +import { + Object_assign, + Object_create, + Object_keys, +} from '@traversable/registry' + +import type { Seed } from './generator-seed.js' +import { byTag } from './generator-seed.js' + +export interface Options extends Partial>, Constraints {} + +export interface Config extends OptionsBase, Constraints {} + +export declare namespace Config { + export { + Options, + Constraints, + } +} + +export type InferConfigType = S extends Options ? T : never + +export interface OptionsBase< + T = never, + K extends + | string & keyof T + = string & keyof T +> { + include: readonly K[] + exclude: readonly K[] + root: '*' | K + sortBias: { [K in keyof Seed]+?: number } + forceInvalid: boolean + noDescriptions: boolean +} + +export declare namespace Constraints { + type Argument = fc.UniqueArrayConstraints + type Directive = fc.UniqueArrayConstraints + type DirectiveDefinition = fc.UniqueArrayConstraints + type Document = fc.UniqueArrayConstraints + type EnumTypeDefinition = fc.UniqueArrayConstraints + type InputValueDefinition = fc.UniqueArrayConstraints + type FloatValue = fc.DoubleConstraints + type FieldDefinition = fc.UniqueArrayConstraints + type ListValue = fc.ArrayConstraints + type NamedType = fc.UniqueArrayConstraints + type ObjectValue = fc.UniqueArrayConstraints + type SchemaDefinition = fc.UniqueArrayConstraints + type SelectionSet = fc.UniqueArrayConstraints + type VariableDefinition = fc.UniqueArrayConstraints +} + +export type Constraints = { + Argument?: Constraints.Argument + Boolean?: {} + BooleanValue?: {} + Directive?: Constraints.Directive + DirectiveDefinition?: Constraints.DirectiveDefinition + Document?: Constraints.Document + EnumTypeDefinition?: Constraints.EnumTypeDefinition + EnumValue?: {} + EnumValueDefinition?: {} + Field?: {} + FieldDefinition?: Constraints.FieldDefinition + Float?: {} + FloatValue?: Constraints.FloatValue + FragmentDefinition?: {} + FragmentSpread?: {} + ID?: {} + InlineFragment?: {} + InputObjectTypeDefinition?: {} + InputValueDefinition?: Constraints.InputValueDefinition + Int?: {} + InterfaceTypeDefinition?: {} + IntValue?: {} + ListType?: {} + ListValue?: Constraints.ListValue + Name?: {} + NamedType?: Constraints.NamedType + NonNullType?: {} + Null?: {} + NullValue?: {} + Number?: {} + ObjectField?: {} + ObjectTypeDefinition?: {} + ObjectValue?: Constraints.ObjectValue + OperationDefinition?: {} + OperationTypeDefinition?: {} + ScalarTypeDefinition?: {} + SchemaDefinition?: Constraints.SchemaDefinition + SelectionSet?: Constraints.SelectionSet + String?: {} + StringValue?: {} + UnionTypeDefinition?: {} + Variable?: {} + VariableDefinition?: Constraints.VariableDefinition +} + +export const defaultConstraints = { + Argument: { + minLength: 0, + maxLength: 3, + selector: ([, name]) => name, + size: 'xsmall', + }, + Boolean: {}, + BooleanValue: {}, + Directive: { + minLength: 0, + maxLength: 2, + selector: ([, name]) => name, + size: 'xsmall', + }, + DirectiveDefinition: { + minLength: 1, + maxLength: 3, + }, + Document: { + minLength: 1, + maxLength: 5, + selector: ([, name]) => name, + size: 'xsmall', + }, + EnumTypeDefinition: { + minLength: 1, + maxLength: 3, + }, + EnumValue: {}, + EnumValueDefinition: {}, + Field: {}, + FieldDefinition: { + minLength: 1, + maxLength: 5, + selector: ([, name]) => name, + size: 'xsmall', + }, + Float: {}, + FloatValue: { + noNaN: true, + noDefaultInfinity: true, + }, + FragmentDefinition: {}, + FragmentSpread: {}, + ID: {}, + InlineFragment: {}, + InputObjectTypeDefinition: {}, + InputValueDefinition: { + minLength: 1, + maxLength: 3, + selector: ([, name]) => name, + size: 'xsmall', + }, + Int: {}, + InterfaceTypeDefinition: {}, + IntValue: { + }, + ListType: {}, + ListValue: { + minLength: 0, + maxLength: 5, + size: 'xsmall', + }, + Name: {}, + NamedType: { + minLength: 1, + maxLength: 3, + selector: ([, name]) => name, + size: 'xsmall', + }, + NonNullType: {}, + Null: {}, + NullValue: {}, + Number: {}, + ObjectField: {}, + ObjectTypeDefinition: {}, + ObjectValue: { + minLength: 1, + maxLength: 3, + selector: ([key]) => key, + size: 'xsmall', + }, + OperationDefinition: { + }, + OperationTypeDefinition: {}, + ScalarTypeDefinition: {}, + SchemaDefinition: { + minLength: 1, + maxLength: 3, + selector: ([, , operationType]) => operationType, + size: 'xsmall', + }, + SelectionSet: { + minLength: 0, + maxLength: 3, + selector: ([, name]) => name, + size: 'xsmall', + }, + String: {}, + StringValue: {}, + UnionTypeDefinition: {}, + Variable: {}, + VariableDefinition: { + minLength: 0, + maxLength: 2, + selector: ([, name]) => name, + size: 'xsmall', + }, +} satisfies Required + +export const defaultOptions: OptionsBase = { + include: Object_keys(defaultConstraints), + exclude: [], + root: '*', + sortBias: byTag, + forceInvalid: false, + noDescriptions: false, +} + +export const defaults = Object_assign( + Object_create(null), + defaultConstraints, + defaultOptions, +) satisfies Config + +export function parseOptions(options?: Opts): Config> +export function parseOptions(options: Options = defaults): Config { + const { + // options: + exclude = defaults.exclude, + forceInvalid = defaults.forceInvalid, + include = defaults.include, + root = defaults.root, + sortBias = defaults.sortBias, + noDescriptions = defaults.noDescriptions, + // constraints: + Argument = defaults.Argument, + Boolean = defaults.Boolean, + BooleanValue = defaults.BooleanValue, + Directive = defaults.Directive, + DirectiveDefinition = defaults.DirectiveDefinition, + Document = defaults.Document, + EnumTypeDefinition = defaults.EnumTypeDefinition, + EnumValue = defaults.EnumValue, + EnumValueDefinition = defaults.EnumValueDefinition, + Field = defaults.Field, + FieldDefinition = defaults.FieldDefinition, + Float = defaults.Float, + FloatValue = defaults.FloatValue, + FragmentDefinition = defaults.FragmentDefinition, + FragmentSpread = defaults.FragmentSpread, + ID = defaults.ID, + InlineFragment = defaults.InlineFragment, + InputObjectTypeDefinition = defaults.InputObjectTypeDefinition, + InputValueDefinition = defaults.InputValueDefinition, + Int = defaults.Int, + IntValue = defaults.IntValue, + InterfaceTypeDefinition = defaults.InterfaceTypeDefinition, + ListType = defaults.ListType, + ListValue = defaults.ListValue, + Name = defaults.Name, + NamedType = defaults.NamedType, + NonNullType = defaults.NonNullType, + Null = defaults.Null, + NullValue = defaults.NullValue, + Number = defaults.Number, + ObjectField = defaults.ObjectField, + ObjectTypeDefinition = defaults.ObjectTypeDefinition, + ObjectValue = defaults.ObjectValue, + OperationDefinition = defaults.OperationDefinition, + OperationTypeDefinition = defaults.OperationTypeDefinition, + ScalarTypeDefinition = defaults.ScalarTypeDefinition, + SchemaDefinition = defaults.SchemaDefinition, + SelectionSet = defaults.SelectionSet, + String = defaults.String, + StringValue = defaults.StringValue, + UnionTypeDefinition = defaults.UnionTypeDefinition, + Variable = defaults.Variable, + VariableDefinition = defaults.VariableDefinition, + } = options + + return { + include: include.length === 0 || include[0] === ('*' as never) + ? defaults.include + : include, + exclude, + root, + sortBias, + forceInvalid, + noDescriptions, + // nodes: + Argument, + Boolean, + BooleanValue, + Directive, + DirectiveDefinition, + Document, + EnumTypeDefinition, + EnumValue, + EnumValueDefinition, + Field, + FieldDefinition, + Float, + FloatValue, + FragmentDefinition, + FragmentSpread, + ID, + InlineFragment, + InputObjectTypeDefinition, + InputValueDefinition, + Int, + IntValue, + InterfaceTypeDefinition, + ListType, + ListValue, + Name, + NamedType, + NonNullType, + Null, + NullValue, + Number, + ObjectField, + ObjectTypeDefinition, + ObjectValue, + OperationDefinition, + OperationTypeDefinition, + ScalarTypeDefinition, + SchemaDefinition, + SelectionSet, + String, + StringValue, + UnionTypeDefinition, + Variable, + VariableDefinition, + } +} + +export function Config() {} +Config.defaults = defaults +Config.parseOptions = parseOptions diff --git a/packages/graphql-test/src/generator-seed.ts b/packages/graphql-test/src/generator-seed.ts new file mode 100644 index 00000000..4cca0433 --- /dev/null +++ b/packages/graphql-test/src/generator-seed.ts @@ -0,0 +1,1030 @@ +import * as fc from 'fast-check' +import type * as T from '@traversable/registry' +import { Object_keys, PATTERN } from '@traversable/registry' +import type { Json } from '@traversable/json' +import type { OperationType } from '@traversable/graphql-types' +import * as F from '@traversable/graphql-types' + +import type { Config } from './generator-options.js' + +type Constraints = Config.Options + +export function invert>(x: T): { [K in keyof T as T[K]]: K } +export function invert(x: Record) { + return Object_keys(x).reduce((acc, k) => (acc[x[k]] = k, acc), {} as typeof x) +} + +export type Tag = byTag[keyof byTag] +export type byTag = typeof byTag +export const byTag = { + Name: 10, + NamedType: 20, + /* scalars */ + Boolean: 30, + Float: 40, + ID: 50, + Int: 60, + Null: 70, + Number: 80, + ScalarTypeDefinition: 90, + String: 100, + /* values */ + BooleanValue: 110, + EnumValue: 120, + FloatValue: 130, + IntValue: 140, + ListValue: 150, + NullValue: 160, + ObjectValue: 170, + StringValue: 180, + /* type definitions */ + EnumTypeDefinition: 190, + EnumValueDefinition: 200, + Field: 210, + FieldDefinition: 220, + ObjectField: 230, + ObjectTypeDefinition: 240, + InterfaceTypeDefinition: 250, + /* modifiers */ + ListType: 260, + NonNullType: 270, + UnionTypeDefinition: 280, + /* args */ + Argument: 290, + InputObjectTypeDefinition: 300, + InputValueDefinition: 310, + Variable: 320, + VariableDefinition: 330, + /* special */ + Directive: 340, + DirectiveDefinition: 350, + Document: 360, + FragmentDefinition: 370, + FragmentSpread: 380, + InlineFragment: 390, + OperationDefinition: 400, + OperationTypeDefinition: 410, + SelectionSet: 420, + SchemaDefinition: 430, + // SchemaExtension: 440, +} as const satisfies Record<'Name' | F.Kind | F.NamedType, number> + +export type bySeed = typeof bySeed +export const bySeed = invert(byTag) + +export const identifier = fc.stringMatching(new RegExp(PATTERN.identifierNoDollar, 'u')).map((x) => `${x.charAt(0).toUpperCase()}${x.slice(1)}`) +// export const name = fc.lorem({ maxCount: 1 }) +export const alias = identifier +export const target = fc.constantFrom(...F.DirectiveTargets) + +export const description = ($: Constraints) => $.noDescriptions ? fc.constant(null) : fc.oneof( + fc.constant(null), + fc.constant(null), + fc.tuple( + fc.lorem(), + fc.boolean(), + ) +) satisfies fc.Arbitrary + +export const operationType = fc.constantFrom( + 'query', + 'mutation', + 'subscription', +) satisfies fc.Arbitrary + +export declare namespace Seed { + type Name = [ + Name: byTag['Name'], + value: string, + ] + + type NamedType = [ + NamedType: byTag['NamedType'], + name: string, + ] + + type Description = null | [description: string, block: boolean] + + //////////////////////// + /// Terminal nodes /// + type Boolean = [Boolean: byTag['Boolean']] + type Float = [Float: byTag['Float']] + type ID = [ID: byTag['ID']] + type Int = [Int: byTag['Int']] + type Null = [Null: byTag['Null']] + type Number = [Number: byTag['Number']] + type String = [String: byTag['String']] + + type ScalarTypeDefinition = [ + ScalarTypeDefinition: byTag['ScalarTypeDefinition'], + name: string, + description: Description, + ] + /// Terminal nodes /// + //////////////////////// + + ///////////////////// + /// Value nodes /// + type NullValue = [ + NullValue: byTag['NullValue'] + ] + + type BooleanValue = [ + BooleanValue: byTag['BooleanValue'], + value: boolean, + ] + + type EnumValue = [ + EnumValue: byTag['EnumValue'], + value: string, + ] + + type FloatValue = [ + FloatValue: byTag['FloatValue'], + value: number, + ] + + type IntValue = [ + IntValue: byTag['IntValue'], + value: number, + ] + + type StringValue = [ + StringValue: byTag['StringValue'], + value: string, + block: boolean, + ] + + type EnumValueDefinition = [ + EnumValueDefinition: byTag['EnumValueDefinition'], + value: string, + ] + + type ListValue = [ + ListValue: byTag['ListValue'], + value: readonly Json[], + ] + + type ObjectValue = [ + ObjectValue: byTag['ObjectValue'], + value: readonly (readonly [k: string, v: Json])[], + ] + /// Value nodes /// + ///////////////////// + + type ListType = [ + ListType: byTag['ListType'], + type: T, + ] + + type NonNullType = [ + NonNullType: byTag['NonNullType'], + type: T, + ] + + type UnionTypeDefinition = [ + UnionTypeDefinition: byTag['UnionTypeDefinition'], + name: string, + types: readonly T[], + directives: readonly T[] + ] + + type Variable = [ + Variable: byTag['Variable'], + name: string, + description: Description, + ] + + type EnumTypeDefinition = [ + EnumTypeDefinition: byTag['EnumTypeDefinition'], + name: string, + description: Description, + values: readonly string[], + directives: readonly T[], + ] + + type Field = [ + Field: byTag['Field'], + name: string, + alias: string, + selectionSet: T | null, + arguments: readonly T[], + directives: readonly T[], + ] + + type FieldDefinition = [ + FieldDefinition: byTag['FieldDefinition'], + name: string, + description: Description, + type: T, + arguments: readonly T[], + directives: readonly T[], + ] + + type ObjectField = [ + ObjectField: byTag['ObjectField'], + name: string, + value: T + ] + + type ObjectTypeDefinition = [ + ObjectTypeDefinition: byTag['ObjectTypeDefinition'], + name: string, + description: Description, + fields: readonly T[], + interfaces: readonly T[], + directives: readonly T[], + ] + + type InterfaceTypeDefinition = [ + InterfaceTypeDefinition: byTag['InterfaceTypeDefinition'], + name: string, + description: Description, + fields: readonly T[], + interfaces: readonly T[], + directives: readonly T[], + ] + + type Argument = [ + Argument: byTag['Argument'], + name: string, + value: T, + ] + + type InputObjectTypeDefinition = [ + InputObjectTypeDefinition: byTag['InputObjectTypeDefinition'], + name: string, + description: Description, + fields: readonly T[], + directives: readonly T[], + ] + + type InputValueDefinition = [ + InputValueDefinition: byTag['InputValueDefinition'], + name: string, + description: Description, + type: T, + defaultValue: T, + directives: readonly T[], + ] + + type VariableDefinition = [ + VariableDefinition: byTag['VariableDefinition'], + variable: string, + type: T, + defaultValue: T, + directives: readonly T[], + ] + + type Directive = [ + Directive: byTag['Directive'], + name: string, + arguments: readonly T[], + ] + + type DirectiveDefinition = [ + DirectiveDefinition: byTag['DirectiveDefinition'], + name: string, + description: Description, + repeatable: boolean, + locations: readonly F.DirectiveTarget[], + arguments: readonly T[], + ] + + type Document = [ + Document: byTag['Document'], + definition: readonly T[], + ] + + type FragmentDefinition = [ + FragmentDefinition: byTag['FragmentDefinition'], + name: string, + typeCondition: string, + selectionSet: T, + directives: readonly T[], + ] + + type FragmentSpread = [ + FragmentSpread: byTag['FragmentSpread'], + name: string, + directives: readonly T[], + ] + + type InlineFragment = [ + InlineFragment: byTag['InlineFragment'], + typeCondition: string, + selectionSet: T, + directives: readonly T[], + ] + + type OperationDefinition = [ + OperationDefinition: byTag['OperationDefinition'], + name: string, + operation: OperationType, + selectionSet: T, + variableDefinitions: readonly T[], + directives: readonly T[], + ] + + type OperationTypeDefinition = [ + OperationTypeDefinition: byTag['OperationTypeDefinition'], + type: string, + operation: F.OperationType, + ] + + type SelectionSet = [ + SelectionSet: byTag['SelectionSet'], + selections: readonly T[], + ] + + type SchemaDefinition = [ + SchemaDefinition: byTag['SchemaDefinition'], + _unused: string, + description: Description, + operationTypes: readonly Seed.OperationTypeDefinition[], + directives: readonly T[], + ] + + // type SchemaExtension = [ + // SchemaExtension: byTag['SchemaExtension'], + // operationTypes: readonly T[], + // directives: readonly T[], + // ] + + type Terminal = TerminalMap[keyof TerminalMap] + type TerminalMap = { + Boolean: Boolean + Float: Float + ID: ID + Int: Int + Name: Name + NamedType: NamedType + Null: Null + Number: Number + ScalarTypeDefinition: ScalarTypeDefinition + String: String + Variable: Variable + OperationTypeDefinition: OperationTypeDefinition + } + + type Value = ValueMap[keyof ValueMap] + type ValueMap = { + BooleanValue: BooleanValue + FloatValue: FloatValue + IntValue: IntValue + NullValue: NullValue + StringValue: StringValue + EnumValue: EnumValue + EnumValueDefinition: EnumValueDefinition + ListValue: ListValue + ObjectValue: ObjectValue + ObjectField: ObjectField + } + + type UnaryMap = { + ListType: ListType + NonNullType: NonNullType + UnionTypeDefinition: UnionTypeDefinition + EnumTypeDefinition: EnumTypeDefinition + Field: Field + FieldDefinition: FieldDefinition + ObjectTypeDefinition: ObjectTypeDefinition + InterfaceTypeDefinition: InterfaceTypeDefinition + Argument: Argument + InputObjectTypeDefinition: InputObjectTypeDefinition + InputValueDefinition: InputValueDefinition + VariableDefinition: VariableDefinition + Directive: Directive + DirectiveDefinition: DirectiveDefinition + Document: Document + FragmentDefinition: FragmentDefinition + FragmentSpread: FragmentSpread + InlineFragment: InlineFragment + OperationDefinition: OperationDefinition + SelectionSet: SelectionSet + SchemaDefinition: SchemaDefinition + // SchemaExtension: SchemaExtension + } + + type Nullary = + | Seed.Terminal + | Seed.Value + | Seed.EnumValueDefinition + + type Unary = + | Seed.ListType + | Seed.NonNullType + | Seed.UnionTypeDefinition + | Seed.EnumTypeDefinition + | Seed.Field + | Seed.FieldDefinition + | Seed.ObjectTypeDefinition + | Seed.InterfaceTypeDefinition + | Seed.Argument + | Seed.InputObjectTypeDefinition + | Seed.InputValueDefinition + | Seed.VariableDefinition + | Seed.Directive + | Seed.DirectiveDefinition + | Seed.Document + | Seed.FragmentDefinition + | Seed.FragmentSpread + | Seed.InlineFragment + | Seed.OperationDefinition + | Seed.SelectionSet + | Seed.SchemaDefinition + // | Seed.SchemaExtension + + type F = + | Seed.Nullary + | Seed.Value + | Seed.Unary + + type Fixpoint = + | Seed.Nullary + | Seed.Value + | Seed.ListType + | Seed.NonNullType + | Seed.UnionTypeDefinition + | Seed.Variable + | Seed.EnumTypeDefinition + | Seed.Field + | Seed.FieldDefinition + | Seed.ObjectTypeDefinition + | Seed.InterfaceTypeDefinition + | Seed.Argument + | Seed.InputObjectTypeDefinition + | Seed.InputValueDefinition + | Seed.VariableDefinition + | Seed.Directive + | Seed.DirectiveDefinition + | Seed.Document + | Seed.FragmentDefinition + | Seed.FragmentSpread + | Seed.InlineFragment + | Seed.OperationDefinition + | Seed.OperationTypeDefinition + | Seed.SelectionSet + | Seed.SchemaDefinition + // | Seed.SchemaExtension + + interface Free extends T.HKT { [-1]: Seed.F } +} + +export type Seed = ( + & Seed.TerminalMap + & Seed.ValueMap + & Seed.UnaryMap +) + +const NamedType = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.NamedType), + identifier, +) + +type JsonValue = { + null: null + number: number + string: string + array: readonly JsonValue[] + object: { [x: string]: JsonValue } + "*": JsonValue +} + +const jsonValue = fc.letrec((tie: fc.LetrecTypedTie) => { + return { + null: fc.constant(null), + number: fc.integer(), + // fc.double({ noNaN: true, noDefaultInfinity: true }), + string: identifier, + array: fc.array(tie('*')), + object: fc.uniqueArray( + fc.tuple( + identifier, + tie('*'), + ), + { selector: ([k]) => k } + ).map((xs) => Object.fromEntries(xs)), + ['*']: fc.oneof( + tie('null'), + tie('number'), + tie('string'), + tie('array'), + tie('object'), + ) + } +}) + +const Boolean = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.Boolean]) +const Float = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.Float]) +const Int = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.Int]) +const ID = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.ID]) +const Null = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.Null]) +const Number = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.Number]) +const String = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.String]) + +const NullValue = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.constant([byTag.NullValue]) +const BooleanValue = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.BooleanValue), + fc.boolean(), +) +const IntValue = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.IntValue), + fc.integer(), +) +const FloatValue = (_tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.FloatValue), + fc.double($.FloatValue), +) +const StringValue = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.StringValue), + fc.string(), + fc.boolean(), +) + +const ScalarTypeDefinition = (_tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.ScalarTypeDefinition), + identifier, + description($), +) + +const EnumValue = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.EnumValue), + identifier, +) + +const EnumValueDefinition = (_tie: fc.LetrecTypedTie, _$: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.EnumValueDefinition), + identifier, +) + +const ListValue = (_tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.ListValue), + fc.array( + jsonValue['*'], + $.ListValue + ), +) + +const ListType = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.ListType), + TypeNode(tie, $), +) + +const NonNullType = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.NonNullType), + fc.oneof( + NamedType(tie, $), + ListType(tie, $), + ) +) + +const UnionTypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.UnionTypeDefinition), + identifier, + fc.uniqueArray( + NamedType(tie, $), + $.NamedType! + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ) +) + +const Variable = (_tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.Variable), + identifier, + description($), +) + +const EnumTypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.EnumTypeDefinition), + identifier, + description($), + fc.uniqueArray( + identifier, + $.EnumTypeDefinition! + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ), +) + +const Field = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.Field), + identifier, + alias, + fc.oneof( + fc.constant(null), + NonEmptySelectionSet(tie, $), + ), + fc.uniqueArray( + Argument(tie, $), + $.Argument! + ), + fc.uniqueArray( + Directive(tie, $), + $.Directive! + ), +) + +const FieldDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.FieldDefinition), + identifier, + description($), + TypeNode(tie, $), + fc.uniqueArray( + InputValueDefinition(tie, $), + $.InputValueDefinition! + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ) +) + +const ObjectTypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.ObjectTypeDefinition), + identifier, + description($), + fc.uniqueArray( + FieldDefinition(tie, $), + $.FieldDefinition! + ), + fc.uniqueArray( + NamedType(tie, $), + $.NamedType! + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ) +) + +const InterfaceTypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.InterfaceTypeDefinition), + identifier, + description($), + fc.uniqueArray( + FieldDefinition(tie, $), + $.FieldDefinition! + ), + fc.uniqueArray( + NamedType(tie, $), + $.NamedType! + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ) +) + +const Argument = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.Argument), + identifier, + ValueNode(tie, $), +) + +const InputObjectTypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.InputObjectTypeDefinition), + identifier, + description($), + fc.uniqueArray( + InputValueDefinition(tie, $), + $.InputValueDefinition! + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ), +) + +const InputValueDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.InputValueDefinition), + identifier, + description($), + TypeNode(tie, $), + ConstValueNode(tie, $), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ), +) + +const VariableDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.VariableDefinition), + identifier, + TypeNode(tie, $), + ConstValueNode(tie, $), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ), +) + +const Directive = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => { + return fc.tuple( + fc.constant(byTag.Directive), + identifier, + fc.uniqueArray( + Argument(tie, $), + $.Argument! + ), + ) +} + +const DirectiveDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.DirectiveDefinition), + identifier, + description($), + fc.boolean(), + fc.uniqueArray( + target, + $.DirectiveDefinition! + ), + fc.uniqueArray( + InputValueDefinition(tie, $), + $.InputValueDefinition! + ), +) + +const FragmentDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.FragmentDefinition), + identifier, + identifier, + NonEmptySelectionSet(tie, $), + fc.uniqueArray( + Directive(tie, $), + $.Directive! + ), +) + +const FragmentSpread = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.FragmentSpread), + identifier, + fc.uniqueArray( + Directive(tie, $), + $.Directive! + ), +) + +const InlineFragment = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.InlineFragment), + identifier, + NonEmptySelectionSet(tie, $), + fc.uniqueArray( + Directive(tie, $), + $.Directive! + ), +) + +const NonEmptySelectionSet = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.SelectionSet), + fc.uniqueArray( + Selection(tie, $), + { ...$.SelectionSet, minLength: 1 } + ), +) + +const OperationDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.OperationDefinition), + identifier, + operationType, + NonEmptySelectionSet(tie, $), + fc.uniqueArray( + VariableDefinition(tie, $), + $.VariableDefinition! + ), + fc.uniqueArray( + Directive(tie, $), + $.Directive! + ), +) + +const OperationTypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.OperationTypeDefinition), + identifier, + operationType, +) + +const SelectionSet = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.SelectionSet), + fc.uniqueArray( + Selection(tie, $), + $.SelectionSet! + ), +) + +const SchemaDefinition = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.SchemaDefinition), + identifier, + description($), + fc.uniqueArray( + OperationTypeDefinition(tie, $), + $.SchemaDefinition!, + ), + fc.uniqueArray( + ConstDirective(tie, $), + $.Directive! + ) +) + +const Document = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => { + return fc.tuple( + fc.constant(byTag.Document), + fc.uniqueArray( + Definition(tie, $), + $.Document! + ), + ) +} + +///////////////// +/// DERIVED /// + +const ConstArgument = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.Argument), + identifier, + ConstValueNode(tie, $), +) + +const ConstDirective = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.Directive), + identifier, + fc.uniqueArray( + ConstArgument(tie, $), + $.Argument! + ), +) + +const TypeNode = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + NamedType(tie, $), + tie('ListType'), + tie('NonNullType'), +) + +/** + * ## {@link Selection `Selection`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L355 + */ +const Selection = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + tie('Field'), + tie('FragmentSpread'), + tie('InlineFragment'), +) + +const ObjectValue = (_tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.ObjectValue), + fc.uniqueArray( + fc.tuple( + identifier, + jsonValue['*'], + ), + $.ObjectValue! + ), +) + +/** + * ## {@link ValueNode `ValueNode`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L411-L420 + */ +const ValueNode = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + IntValue(tie, $), + FloatValue(tie, $), + StringValue(tie, $), + BooleanValue(tie, $), + NullValue(tie, $), + EnumValue(tie, $), + ListValue(tie, $), + ObjectValue(tie, $), +) + +const ObjectField = (tie: fc.LetrecTypedTie, $: Constraints): fc.Arbitrary => fc.tuple( + fc.constant(byTag.ObjectField), + identifier, + ValueNode(tie, $), +) + +const ConstListValue = (tie: fc.LetrecTypedTie, $: Constraints) => fc.tuple( + fc.constant(byTag.ListValue), + fc.array(ConstValueNode(tie, $)), +) + +/** + * ## {@link ConstValueNode `ConstValueNode`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L422 + */ +const ConstValueNode = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + IntValue(tie, $), + FloatValue(tie, $), + StringValue(tie, $), + BooleanValue(tie, $), + NullValue(tie, $), + EnumValue(tie, $), + // TODO: + // tie('ConstListValue'), + // tie('ConstObjectValue'), +) + +/** + * ## {@link ExecutableDefinition `ExecutableDefinition`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L313-L315 + */ +const ExecutableDefinition = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + tie('OperationDefinition'), + tie('FragmentDefinition'), +) + +/** + * ## {@link TypeDefinition `TypeDefinition`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L568-L574 + */ +const TypeDefinition = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + tie('ScalarTypeDefinition'), + tie('ObjectTypeDefinition'), + tie('InterfaceTypeDefinition'), + tie('UnionTypeDefinition'), + tie('EnumTypeDefinition'), + tie('InputObjectTypeDefinition'), +) + +/** + * ## {@link TypeSystemDefinition `TypeSystemDefinition`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L546-L549 + */ +const TypeSystemDefinition = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + TypeDefinition(tie, $), + tie('SchemaDefinition'), + tie('DirectiveDefinition'), + // TypeSystemExtension(tie), +) + +/** + * ## {@link Definition `Definition`} + * See also: + * - https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts#L308-L311 + */ +const Definition = (tie: fc.LetrecTypedTie, $: Constraints) => fc.oneof( + ExecutableDefinition(tie, $), + TypeSystemDefinition(tie, $), +) + +export const Seed = { + Boolean, + Float, + Int, + ID, + Null, + Number, + String, + NamedType, + NullValue, + BooleanValue, + FloatValue, + IntValue, + StringValue, + ScalarTypeDefinition, + EnumValue, + Variable, + EnumValueDefinition, + ObjectValue, + ObjectField, + ListValue, + ListType, + NonNullType, + UnionTypeDefinition, + EnumTypeDefinition, + Field, + FieldDefinition, + ObjectTypeDefinition, + InterfaceTypeDefinition, + Argument, + InputObjectTypeDefinition, + InputValueDefinition, + VariableDefinition, + Directive, + DirectiveDefinition, + Document, + FragmentDefinition, + FragmentSpread, + InlineFragment, + OperationDefinition, + OperationTypeDefinition, + SelectionSet, + SchemaDefinition, + // SchemaExtension, +} satisfies { [K in F.Kind | F.NamedType]: (tie: fc.LetrecTypedTie, $: Constraints) => fc.Arbitrary } + +// const SchemaExtension = (tie: fc.LetrecTypedTie) => fc.tuple( +// fc.constant(byTag.SchemaExtension), +// fc.uniqueArray(tie('*')), +// directives(tie('*')), +// ) diff --git a/packages/graphql-test/src/generator.ts b/packages/graphql-test/src/generator.ts new file mode 100644 index 00000000..e8bb0de6 --- /dev/null +++ b/packages/graphql-test/src/generator.ts @@ -0,0 +1,411 @@ +import type * as gql from 'graphql' +import * as fc from 'fast-check' + +import type { inline } from '@traversable/registry' +import { + Array_isArray, + fn, + has, + isKeyOf, + Number_isFinite, + Number_isSafeInteger, + Object_assign, + Object_entries, + omit, + PATTERN, + pick, +} from '@traversable/registry' +import { Json } from '@traversable/json' + +import { Config } from './generator-options.js' +import * as Bounds from './generator-bounds.js' + +import { AST, Kind, NamedType, OperationType } from '@traversable/graphql-types' + +import type { Tag } from './generator-seed.js' +import { bySeed, Seed } from './generator-seed.js' +import { fold } from './functor.js' + +const identifier = fc.stringMatching(new RegExp(PATTERN.identifier, 'u')) + +const nameNode = (name: T): AST.NameNode => ({ + kind: 'Name', + value: name, +}) + +const namedTypeNode = (name: T) => ({ + kind: Kind.NamedType, + name: nameNode(name), +}) satisfies AST.NamedTypeNode + +const nullValueNode = () => ({ + kind: Kind.NullValue, +}) satisfies AST.NullValueNode + +const booleanValueNode = (value: boolean) => ({ + kind: Kind.BooleanValue, + value, +}) satisfies AST.BooleanValueNode + +const intValueNode = (value: number) => ({ + kind: Kind.IntValue, + value, +}) satisfies AST.IntValueNode + +const floatValueNode = (value: number) => ({ + kind: Kind.FloatValue, + value, +}) satisfies AST.FloatValueNode + +const stringValueNode = (value: string, block?: boolean) => ({ + kind: Kind.StringValue, + value, + block: block ?? new RegExp(PATTERN.newline).test(value), +}) satisfies AST.StringValueNode + +const listValueNode = (values: readonly AST.ValueNode[]) => ({ + kind: Kind.ListValue, + values, +}) satisfies AST.ListValueNode + +const objectFieldNodes = (fields: { [x: string]: Json }) => Object_entries(fields).map( + ([k, json]) => ({ + kind: Kind.ObjectField, + name: nameNode(k), + value: valueNodeFromJson(json), + }) +) satisfies AST.ObjectFieldNode[] + +const variableNode = (name: string): AST.VariableNode => ({ + kind: Kind.Variable, + name: nameNode(name), +}) + +const enumValueDefinition = (name: string): AST.EnumValueDefinitionNode => ({ + kind: Kind.EnumValueDefinition, + name: nameNode(name), +}) + +const operationTypeDefinition = (name: string, operation: OperationType): AST.OperationTypeDefinitionNode => ({ + kind: Kind.OperationTypeDefinition, + type: namedTypeNode(name), + operation, +}) + +const valueNodeFromJson = Json.fold((x) => { + switch (true) { + default: return x satisfies never + case x == null: return nullValueNode() + case x === true: + case x === false: return booleanValueNode(x) + case Number_isSafeInteger(x): return intValueNode(x) + case Number_isFinite(x): return floatValueNode(x) + case typeof x === 'string': return stringValueNode(x) + case Array_isArray(x): return listValueNode(x) + case !!x && typeof x === 'object': return { + kind: Kind.ObjectValue, + fields: Object_entries(x).map(([k, v]) => ({ + kind: Kind.ObjectField, + name: nameNode(k), + value: v, + })) + } + } +}) + +export function pickAndSortNodes(nodes: readonly ([K, fc.Arbitrary])[]): ($: Config) => K[] { + return ({ include, exclude, sortBias }) => nodes + .map(([k]) => k) + .filter((x) => + (include ? include.includes(x as never) : true) && + (exclude ? !exclude.includes(x as never) : true) + ) + .sort((lk, rk) => { + if ( + has(lk, (bias) => typeof bias === 'number')(sortBias) && + has(rk, (bias) => typeof bias === 'number')(sortBias) + ) { + return sortBias[lk] < sortBias[rk] ? -1 : sortBias[lk] > sortBias[rk] ? 1 : 0 + } else { + return 0 + } + }) +} + +export declare namespace Gen { + type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $) => fc.Arbitrary } + type Values = never | T[Exclude] + type InferArb = S extends fc.Arbitrary ? T : never + type Builder = T & BuilderStar + interface BuilderStar { ['*']: fc.Arbitrary>> } + type BuildBuilder, Out extends {} = BuilderBase> = never | Builder + type BuilderBase, $ extends ParseOptions = ParseOptions> = never | + & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) + & { [K in Exclude<$['include'], $['exclude']>]: fc.Arbitrary } + type ParseOptions> = never | { + include: Options['include'] extends readonly unknown[] ? Options['include'][number] : keyof T + exclude: Options['exclude'] extends readonly unknown[] ? Options['exclude'][number] : never + root: Options['root'] extends keyof T ? T[Options['root']] : never + } +} + +export function Gen(base: Gen.Base>): + >( + options?: Options, + overrides?: Partial>> + ) => Gen.BuildBuilder + +export function Gen(base: Gen.Base>) { + return >( + options?: Options, + overrides?: Partial>> + ): Builder => { + return fc.letrec(Builder(base)(options, overrides)) + } +} + +export interface Builder extends inline<{ [K in Tag]+?: fc.Arbitrary }> { + root?: fc.Arbitrary + ['*']: fc.Arbitrary +} + +export function Builder(base: Gen.Base>): + >( + options?: Options, + overrides?: Partial>> + ) => (tie: fc.LetrecLooselyTypedTie) => Builder + +export function Builder(base: Gen.Base>) { + return (options?: Config.Options, overrides?: Partial>) => { + const $ = Config.parseOptions(options) + return (tie: fc.LetrecLooselyTypedTie) => { + const builder = fn.pipe( + { ...base, ...overrides }, + (x) => pick(x, $.include), + (x) => omit(x, $.exclude as []), + (x) => fn.map(x, (f) => f(tie, $)), + ) + const nodes = pickAndSortNodes(Object_entries(builder))($) + const star = fc.oneof(...fn.map(nodes, (k) => builder[k])) + + return Object_assign( + builder, + { ['*']: star } + ) + } + } +} + +const SchemaMap = { + Name: ([, name]) => nameNode(name), + Null: (_) => namedTypeNode(NamedType.Null), + Boolean: (_) => namedTypeNode(NamedType.Boolean), + Int: (_) => namedTypeNode(NamedType.Int), + Float: (_) => namedTypeNode(NamedType.Float), + Number: (_) => namedTypeNode(NamedType.Number), + String: (_) => namedTypeNode(NamedType.String), + ID: (_) => namedTypeNode(NamedType.ID), + NullValue: (_) => ({ kind: Kind.NullValue }), + BooleanValue: ([, value]) => ({ + kind: Kind.BooleanValue, + value, + }), + FloatValue: ([, value]) => ({ + kind: Kind.FloatValue, + value, + }), + IntValue: ([, value]) => ({ + kind: Kind.IntValue, + value, + }), + StringValue: ([, value, block]) => ({ + kind: Kind.StringValue, + value, + block, + }), + ScalarTypeDefinition: ([, name, description]) => ({ + kind: Kind.ScalarTypeDefinition, + name: nameNode(name), + ...description && { description: stringValueNode(...description) }, + }), + EnumValue: ([, value]) => ({ + kind: Kind.EnumValue, + value, + }), + EnumValueDefinition: ([, name]) => ({ + kind: Kind.EnumValueDefinition, + name: nameNode(name), + }), + ListValue: ([, values]) => ({ + kind: Kind.ListValue, + values: values.map((value) => valueNodeFromJson(value)), + }), + ObjectValue: ([, fields]) => ({ + kind: Kind.ObjectValue, + fields: fields.map(([name, value]) => ({ + kind: Kind.ObjectField, + name: nameNode(name), + value: valueNodeFromJson(value), + })) + }), + Argument: ([, name, value]) => ({ + kind: Kind.Argument, + name: nameNode(name), + value, + }), + Document: ([, definitions]) => ({ + kind: Kind.Document, + definitions + }), + Directive: ([__kind, name, args]) => ({ + kind: Kind.Directive, + name: nameNode(name), + arguments: args, + }), + DirectiveDefinition: ([, name, description, repeatable, locations, args]) => ({ + kind: Kind.DirectiveDefinition, + name: nameNode(name), + repeatable, + locations: locations.map((location) => nameNode(location)), + ...description && { description: stringValueNode(...description) }, + ...args.length && { arguments: args }, + }), + EnumTypeDefinition: ([, name, description, values]) => ({ + kind: Kind.EnumTypeDefinition, + name: nameNode(name), + values: values.map(enumValueDefinition), + ...description && { description: stringValueNode(...description) }, + }), + Field: ([, name, alias, selectionSet, args, directives]) => ({ + kind: Kind.Field, + name: nameNode(name), + alias: nameNode(alias), + ...selectionSet !== null && { selectionSet }, + ...args.length && { arguments: args }, + ...directives.length && { directives }, + }), + FieldDefinition: ([, name, description, type, args, directives]) => ({ + kind: Kind.FieldDefinition, + name: nameNode(name), + type, + ...description && { description: stringValueNode(...description) }, + ...args.length && { arguments: args }, + ...directives.length && { directives }, + }), + FragmentDefinition: ([, name, typeCondition, selectionSet, directives]) => ({ + kind: Kind.FragmentDefinition, + name: nameNode(name), + typeCondition: namedTypeNode(typeCondition), + selectionSet, + ...directives.length && { directives }, + }), + FragmentSpread: ([, name, directives]) => ({ + kind: Kind.FragmentSpread, + name: nameNode(name), + ...directives.length && { directives }, + }), + InlineFragment: ([, typeCondition, selectionSet, directives]) => ({ + kind: Kind.InlineFragment, + selectionSet, + typeCondition: namedTypeNode(typeCondition), + ...directives.length && { directives }, + }), + InputObjectTypeDefinition: ([, name, description, fields, directives]) => ({ + kind: Kind.InputObjectTypeDefinition, + name: nameNode(name), + fields, + ...description && { description: stringValueNode(...description) }, + ...directives.length && { directives }, + }), + InputValueDefinition: ([, name, description, type, defaultValue, directives]) => ({ + kind: Kind.InputValueDefinition, + name: nameNode(name), + type, + ...defaultValue != null && { defaultValue }, + ...description && { description: stringValueNode(...description) }, + ...directives.length && { directives }, + }), + InterfaceTypeDefinition: ([, name, description, fields, interfaces, directives]) => ({ + kind: Kind.InterfaceTypeDefinition, + name: nameNode(name), + fields, + interfaces, + ...description && { description: stringValueNode(...description) }, + ...directives.length && { directives }, + }), + ListType: ([, type]) => ({ + kind: Kind.ListType, + type, + }), + NamedType: ([, name]) => ({ + kind: Kind.NamedType, + name: nameNode(name as never), // <-- TODO + }), + NonNullType: ([, type]) => ({ + kind: Kind.NonNullType, + type, + }), + ObjectField: ([, name, value]) => ({ + kind: Kind.ObjectField, + name: nameNode(name), + value: value as AST.ValueNode, // <-- TODO + // value: valueNodeFromJson(value), + }), + ObjectTypeDefinition: ([, name, description, fields, interfaces, directives]) => ({ + kind: Kind.ObjectTypeDefinition, + name: nameNode(name), + fields, + interfaces, + ...description && { description: stringValueNode(...description) }, + ...directives.length && { directives }, + }), + OperationDefinition: ([, name, operationType, selectionSet, variableDefinitions, directives]) => ({ + kind: Kind.OperationDefinition, + name: nameNode(name), // <-- TODO: optional? + operation: operationType, + selectionSet, + ...variableDefinitions.length && { variableDefinitions }, + ...directives.length && { directives }, + }), + OperationTypeDefinition: ([, type, operation]) => ({ + kind: Kind.OperationTypeDefinition, + type: namedTypeNode(type), + operation, + }), + SchemaDefinition: ([, _, description, operationTypes, directives]) => ({ + kind: Kind.SchemaDefinition, + operationTypes: operationTypes.map(([, name, operationType]) => operationTypeDefinition(name, operationType)), + ...description && { description: stringValueNode(...description) }, + ...directives.length && { directives }, + }), + SelectionSet: ([, selections]) => ({ + kind: Kind.SelectionSet, + selections, + }), + UnionTypeDefinition: ([, name, types, directives]) => ({ + kind: Kind.UnionTypeDefinition, + name: nameNode(name), + types, + ...directives.length && { directives }, + }), + Variable: ([, name, description]) => ({ + kind: Kind.Variable, + name: nameNode(name), + ...description && { description: stringValueNode(...description) }, + }), + VariableDefinition: ([, name, type, defaultValue, directives]) => ({ + kind: Kind.VariableDefinition, + variable: variableNode(name), + type, + ...defaultValue != null && { defaultValue }, + ...directives.length && { directives }, + }), +} satisfies ( + & { [K in keyof AST.Catalog.byKind]: (x: Seed[K]) => AST.Catalog.byKind[K] } + & { [K in keyof AST.Catalog.byNamedType]: (x: Seed[K]) => AST.Catalog.byNamedType[K] } + ) + +export const SeedGenerator = Gen(Seed) + +export const seedToSchema = fold((x) => SchemaMap[bySeed[x[0]]](x as never)) + diff --git a/packages/graphql-test/src/index.ts b/packages/graphql-test/src/index.ts new file mode 100644 index 00000000..9fd152fb --- /dev/null +++ b/packages/graphql-test/src/index.ts @@ -0,0 +1 @@ +export * from './exports.js' \ No newline at end of file diff --git a/packages/graphql-test/src/version.ts b/packages/graphql-test/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/graphql-test/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/graphql-test/test/generator.test.ts b/packages/graphql-test/test/generator.test.ts new file mode 100644 index 00000000..b2693249 --- /dev/null +++ b/packages/graphql-test/test/generator.test.ts @@ -0,0 +1,29 @@ +import * as vi from 'vitest' +import * as fc from 'fast-check' +import prettier from '@prettier/sync' +import { SeedGenerator, seedToSchema } from '@traversable/graphql-test' +import * as F from '@traversable/graphql-types' + +function format(src: string) { + return prettier.format(src, { parser: 'graphql', printWidth: 60 }) +} + +const Builder = SeedGenerator() + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/graphql-test❳', () => { + vi.test('〖⛳️〗› ❲SeedGenerator❳: generates valid seeds', () => { + fc.assert( + fc.property( + Builder.Document, + (seed) => { + const schema = seedToSchema(seed) + vi.assert.doesNotThrow(() => format(F.toString(schema))) + } + ), + { + endOnFailure: true, + // numRuns: 4_000, + } + ) + }) +}) diff --git a/packages/graphql-test/test/version.test.ts b/packages/graphql-test/test/version.test.ts new file mode 100644 index 00000000..5494d934 --- /dev/null +++ b/packages/graphql-test/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/graphql-test' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/graphql-test❳', () => { + vi.test('〖⛳️〗› ❲VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) diff --git a/packages/graphql-test/tsconfig.build.json b/packages/graphql-test/tsconfig.build.json new file mode 100644 index 00000000..23cce151 --- /dev/null +++ b/packages/graphql-test/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [ + { "path": "../graphql-types" }, + { "path": "../json" }, + { "path": "../registry" } + ] +} diff --git a/packages/graphql-test/tsconfig.json b/packages/graphql-test/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/graphql-test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/graphql-test/tsconfig.src.json b/packages/graphql-test/tsconfig.src.json new file mode 100644 index 00000000..851d36a7 --- /dev/null +++ b/packages/graphql-test/tsconfig.src.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [ + { "path": "../graphql-types" }, + { "path": "../json" }, + { "path": "../registry" } + ], + "include": ["src"] +} diff --git a/packages/graphql-test/tsconfig.test.json b/packages/graphql-test/tsconfig.test.json new file mode 100644 index 00000000..298a1b08 --- /dev/null +++ b/packages/graphql-test/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../graphql-types" }, + { "path": "../json" }, + { "path": "../registry" } + ], + "include": ["test"] +} diff --git a/packages/graphql-test/vite.config.ts b/packages/graphql-test/vite.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/graphql-test/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/packages/graphql-types/README.md b/packages/graphql-types/README.md new file mode 100644 index 00000000..9f380e9b --- /dev/null +++ b/packages/graphql-types/README.md @@ -0,0 +1,40 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝗴𝗿𝗮𝗽𝗵𝗾𝗹-𝘁𝘆𝗽𝗲𝘀

+
+ +

+ "Low-level" primitives like types and predicates. Most of this package is re-exported by @traversable/graphql, so you probably won't need to use it directly. +

+ +
+ NPM Version +   + TypeScript +   + License +   + npm +   +
+ +
+ + Static Badge +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
diff --git a/packages/graphql-types/package.json b/packages/graphql-types/package.json new file mode 100644 index 00000000..9d37f97f --- /dev/null +++ b/packages/graphql-types/package.json @@ -0,0 +1,59 @@ +{ + "name": "@traversable/graphql-types", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/graphql-types" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": [ + "**/*.ts" + ] + }, + "generateIndex": { + "include": [ + "**/*.ts" + ] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/registry": "workspace:^", + "graphql": ">= 16" + }, + "peerDependenciesMeta": { + "@traversable/registry": { "optional": false }, + "graphql": { "optional": true } + }, + "devDependencies": { + "@prettier/sync": "catalog:", + "@traversable/registry": "workspace:^", + "graphql": "catalog:" + } +} diff --git a/packages/graphql-types/src/__generated__/__manifest__.ts b/packages/graphql-types/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..01678ec9 --- /dev/null +++ b/packages/graphql-types/src/__generated__/__manifest__.ts @@ -0,0 +1,55 @@ +export default { + "name": "@traversable/graphql-types", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/graphql-types" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": ["**/*.ts"] + }, + "generateIndex": { + "include": ["**/*.ts"] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/registry": "workspace:^", + "graphql": ">= 16" + }, + "peerDependenciesMeta": { + "@traversable/registry": { "optional": false }, + "graphql": { "optional": true } + }, + "devDependencies": { + "@prettier/sync": "catalog:", + "@traversable/registry": "workspace:^", + "graphql": "catalog:" + } +} as const \ No newline at end of file diff --git a/packages/graphql-types/src/exports.ts b/packages/graphql-types/src/exports.ts new file mode 100644 index 00000000..9107cc06 --- /dev/null +++ b/packages/graphql-types/src/exports.ts @@ -0,0 +1,69 @@ +export { VERSION } from './version.js' + +export { toType } from './to-type.js' +export { toString } from './to-string.js' + +export type { + AST, + Index, +} from './functor.js' + +export { + Functor, + DirectiveTarget, + DirectiveTargets, + Kind, + Kinds, + NamedType, + NamedTypes, + OperationType, + Tag, + Tags, + createIndex, + fold, + isArgumentNode, + isBooleanNode, + isBooleanValueNode, + isDirectiveNode, + isDocumentNode, + isEnumTypeDefinitionNode, + isEnumValueDefinitionNode, + isEnumValueNode, + isFieldDefinitionNode, + isFieldNode, + isFloatNode, + isFloatValueNode, + isFragmentDefinitionNode, + isFragmentSpreadNode, + isIDNode, + isInlineFragmentNode, + isInputObjectTypeDefinitionNode, + isInputValueDefinitionNode, + isInterfaceTypeDefinitionNode, + isIntNode, + isIntValueNode, + isListNode, + isListValueNode, + isMutationOperation, + isNamedTypeNode, + isNonNullTypeNode, + isNullaryNode, + isNullValueNode, + isNumberNode, + isObjectTypeDefinitionNode, + isObjectValueNode, + isOperationDefinitionNode, + isOperationTypeDefinitionNode, + isQueryOperation, + isRefNode, + isScalarTypeDefinition, + isSchemaDefinitionNode, + isSelectionSetNode, + isStringNode, + isStringValueNode, + isSubscriptionOperation, + isUnaryNode, + isUnionTypeDefinitionNode, + isValueNode, + isVariableNode, +} from './functor.js' diff --git a/packages/graphql-types/src/functor.ts b/packages/graphql-types/src/functor.ts new file mode 100644 index 00000000..c85f019f --- /dev/null +++ b/packages/graphql-types/src/functor.ts @@ -0,0 +1,1285 @@ +import type * as GQL from 'graphql' + +import type * as T from '@traversable/registry' +import { fn, has, Object_create, Object_values, topologicalSort } from '@traversable/registry' + +export type Algebra = { + (src: AST.DocumentNode, ix?: Index): { order: string[], byName: Record T> } + (src: GQL.DocumentNode, ix?: Index): { order: string[], byName: Record T> } + (src: AST.DocumentNode, ix?: Index): { order: string[], byName: Record T> } +} + +/** + * ## {@link Kind `Kind`} + * + * [Reference](https://github.com/graphql/graphql-js/blob/16.x.x/src/language/kinds.ts#L4) + */ + +export const Kind = { + Argument: 'Argument', + BooleanValue: 'BooleanValue', + Directive: 'Directive', + DirectiveDefinition: 'DirectiveDefinition', + Document: 'Document', + EnumTypeDefinition: 'EnumTypeDefinition', + EnumValue: 'EnumValue', + EnumValueDefinition: 'EnumValueDefinition', + Field: 'Field', + FieldDefinition: 'FieldDefinition', + FloatValue: 'FloatValue', + FragmentDefinition: 'FragmentDefinition', + FragmentSpread: 'FragmentSpread', + InlineFragment: 'InlineFragment', + InputObjectTypeDefinition: 'InputObjectTypeDefinition', + InputValueDefinition: 'InputValueDefinition', + InterfaceTypeDefinition: 'InterfaceTypeDefinition', + IntValue: 'IntValue', + ListType: 'ListType', + ListValue: 'ListValue', + // Name: 'Name', + NamedType: 'NamedType', + NonNullType: 'NonNullType', + NullValue: 'NullValue', + ObjectField: 'ObjectField', + ObjectTypeDefinition: 'ObjectTypeDefinition', + ObjectValue: 'ObjectValue', + OperationDefinition: 'OperationDefinition', + OperationTypeDefinition: 'OperationTypeDefinition', + ScalarTypeDefinition: 'ScalarTypeDefinition', + SchemaDefinition: 'SchemaDefinition', + // SchemaExtension: 'SchemaExtension', + SelectionSet: 'SelectionSet', + StringValue: 'StringValue', + UnionTypeDefinition: 'UnionTypeDefinition', + Variable: 'Variable', + VariableDefinition: 'VariableDefinition', +} as const + +export type DirectiveTarget = typeof DirectiveTargets[number] +export const DirectiveTarget = { + ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION', + FIELD_DEFINITION: 'FIELD_DEFINITION', + ENUM_VALUE: 'ENUM_VALUE', + INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION', +} as const +export const DirectiveTargets = Object_values(DirectiveTarget) + +export type Kind = typeof Kinds[number] +export const Kinds = Object_values(Kind) + +export declare namespace Kind { + type Argument = typeof Kind.Argument + type BooleanValue = typeof Kind.BooleanValue + type Directive = typeof Kind.Directive + type DirectiveDefinition = typeof Kind.DirectiveDefinition + type Document = typeof Kind.Document + type EnumTypeDefinition = typeof Kind.EnumTypeDefinition + type EnumValue = typeof Kind.EnumValue + type EnumValueDefinition = typeof Kind.EnumValueDefinition + type Field = typeof Kind.Field + type FieldDefinition = typeof Kind.FieldDefinition + type FloatValue = typeof Kind.FloatValue + type FragmentDefinition = typeof Kind.FragmentDefinition + type FragmentSpread = typeof Kind.FragmentSpread + type InlineFragment = typeof Kind.InlineFragment + type InputObjectTypeDefinition = typeof Kind.InputObjectTypeDefinition + type InputValueDefinition = typeof Kind.InputValueDefinition + type InterfaceTypeDefinition = typeof Kind.InterfaceTypeDefinition + type IntValue = typeof Kind.IntValue + type ListType = typeof Kind.ListType + type ListValue = typeof Kind.ListValue + // type Name = typeof Kind.Name + type NamedType = typeof Kind.NamedType + type NonNullType = typeof Kind.NonNullType + type NullValue = typeof Kind.NullValue + type ObjectField = typeof Kind.ObjectField + type ObjectTypeDefinition = typeof Kind.ObjectTypeDefinition + type ObjectValue = typeof Kind.ObjectValue + type OperationDefinition = typeof Kind.OperationDefinition + type OperationTypeDefinition = typeof Kind.OperationTypeDefinition + type ScalarTypeDefinition = typeof Kind.ScalarTypeDefinition + type SchemaDefinition = typeof Kind.SchemaDefinition + type SelectionSet = typeof Kind.SelectionSet + type StringValue = typeof Kind.StringValue + type UnionTypeDefinition = typeof Kind.UnionTypeDefinition + type Variable = typeof Kind.Variable + type VariableDefinition = typeof Kind.VariableDefinition + // type SchemaExtension = typeof Kind.SchemaExtension +} + +export const NamedType = { + Null: 'Null', + Boolean: 'Boolean', + Float: 'Float', + ID: 'ID', + Int: 'Int', + Number: 'Number', + String: 'String', +} as const + +export type NamedType = typeof NamedTypes[number] +export const NamedTypes = Object_values(NamedType) + +export declare namespace NamedType { + type Null = typeof NamedType.Null + type Boolean = typeof NamedType.Boolean + type Float = typeof NamedType.Float + type ID = typeof NamedType.ID + type Int = typeof NamedType.Int + type Number = typeof NamedType.Number + type String = typeof NamedType.String +} + +export const OperationType = { + Query: 'query', + Mutation: 'mutation', + Subscription: 'subscription', +} as const + +export type OperationType = typeof OperationTypes[number] +export const OperationTypes = Object_values(OperationType) + +export declare namespace OperationType { + type Query = typeof OperationType.Query + type Mutation = typeof OperationType.Mutation + type Subscription = typeof OperationType.Subscription +} + +export const Tag = { ...Kind, ...NamedType } +export type Tag = typeof Tags[number] +export const Tags = Object_values(Tag) + +export declare namespace Catalog { + type byKind = { [Node in F as Node['kind']]: Node } + type byNamedType = { + [NamedType.Null]: AST.NullNode + [NamedType.Int]: AST.IntNode + [NamedType.Float]: AST.FloatNode + [NamedType.Number]: AST.NumberNode + [NamedType.Boolean]: AST.BooleanNode + [NamedType.String]: AST.StringNode + [NamedType.ID]: AST.IDNode + } +} + +export interface Location { + start: number + end: number +} + +export interface SelectionSetNode { + kind: Kind.SelectionSet + selections: readonly T[] + loc?: Location +} + +export interface NameNode { + kind: 'Name' + value: Value + loc?: Location +} + +export interface NamedTypeNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +/** + * ## {@link RefNode `RefNode`} + * + * A {@link RefNode `RefNode`} is a named type that is not one of the built-in types. + */ +export interface RefNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface ArgumentNode { + kind: Kind.Argument + loc?: Location + name: NameNode + value: T +} + +export interface DocumentNode { + kind: Kind.Document + definitions: readonly T[] + loc?: Location +} + +export interface InputValueDefinitionNode { + kind: Kind.InputValueDefinition + name: NameNode + type: T + defaultValue?: T + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface InputObjectTypeDefinitionNode { + kind: Kind.InputObjectTypeDefinition + name: NameNode + fields: readonly T[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface VariableNode { + kind: Kind.Variable + name: NameNode + loc?: Location +} + +export interface VariableDefinitionNode { + kind: Kind.VariableDefinition + variable: VariableNode + type: T + defaultValue?: T + directives?: readonly T[] + loc?: Location +} + +export interface ScalarTypeDefinitionNode { + kind: Kind.ScalarTypeDefinition + name: NameNode + directives?: T[] + description?: StringValueNode + loc?: Location +} + +export interface NullNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface BooleanNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface IntNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface NumberNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface FloatNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface StringNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface IDNode { + kind: Kind.NamedType + name: NameNode + loc?: Location +} + +export interface EnumValueDefinitionNode { + kind: Kind.EnumValueDefinition + name: NameNode + loc?: Location +} + +export interface EnumValueNode { + kind: Kind.EnumValue + value: string + description?: StringValueNode + loc?: Location +} + +export interface EnumTypeDefinitionNode { + kind: Kind.EnumTypeDefinition + name: NameNode + values: readonly EnumValueDefinitionNode[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface NonNullTypeNode { + kind: Kind.NonNullType + type: T + loc?: Location +} + +export interface ListNode { + kind: Kind.ListType + type: T + loc?: Location +} + +export interface FieldDefinitionNode { + kind: Kind.FieldDefinition + name: NameNode + type: T + arguments?: readonly T[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface FieldNode { + kind: Kind.Field + alias: NameNode | undefined + name: NameNode + selectionSet?: T + arguments?: readonly T[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface ObjectTypeDefinitionNode { + kind: Kind.ObjectTypeDefinition + name: NameNode + fields: readonly T[] + interfaces: readonly T[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface InterfaceTypeDefinitionNode { + kind: Kind.InterfaceTypeDefinition + name: NameNode + fields: readonly T[] + interfaces: readonly T[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface UnionTypeDefinitionNode { + kind: Kind.UnionTypeDefinition + name: NameNode + types: readonly T[] + directives?: readonly T[] + description?: StringValueNode + loc?: Location +} + +export interface FragmentDefinitionNode { + kind: Kind.FragmentDefinition + name: NameNode + typeCondition: NamedTypeNode + directives?: readonly T[] + selectionSet: T + loc?: Location +} + +export interface FragmentSpreadNode { + kind: Kind.FragmentSpread + name: NameNode + directives?: readonly T[] + loc?: Location +} + +export interface InlineFragmentNode { + kind: Kind.InlineFragment + typeCondition?: NamedTypeNode + directives?: readonly T[] + selectionSet: T + loc?: Location +} + +export interface IntValueNode { + kind: Kind.IntValue + value: number + loc?: Location +} + +export interface FloatValueNode { + kind: Kind.FloatValue + value: number + loc?: Location +} + +export interface StringValueNode { + kind: Kind.StringValue + value: string + block: boolean + loc?: Location +} + +export interface BooleanValueNode { + kind: Kind.BooleanValue + value: boolean + loc?: Location +} + +export interface NullValueNode { + kind: Kind.NullValue + loc?: Location +} + +export interface ListValueNode { + kind: Kind.ListValue + values: readonly ValueNode[] + loc?: Location +} + +export interface ObjectValueNode { + kind: Kind.ObjectValue + fields: readonly ObjectFieldNode[] + loc?: Location +} + +export interface ObjectFieldNode { + kind: Kind.ObjectField + name: NameNode + value: ValueNode + loc?: Location +} + +export interface DirectiveNode { + kind: Kind.Directive + name: NameNode + arguments?: readonly T[] + loc?: Location +} + +export interface DirectiveDefinitionNode { + kind: Kind.DirectiveDefinition + name: NameNode + arguments?: readonly T[] + repeatable: boolean + locations: readonly NameNode[] + description?: StringValueNode + loc?: Location +} + +export interface QueryOperation { + kind: Kind.OperationDefinition + operation: OperationType.Query + selectionSet: T + name?: NameNode + variableDefinitions?: readonly T[] + directives?: readonly T[] + loc?: Location +} + +export interface MutationOperation { + kind: Kind.OperationDefinition + operation: OperationType.Mutation + selectionSet: T + name?: NameNode + variableDefinitions?: readonly T[] + directives?: readonly T[] + loc?: Location +} + +export interface SubscriptionOperation { + kind: Kind.OperationDefinition + operation: OperationType.Subscription + name?: NameNode + variableDefinitions?: readonly T[] + directives?: readonly T[] + selectionSet: T + loc?: Location +} + +export interface OperationTypeDefinitionNode { + kind: Kind.OperationTypeDefinition + type: NamedTypeNode + operation: OperationType + loc?: Location +} + +export interface SchemaDefinitionNode { + kind: Kind.SchemaDefinition + directives?: readonly T[] + operationTypes: readonly OperationTypeDefinitionNode[] + description?: StringValueNode + loc?: Location +} + +export type ValueNode = + | VariableNode + | IntValueNode + | FloatValueNode + | StringValueNode + | BooleanValueNode + | NullValueNode + | ListValueNode + | ObjectValueNode + | EnumValueNode + +export type Nullary = + | NullNode + | NameNode + | BooleanNode + | IntNode + | NumberNode + | FloatNode + | StringNode + | IDNode + | EnumValueDefinitionNode + | ObjectFieldNode + | OperationTypeDefinitionNode + +export type OperationDefinitionNode = + | QueryOperation + | MutationOperation + | SubscriptionOperation + +export type Unary = + | NonNullTypeNode + | ListNode + | FieldNode + | FieldDefinitionNode + | ScalarTypeDefinitionNode + | ObjectTypeDefinitionNode + | InterfaceTypeDefinitionNode + | UnionTypeDefinitionNode + | ArgumentNode + | InputValueDefinitionNode + | InputObjectTypeDefinitionNode + | SelectionSetNode + | FragmentDefinitionNode + | FragmentSpreadNode + | InlineFragmentNode + | DirectiveNode + | EnumTypeDefinitionNode + | DirectiveDefinitionNode + | SchemaDefinitionNode + | VariableDefinitionNode + | OperationDefinitionNode + | DocumentNode + +export type F = + | Nullary + | RefNode + | ValueNode + | Unary + +export type Fixpoint = + | Nullary + | RefNode + | ValueNode + | NonNullTypeNode + | ListNode + | FieldNode + | FieldDefinitionNode + | ScalarTypeDefinitionNode + | ObjectTypeDefinitionNode + | InterfaceTypeDefinitionNode + | UnionTypeDefinitionNode + | ArgumentNode + | InputValueDefinitionNode + | InputObjectTypeDefinitionNode + | SelectionSetNode + | FragmentDefinitionNode + | FragmentSpreadNode + | InlineFragmentNode + | DirectiveNode + | EnumTypeDefinitionNode + | DirectiveDefinitionNode + | SchemaDefinitionNode + | VariableDefinitionNode + | OperationDefinitionNode + | OperationTypeDefinitionNode + | DocumentNode + +export declare namespace AST { + export { + NameNode, + Nullary, + Unary, + F, + Fixpoint, + Catalog, + RefNode, + // nodes: + ArgumentNode, + BooleanNode, + BooleanValueNode, + DirectiveDefinitionNode, + DirectiveNode, + DocumentNode, + EnumTypeDefinitionNode, + EnumValueDefinitionNode, + EnumValueNode, + FieldDefinitionNode, + FieldNode, + FloatNode, + FloatValueNode, + FragmentDefinitionNode, + FragmentSpreadNode, + IDNode, + InlineFragmentNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + IntNode, + IntValueNode, + ListNode, + ListValueNode, + MutationOperation, + NamedTypeNode, + NonNullTypeNode, + NullNode, + NullValueNode, + NumberNode, + ObjectTypeDefinitionNode, + ObjectFieldNode, + ObjectValueNode, + OperationDefinitionNode, + OperationTypeDefinitionNode, + QueryOperation, + ScalarTypeDefinitionNode, + SchemaDefinitionNode, + SelectionSetNode, + StringNode, + StringValueNode, + SubscriptionOperation, + UnionTypeDefinitionNode, + ValueNode, + VariableNode, + VariableDefinitionNode, + } +} + +export function isScalarTypeDefinition(x: unknown): x is AST.ScalarTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.ScalarTypeDefinition)(x) +} + +export function isNullNode(x: unknown): x is AST.NullNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.Null)(x) +} + +export function isBooleanNode(x: unknown): x is AST.BooleanNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.Boolean)(x) +} + +export function isIntNode(x: unknown): x is AST.IntNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.Int)(x) +} + +export function isNumberNode(x: unknown): x is AST.NumberNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.Number)(x) +} + +export function isFloatNode(x: unknown): x is AST.FloatNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.Float)(x) +} + +export function isStringNode(x: unknown): x is AST.StringNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.String)(x) +} + +export function isIDNode(x: unknown): x is AST.IDNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => value === NamedType.ID)(x) +} + +export function isObjectFieldNode(x: unknown): x is AST.ObjectFieldNode { + return has('kind', (kind) => kind === Kind.ObjectField)(x) +} + +export function isEnumTypeDefinitionNode(x: unknown): x is AST.EnumTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.EnumTypeDefinition)(x) +} + +export function isEnumValueDefinitionNode(x: unknown): x is AST.EnumValueDefinitionNode { + return has('kind', (kind) => kind === Kind.EnumValueDefinition)(x) +} + +export function isEnumValueNode(x: unknown): x is AST.EnumValueNode { + return has('kind', (kind) => kind === Kind.EnumValue)(x) +} + +export function isVariableNode(x: unknown): x is AST.VariableNode { + return has('kind', (kind) => kind === Kind.Variable)(x) +} + +export function isVariableDefinitionNode(x: unknown): x is AST.VariableDefinitionNode { + return has('kind', (kind) => kind === Kind.VariableDefinition)(x) +} + +export function isBooleanValueNode(x: unknown): x is AST.BooleanValueNode { + return has('kind', (kind) => kind === Kind.BooleanValue)(x) +} + +export function isIntValueNode(x: unknown): x is AST.IntValueNode { + return has('kind', (kind) => kind === Kind.IntValue)(x) +} + +export function isFloatValueNode(x: unknown): x is AST.FloatValueNode { + return has('kind', (kind) => kind === Kind.FloatValue)(x) +} + +export function isStringValueNode(x: unknown): x is AST.StringValueNode { + return has('kind', (kind) => kind === Kind.StringValue)(x) +} + +export function isNullValueNode(x: unknown): x is AST.NullValueNode { + return has('kind', (kind) => kind === Kind.NullValue)(x) +} + +export function isListValueNode(x: unknown): x is AST.ListValueNode { + return has('kind', (kind) => kind === Kind.ListValue)(x) +} + +export function isObjectValueNode(x: unknown): x is AST.ObjectValueNode { + return has('kind', (kind) => kind === Kind.ObjectValue)(x) +} + +export function isNonNullTypeNode(x: unknown): x is AST.NonNullTypeNode { + return has('kind', (kind) => kind === Kind.NonNullType)(x) +} + +export function isListNode(x: unknown): x is AST.ListNode { + return has('kind', (kind) => kind === Kind.ListType)(x) +} + +export function isFieldNode(x: unknown): x is AST.FieldNode { + return has('kind', (kind) => kind === Kind.Field)(x) +} + +export function isFieldDefinitionNode(x: unknown): x is AST.FieldDefinitionNode { + return has('kind', (kind) => kind === Kind.FieldDefinition)(x) +} + +export function isObjectTypeDefinitionNode(x: unknown): x is AST.ObjectTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.ObjectTypeDefinition)(x) +} + +export function isInterfaceTypeDefinitionNode(x: unknown): x is AST.InterfaceTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.InterfaceTypeDefinition)(x) +} + +export function isUnionTypeDefinitionNode(x: unknown): x is AST.UnionTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.UnionTypeDefinition)(x) +} + +export function isArgumentNode(x: unknown): x is AST.ArgumentNode { + return has('kind', (kind) => kind === Kind.Argument)(x) +} + +export function isInputValueDefinitionNode(x: unknown): x is AST.InputValueDefinitionNode { + return has('kind', (kind) => kind === Kind.InputValueDefinition)(x) +} + +export function isInputObjectTypeDefinitionNode(x: unknown): x is AST.InputObjectTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.InputObjectTypeDefinition)(x) +} + +export function isNameNode(x: unknown): x is AST.NameNode { + return has('kind', (kind) => kind == 'Name')(x) +} + +export function isNamedTypeNode(x: unknown): x is AST.NamedTypeNode { + return has('name', 'value', (value) => typeof value === 'string')(x) +} + +export function isSelectionSetNode(x: unknown): x is AST.SelectionSetNode { + return has('kind', (kind) => kind === Kind.SelectionSet)(x) +} + +export function isFragmentDefinitionNode(x: unknown): x is AST.FragmentDefinitionNode { + return has('kind', (kind) => kind === Kind.FragmentDefinition)(x) +} + +export function isFragmentSpreadNode(x: unknown): x is AST.FragmentSpreadNode { + return has('kind', (kind) => kind === Kind.FragmentSpread)(x) +} + +export function isInlineFragmentNode(x: unknown): x is AST.InlineFragmentNode { + return has('kind', (kind) => kind === Kind.InlineFragment)(x) +} + +export function isDirectiveNode(x: unknown): x is AST.DirectiveNode { + return has('kind', (kind) => kind === Kind.Directive)(x) +} + +export function isDirectiveDefinitionNode(x: unknown): x is AST.DirectiveDefinitionNode { + return has('kind', (kind) => kind === Kind.DirectiveDefinition)(x) +} + +export function isQueryOperation(x: unknown): x is AST.QueryOperation { + return has('operation', (op) => op === OperationType.Query)(x) +} + +export function isMutationOperation(x: unknown): x is AST.MutationOperation { + return has('operation', (op) => op === OperationType.Mutation)(x) +} + +export function isSubscriptionOperation(x: unknown): x is AST.SubscriptionOperation { + return has('operation', (op) => op === OperationType.Subscription)(x) +} + +export function isSchemaDefinitionNode(x: unknown): x is AST.SchemaDefinitionNode { + return has('kind', (kind) => kind === Kind.SchemaDefinition)(x) +} + +export function isDocumentNode(x: unknown): x is AST.DocumentNode { + return has('kind', (kind) => kind === Kind.Document)(x) +} + +export function isRefNode(x: unknown): x is AST.RefNode { + return has('kind', (kind) => kind === Kind.NamedType)(x) + && has('name', 'value', (value) => typeof value === 'string')(x) + && !isNullaryNode(x) +} + +export function isValueNode(x: unknown): x is AST.ValueNode { + return isNullValueNode(x) + || isIntValueNode(x) + || isFloatValueNode(x) + || isStringValueNode(x) + || isBooleanValueNode(x) + || isEnumValueNode(x) + || isListValueNode(x) + || isObjectValueNode(x) + || isEnumValueNode(x) + || isEnumValueDefinitionNode(x) + || isVariableNode(x) +} + +export function isNullaryNode(x: unknown): x is AST.Nullary { + return isNullNode(x) + || isNameNode(x) + || isBooleanNode(x) + || isIntNode(x) + || isNumberNode(x) + || isFloatNode(x) + || isStringNode(x) + || isIDNode(x) + || isEnumValueDefinitionNode(x) + || isOperationTypeDefinitionNode(x) + // || isScalarTypeDefinition(x) +} + +export function isUnaryNode(x: unknown): x is AST.Unary { + return isNonNullTypeNode(x) + || isListNode(x) + || isFieldNode(x) + || isFieldDefinitionNode(x) + || isFieldDefinitionNode(x) + || isObjectTypeDefinitionNode(x) + || isInterfaceTypeDefinitionNode(x) + || isUnionTypeDefinitionNode(x) + || isArgumentNode(x) + || isInputValueDefinitionNode(x) + || isInputObjectTypeDefinitionNode(x) + || isSelectionSetNode(x) + || isEnumTypeDefinitionNode(x) + || isFragmentDefinitionNode(x) + || isFragmentSpreadNode(x) + || isInlineFragmentNode(x) + || isDirectiveNode(x) + || isDirectiveDefinitionNode(x) + || isSchemaDefinitionNode(x) + || isVariableDefinitionNode(x) + || isOperationDefinitionNode(x) + || isOperationTypeDefinitionNode(x) + || isDocumentNode(x) +} + +export function isOperationDefinitionNode(x: unknown): x is AST.OperationDefinitionNode { + return has('kind', (kind) => kind === Kind.OperationDefinition)(x) +} + +export function isOperationTypeDefinitionNode(x: unknown): x is AST.OperationTypeDefinitionNode { + return has('kind', (kind) => kind === Kind.OperationTypeDefinition)(x) +} + +export interface Index { + namedTypes: Record T> +} + +export const createIndex: () => Index = () => ({ + namedTypes: Object_create(null), +}) + +export interface Functor extends T.HKT { [-1]: AST.F } +export declare namespace Functor { export { Index } } + +export const Functor: T.Functor.Ix = { + map(g) { + return (x) => { + switch (true) { + default: return fn.exhaustive(x) + case isRefNode(x): return x + case isNullaryNode(x): return x + case isValueNode(x): return x + case isListNode(x): return { ...x, type: g(x.type) } + case isNonNullTypeNode(x): return { ...x, type: g(x.type) } + case isSelectionSetNode(x): return { ...x, selections: x.selections.map(g) } + case isDocumentNode(x): return { ...x, definitions: x.definitions.map(g) } + case isArgumentNode(x): return { ...x, value: g(x.value) } + case isScalarTypeDefinition(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + } + } + case isEnumTypeDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + } + } + case isVariableDefinitionNode(x): { + const { directives, defaultValue, type, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + ...defaultValue && { defaultValue: g(defaultValue) }, + type: g(type), + } + } + case isUnionTypeDefinitionNode(x): { + const { directives, types, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + types: types.map(g), + } + } + case isFieldNode(x): { + const { arguments: args, directives, selectionSet, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map(g) }, + ...directives && { directives: directives.map(g) }, + ...selectionSet && { selectionSet: g(selectionSet) }, + } + } + case isFieldDefinitionNode(x): { + const { arguments: args, directives, type, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map(g) }, + ...directives && { directives: directives.map(g) }, + type: g(type), + } + } + case isObjectTypeDefinitionNode(x): { + const { directives, interfaces, fields, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + interfaces: interfaces.map(g), + fields: fields.map(g), + } + } + case isInterfaceTypeDefinitionNode(x): { + const { directives, interfaces, fields, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + interfaces: interfaces.map(g), + fields: fields.map(g), + } + } + case isInputValueDefinitionNode(x): { + const { directives, defaultValue, type, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + ...defaultValue && { defaultValue: g(defaultValue) }, + type: g(type), + } + } + case isInputObjectTypeDefinitionNode(x): { + const { directives, fields, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + fields: fields.map(g), + } + } + case isFragmentDefinitionNode(x): { + const { directives, selectionSet, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + selectionSet: g(selectionSet), + } + } + case isFragmentSpreadNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + } + } + case isInlineFragmentNode(x): { + const { directives, selectionSet, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + selectionSet: g(selectionSet), + } + } + case isDirectiveNode(x): { + const { arguments: args, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map(g) }, + } + } + case isDirectiveDefinitionNode(x): { + const { arguments: args, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map(g) }, + } + } + case isOperationDefinitionNode(x): { + const { directives, variableDefinitions: vars, selectionSet, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + ...vars && { variableDefinitions: vars.map(g) }, + selectionSet: g(selectionSet) + } + } + case isSchemaDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map(g) }, + // operationTypes: fn.map(operationTypes, (ot) => ({ ...ot, operation: g(ot.operation) })), + } + } + } + } + }, + mapWithIndex(g) { + return (x, ix) => { + switch (true) { + // default: return fn.exhaustive(x) + default: return (console.log('EXHAUSTIVE, x:', x), x) satisfies never + case isRefNode(x): return x + case isNullaryNode(x): return x + case isValueNode(x): return x + case isArgumentNode(x): return { ...x, value: g(x.value, ix, x) } + case isListNode(x): return { ...x, type: g(x.type, ix, x) } + case isNonNullTypeNode(x): return { ...x, type: g(x.type, ix, x) } + case isDocumentNode(x): return { ...x, definitions: x.definitions.map((_) => g(_, ix, x)) } + case isScalarTypeDefinition(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + } + } + case isEnumTypeDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + } + } + case isVariableDefinitionNode(x): { + const { directives, defaultValue, type, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + ...defaultValue && { defaultValue: g(defaultValue, ix, x) }, + type: g(type, ix, x), + } + } + case isSelectionSetNode(x): { + return { ...x, selections: x.selections.map((_) => g(_, ix, x)) } + } + case isUnionTypeDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + types: x.types.map((_) => g(_, ix, x)), + } + } + case isFieldNode(x): { + const { arguments: args, directives, selectionSet, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map((_) => g(_, ix, x)) }, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + ...selectionSet && { selectionSet: g(selectionSet, ix, x) }, + } + } + case isFieldDefinitionNode(x): { + const { arguments: args, directives, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map((_) => g(_, ix, x)) }, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + type: g(x.type, ix, x), + } + } + case isObjectTypeDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + fields: x.fields.map((_) => g(_, ix, x)), + interfaces: x.interfaces.map((_) => g(_, ix, x)), + } + } + case isInterfaceTypeDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + fields: x.fields.map((_) => g(_, ix, x)), + interfaces: x.interfaces.map((_) => g(_, ix, x)), + } + } + case isInputValueDefinitionNode(x): { + const { directives, defaultValue, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + ...defaultValue && { defaultValue: g(defaultValue, ix, x) }, + type: g(x.type, ix, x), + } + } + case isInputObjectTypeDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + fields: x.fields.map((_) => g(_, ix, x)), + } + } + case isFragmentDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + selectionSet: g(x.selectionSet, ix, x), + } + } + case isFragmentSpreadNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + } + } + case isInlineFragmentNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + selectionSet: g(x.selectionSet, ix, x), + } + } + case isDirectiveNode(x): { + const { arguments: args, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map((_) => g(_, ix, x)) }, + } + } + case isDirectiveDefinitionNode(x): { + const { arguments: args, ...xs } = x + return { + ...xs, + ...args && { arguments: args.map((_) => g(_, ix, x)) }, + } + } + case isOperationDefinitionNode(x): { + const { directives, variableDefinitions: vars, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + ...vars && { variableDefinitions: vars.map((_) => g(_, ix, x)) }, + selectionSet: g(x.selectionSet, ix, x) + } + } + case isSchemaDefinitionNode(x): { + const { directives, ...xs } = x + return { + ...xs, + ...directives && { directives: directives.map((_) => g(_, ix, x)) }, + } + } + } + } + } +} + +const fold_ = fn.catamorphism(Functor, createIndex()) + +function getDependencies(node: GQL.ASTNode, ix: Index): Set { + const deps = new Set() + void fold_((x) => isRefNode(x) ? (void deps.add(x.name.value), x) : x)(node, ix) + return deps +} + +function reducer(g: (src: AST.F, ix: Index, x: GQL.ASTNode) => T, ix: Index): + (acc: Record T>, node: T | GQL.ASTNode, i: number) => Record T> + +function reducer(g: (src: AST.F, ix: Index, x: GQL.ASTNode) => T, ix: Index) { + return (acc: Record T>, node: GQL.ASTNode, i: number) => { + const key = has('name', 'value', (v) => typeof v === 'string')(node) ? node.name.value : `__${i}` + acc[key] = () => fold_(g)(node, ix) + return acc + } +} + +function setGroups(groups: Map, ix: Index): (node: T | GQL.ASTNode, i: number) => void +function setGroups(groups: Map, ix: Index): (node: GQL.ASTNode, i: number) => void { + return (node: GQL.ASTNode, i: number) => { + if (has('name', 'value', (v) => typeof v === 'string')(node)) { + groups.set(node.name.value, Array.from(getDependencies(node, ix))) + } else { + groups.set(`__${i}`, Array.from(getDependencies(node, ix))) + } + } +} + +export function fold(g: (src: AST.F, ix: Index, x: GQL.ASTNode) => T): Algebra { + return (x, ix) => { + const index = ix ?? createIndex() as Index + const groups = new Map() + const byName = x.definitions.reduce(reducer(g, index), index.namedTypes) + void x.definitions.forEach(setGroups(groups, index)) + + try { + const graph = topologicalSort(groups) + const order = graph.chunks.flat() + return { order, byName } + } catch (e) { + console.error('Error:', e) + console.debug('Groups:', groups) + throw Error( + 'Dependency graph contains unknown nodes: { ' + + Array.from(groups).map(([k, v]) => `${k}: [${v.join(', ')}]`).join(', ') + + ' }' + ) + } + } +} diff --git a/packages/graphql-types/src/index.ts b/packages/graphql-types/src/index.ts new file mode 100644 index 00000000..410a4bcb --- /dev/null +++ b/packages/graphql-types/src/index.ts @@ -0,0 +1 @@ +export * from './exports.js' diff --git a/packages/graphql-types/src/to-string.ts b/packages/graphql-types/src/to-string.ts new file mode 100644 index 00000000..7b3ae97e --- /dev/null +++ b/packages/graphql-types/src/to-string.ts @@ -0,0 +1,130 @@ +import { escape, has } from '@traversable/registry' +import * as F from './functor.js' +import * as gql from 'graphql' + +function directives(x: { directives?: readonly string[] }): string { + return !x.directives ? '' : ` ${x.directives.join(' ')} ` +} + +function defaultValue(x: { defaultValue?: F.ValueNode | string }): string { + return x.defaultValue ? ` = ${serializeValueNode(x.defaultValue)}` : '' +} + +function description(x: { description?: F.StringValueNode }): string { + return !x.description ? '' + : x.description.block ? + `\n"""\n${x.description.value}\n"""\n` + : `"${x.description.value}"\n` +} + +function serializeValueNode(x?: F.ValueNode | string): string { + if (!x) return '' + else if (typeof x === 'string') return x + else { + switch (true) { + default: return x satisfies never + case F.isNullValueNode(x): return 'Null' + case F.isBooleanValueNode(x): return `${x.value}` + case F.isIntValueNode(x): return `${x.value}` + case F.isFloatValueNode(x): return `${x.value}` + case F.isStringValueNode(x): return `"${escape(x.value)}"` + case F.isEnumValueNode(x): return `${x.value}` + case F.isListValueNode(x): return `[${x.values.map(serializeValueNode).join(', ')}]` + case F.isVariableNode(x): return `$${x.name.value}` + case F.isObjectValueNode(x): return `{ ${x.fields.map((n) => `${n.name.value}: ${serializeValueNode(n.value)}`).join(', ')} }` + } + } +} + +const fold = F.fold((x) => { + switch (true) { + // default: return fn.exhaustive(x) + default: return x satisfies never + case F.isEnumValueDefinitionNode(x): throw Error('Not sure what an `EnumValueDefinitionNode` is...') + case F.isObjectFieldNode(x): throw Error('Not sure what an `ObjectFieldNode` is...') + case F.isOperationTypeDefinitionNode(x): return `${x.operation}: ${x.type.name.value}` + // return `OperationTypeDefinition: ${x.operation}` // throw Error('Not sure what an `OperationTypeDefinitionNode` is...') + case F.isValueNode(x): return serializeValueNode(x) + case F.isSelectionSetNode(x): return `{ ${x.selections.join('\n')} }` + case F.isScalarTypeDefinition(x): return `scalar ${directives(x)}${x.name.value}` + case F.isBooleanNode(x): return 'Boolean' + case F.isIntNode(x): return 'Int' + case F.isNumberNode(x): return 'Number' + case F.isFloatNode(x): return 'Float' + case F.isStringNode(x): return 'String' + case F.isIDNode(x): return 'ID' + case F.isNullNode(x): return 'Null' + case F.isListNode(x): return `[${x.type}]` + case F.isNonNullTypeNode(x): return `${x.type}!` + case F.isArgumentNode(x): return `${x.name.value}: ${x.value}` + case F.isDirectiveNode(x): return `@${x.name.value}${(!x.arguments?.length ? '' : `(${x.arguments.join(', ')})`)}` + case F.isInputObjectTypeDefinitionNode(x): { + return `${description(x)}input ${x.name.value} { ${x.fields.join('\n')} } ` + } + case F.isUnionTypeDefinitionNode(x): return `${description(x)}union ${x.name.value}${directives(x)} = ${x.types.join(' | ')}` + case F.isEnumTypeDefinitionNode(x): return `enum ${x.name.value}${directives(x)} { ${x.values.map((v) => v.name.value).join('\n')} }` + case F.isDirectiveDefinitionNode(x): { + const ARGS = x.arguments?.length ? `(${x.arguments.join(', ')})` : '' + return `${description(x)}directive @${x.name.value}${ARGS} on ${x.locations.map((loc) => loc.value).join(' | ')}` + } + case F.isInlineFragmentNode(x): return `...${!x.typeCondition ? '' : ` on ${x.typeCondition.name.value}`} ${x.selectionSet} ` + case F.isFragmentDefinitionNode(x): { + return `fragment ${x.name.value} on ${x.typeCondition.name.value}${directives(x)}${x.selectionSet}` + } + case F.isFragmentSpreadNode(x): return `...${x.name.value}${directives(x)}` + case F.isVariableDefinitionNode(x): return `$${x.variable.name.value}: ${x.type}${defaultValue(x)}${directives(x)}` + case F.isDocumentNode(x): return x.definitions.join('\n\r') + case F.isFieldNode(x): { + const KEY = !x.alias?.value ? `${x.name.value} ` : `${x.alias.value}: ${x.name.value}` + const ARGS = !x.arguments?.length ? '' : `(${x.arguments.join(', ')})` + return x.selectionSet + ? `${description(x)}${KEY}${ARGS}${directives(x)}${x.selectionSet}` + : `${description(x)}${KEY}${ARGS}${directives(x)}` + } + case F.isFieldDefinitionNode(x): { + const ARGS = !x.arguments?.length ? '' : `(${x.arguments.join(', ')})` + return `${description(x)}${x.name.value}${ARGS}: ${x.type}${directives(x)}` + } + case F.isInputValueDefinitionNode(x): { + return `${description(x)}${x.name.value}: ${x.type}${defaultValue(x)}${directives(x)}` + } + case F.isObjectTypeDefinitionNode(x): { + const IMPLEMENTS = x.interfaces.length ? ` implements ${x.interfaces.join(' & ')}` : '' + return `${description(x)}type ${x.name.value}${IMPLEMENTS}${directives(x)} { ${x.fields.join('\n')} } ` + } + case F.isInterfaceTypeDefinitionNode(x): { + const IMPLEMENTS = x.interfaces.length ? ` implements ${x.interfaces.join(' & ')}` : '' + return `${description(x)}interface ${x.name.value}${IMPLEMENTS}${directives(x)} { ${x.fields.join('\n')} } ` + } + case F.isOperationDefinitionNode(x): { + const NAME = x.name?.value ? ` ${x.name.value} ` : '' + const VARS = !x.variableDefinitions?.length ? '' : `(${x.variableDefinitions.join(', ')})` + return `${x.operation}${NAME}${VARS}${directives(x)} ${x.selectionSet} ` + } + case F.isSchemaDefinitionNode(x): { + return `${description(x)}schema ${directives(x)}{ ${x.operationTypes.map((op) => `${op.operation}: ${op.type.name.value}`).join('\n')} }` + } + case F.isRefNode(x): return x.name.value + case F.isNameNode(x): return x.value + } +}) + +export declare namespace toString {} + +/** + * ## {@link toType `toType`} + * + * Convert a GraphQL AST into its corresponding TypeScript type. + */ +export function toString(doc: gql.DocumentNode): string +export function toString(doc: F.AST.Fixpoint): string +export function toString(doc: F.AST.Fixpoint | gql.DocumentNode): string { + const ast: F.AST.DocumentNode + = has('kind', (kind) => kind === gql.Kind.DOCUMENT)(doc) + ? doc + : { kind: 'Document', definitions: [doc as F.F] } + + return Object + .values(fold(ast as F.AST.DocumentNode).byName) + .map((thunk) => thunk()).join('\n\r') +} diff --git a/packages/graphql-types/src/to-type.ts b/packages/graphql-types/src/to-type.ts new file mode 100644 index 00000000..0f39c382 --- /dev/null +++ b/packages/graphql-types/src/to-type.ts @@ -0,0 +1,122 @@ +import { fn, has, parseKey } from '@traversable/registry' +import * as F from './functor.js' +import type * as gql from 'graphql' +import { Kind, NamedType } from './functor.js' +import * as AST from './functor.js' + +const unsupported = [ + 'Directive', + 'FragmentDefinition', + 'FragmentSpread', + 'InlineFragment', + 'InputObjectTypeDefinition', + 'InputValueDefinition', + 'SelectionSet', + 'OperationDefinition', + 'OperationTypeDefinition', + 'Argument', + 'SchemaDefinition', + 'VariableDefinition', + 'DirectiveDefinition', + 'ObjectField', +] as const satisfies Array + +type UnsupportedNodeMap = Pick +type UnsupportedNode = UnsupportedNodeMap[keyof UnsupportedNodeMap] + +function isUnsupportedNode(x: unknown): x is UnsupportedNode { + return unsupported.some( + (tag) => has('kind', (kind) => kind === tag)(x) + ) +} + +function valueNodeToString(x: AST.ValueNode): string { + switch (true) { + default: return fn.exhaustive(x) + case AST.isNullNode(x): return 'null' + case AST.isNullValueNode(x): return 'null' + case AST.isBooleanValueNode(x): return `${x.value}` + case AST.isIntValueNode(x): return `${x.value}` + case AST.isFloatValueNode(x): return `${x.value}` + case AST.isStringValueNode(x): return `"${x.value}"` + case AST.isEnumValueNode(x): return `${x.value}` + case AST.isListValueNode(x): return `[${x.values.map(valueNodeToString).join(', ')}]` + case AST.isObjectValueNode(x): return '' + + '{ ' + + x.fields.map((node) => `${parseKey(node.name.value)}: ${valueNodeToString(node.value)}`).join(', ') + + ' }' + case AST.isVariableNode(x): return `${x.name.value}` + case AST.isNamedTypeNode(x): return (x as AST.NamedTypeNode).name.value + } +} + +const fold = F.fold((x) => { + switch (true) { + default: return fn.exhaustive(x) + case isUnsupportedNode(x): return '' + case F.isRefNode(x): return x.name.value + case F.isNameNode(x): return x.value + case F.isEnumValueDefinitionNode(x): throw Error('Not sure what an enum value definition node is...') + case F.isValueNode(x): return valueNodeToString(x) + case F.isScalarTypeDefinition(x): return x.name.value + case F.isBooleanNode(x): return 'boolean' + case F.isIntNode(x): return 'number' + case F.isNumberNode(x): return 'number' + case F.isFloatNode(x): return 'number' + case F.isStringNode(x): return 'string' + case F.isIDNode(x): return 'string' + case F.isEnumTypeDefinitionNode(x): return ( + x.values.length === 1 ? JSON.stringify(x.values[0]) + : `(${x.values.map((v) => JSON.stringify(v)).join(' | ')})` + ) + case F.isNonNullTypeNode(x): return `${x.type}!` + case F.isUnionTypeDefinitionNode(x): return ( + x.types.length === 1 ? `type ${x.name.value} = ${JSON.stringify(x.types[0])}` + : `type ${x.name.value} = ${x.types.map((v) => JSON.stringify(v)).join(' | ')}` + ) + case F.isListNode(x): return `Array<${x.type.endsWith('!') ? x.type.slice(0, -1) : `${x.type} | null`}>` + case F.isFieldDefinitionNode(x): { + const isNonNull = x.type.endsWith('!') + const VALUE = isNonNull ? x.type.slice(0, -1) : x.type + return `${parseKey(x.name.value)}${isNonNull ? '' : '?'}: ${VALUE}` + } + case F.isFieldNode(x): { + const KEY = x.alias?.value ?? x.name.value + return x.selectionSet + ? `${KEY}: ${x.selectionSet}` + : `${KEY}: ${x.name.value}` + } + case F.isObjectTypeDefinitionNode(x): { + const IMPLEMENTS = !x.interfaces.length ? '' : `${x.interfaces.join(' & ')} & ` + return `type ${x.name.value} = ${IMPLEMENTS}{ + __typename?: ${x.name.value} + ${x.fields.join('\n')} + }` + } + case F.isInterfaceTypeDefinitionNode(x): { + const IMPLEMENTS = !x.interfaces.length ? '' : `extends ${x.interfaces.join(', ')} ` + return `interface ${x.name.value} ${IMPLEMENTS}{ + __typename?: ${x.name.value} + ${x.fields.join('\n')} + }` + } + case F.isDocumentNode(x): return x.definitions.join('\n\r') + } +}) + +export declare namespace toType { + export type { + UnsupportedNode + } +} + +toType.unsupported = unsupported + +/** + * ## {@link toType `toType`} + * + * Convert a GraphQL AST into its corresponding TypeScript type. + */ +export function toType(doc: gql.DocumentNode) { + return Object.values(fold(doc).byName).map((thunk) => thunk()).join('\n\r') +} diff --git a/packages/graphql-types/src/version.ts b/packages/graphql-types/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/graphql-types/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/graphql-types/test/to-string.test.ts b/packages/graphql-types/test/to-string.test.ts new file mode 100644 index 00000000..f6ef044e --- /dev/null +++ b/packages/graphql-types/test/to-string.test.ts @@ -0,0 +1,769 @@ +import * as vi from 'vitest' +import * as AST from '@traversable/graphql-types' +import * as graphql from 'graphql' +import prettier from '@prettier/sync' + +const format = (src: string) => prettier.format( + src, + { parser: 'graphql', semi: false, printWidth: 60 } +) + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/graphql-types❳', () => { + vi.test('〖⛳️〗› ❲AST.toString❳: AST.ScalarTypeDefinition', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + scalar Date + `))))).toMatchInlineSnapshot + (` + "scalar Date + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.NullNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: Null + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: Null + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.BooleanNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: Boolean + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: Boolean + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.IntNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: Int + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: Int + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.NumberNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: Number + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: Number + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.FloatNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: Float + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: Float + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.StringNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: String + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: String + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.IDNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: ID + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: ID + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.NonNullTypeNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: String! + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: String! + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.ListNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Test { + abc: [String] + def: [String!] + ghi: [String]! + jkl: [String!]! + } + `))))).toMatchInlineSnapshot + (` + "type Test { + abc: [String] + def: [String!] + ghi: [String]! + jkl: [String!]! + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.EnumTypeDefinition', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + enum Role { + USER + ADMIN + } + `))))).toMatchInlineSnapshot + (` + "enum Role { + USER + ADMIN + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.OperationDefinitionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + query { + abc: Boolean + } + `))))).toMatchInlineSnapshot + (` + "query { + abc: Boolean + } + " + `) + + vi.expect.soft(format(AST.toString(graphql.parse(format(` + query { + empireHero: hero { + stuff + } + } + `))))).toMatchInlineSnapshot + (` + "query { + empireHero: hero { + stuff + } + } + " + `) + + vi.expect.soft(format(AST.toString(graphql.parse(format(` + query { + empireHero: hero(episode: EMPIRE) { + name + } + jediHero: hero(episode: JEDI) { + name + } + } + `))))).toMatchInlineSnapshot + (` + "query { + empireHero: hero(episode: EMPIRE) { + name + } + jediHero: hero(episode: JEDI) { + name + } + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.QueryOperation', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Query { + createReview(episode: Episode, review: ReviewInput!): Review + } + `))))).toMatchInlineSnapshot + (` + "type Query { + createReview( + episode: Episode + review: ReviewInput! + ): Review + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.MutationOperation', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Mutation { + createReview( + episode: Episode, + review: ReviewInput! + ): Review + } + `))))).toMatchInlineSnapshot + (` + "type Mutation { + createReview( + episode: Episode + review: ReviewInput! + ): Review + } + " + `) + + vi.expect.soft(format(AST.toString(graphql.parse(format(` + mutation CreateReviewForEpisode( + $ep: Episode!, + $review: ReviewInput! + ) { + createReview(episode: $ep, review: $review) { + stars + commentary + } + } + `))))).toMatchInlineSnapshot + (` + "mutation CreateReviewForEpisode( + $ep: Episode! + $review: ReviewInput! + ) { + createReview(episode: $ep, review: $review) { + stars + commentary + } + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.SubscriptionOperation', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Subscription { + createReview( + episode: Episode, + review: ReviewInput! + ): Review + } + `))))).toMatchInlineSnapshot + (` + "type Subscription { + createReview( + episode: Episode + review: ReviewInput! + ): Review + } + " + `) + + vi.expect.soft(format(AST.toString(graphql.parse(format(` + subscription { + episode: Episode, + review: ReviewInput + } + `))))).toMatchInlineSnapshot + (` + "subscription { + episode: Episode + review: ReviewInput + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.DirectiveNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + directive @directive1 on FIELD_DEFINITION | ENUM_VALUE + + directive @directive2( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + `))))).toMatchInlineSnapshot + (` + "directive @directive1 on FIELD_DEFINITION | ENUM_VALUE + + directive @directive2( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.FieldDefinitionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Character { + "The name of the character." + name: String! + } + `))))).toMatchInlineSnapshot + (` + "type Character { + "The name of the character." + name: String! + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.InputObjectTypeDefinitionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + input ReviewInput { + stars: Int! + commentary: String + } + `))))).toMatchInlineSnapshot + (` + "input ReviewInput { + stars: Int! + commentary: String + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.VariableDefinitionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + query HeroNameAndFriends($episode: Episode = JEDI) { + hero(episode: $episode) { + name + friends { + name + } + } + } + `))))).toMatchInlineSnapshot + (` + "query HeroNameAndFriends($episode: Episode = JEDI) { + hero(episode: $episode) { + name + friends { + name + } + } + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.FragmentDefinitionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + fragment comparisonFields on Character { + name + appearsIn + friends { + name + } + } + `))))).toMatchInlineSnapshot + (` + "fragment comparisonFields on Character { + name + appearsIn + friends { + name + } + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.FragmentSpreadNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + query findUser($userId: ID!) { + user(id: $userId) { + ...UserFields + } + } + `))))).toMatchInlineSnapshot + (` + "query findUser($userId: ID!) { + user(id: $userId) { + ...UserFields + } + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.InputValueDefinitionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Starship { + id: ID! + name: String! + length(unit: LengthUnit = METER): Float + } + `))))).toMatchInlineSnapshot + (` + "type Starship { + id: ID! + name: String! + length(unit: LengthUnit = METER): Float + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.ObjectNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + type Pet { + petName: [String!] + } + + type Human { + humanName: String! + pet: Pet + } + `))))).toMatchInlineSnapshot + (` + "type Pet { + petName: [String!] + } + + type Human { + humanName: String! + pet: Pet + } + " + `) + + vi.expect.soft(format(AST.toString(graphql.parse(format(` + """ + A character from the Star Wars universe + """ + type Character { + "The name of the character." + name: String! + } + `))))).toMatchInlineSnapshot + (` + """" + A character from the Star Wars universe + """ + type Character { + "The name of the character." + name: String! + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.InterfaceNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + interface Node { + id: ID! + } + + interface Character implements Node { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + } + + type Episode { + id: ID! + } + `))))).toMatchInlineSnapshot + (` + "interface Node { + id: ID! + } + + interface Character implements Node { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + } + + type Episode { + id: ID! + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.UnionNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + union SearchResult = Human | Droid | Starship + `))))).toMatchInlineSnapshot + (` + "union SearchResult = Human | Droid | Starship + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: AST.ArgumentNode', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + query { + search(text: "an") { + __typename + ... on Human { + name + height + } + ... on Droid { + name + primaryFunction + } + ... on Starship { + name + length + } + } + } + `))))).toMatchInlineSnapshot + (` + "query { + search(text: "an") { + __typename + ... on Human { + name + height + } + ... on Droid { + name + primaryFunction + } + ... on Starship { + name + length + } + } + } + " + `) + }) + + vi.test('〖⛳️〗› ❲AST.toString❳: kitchen sink', () => { + vi.expect.soft(format(AST.toString(graphql.parse(format(` + scalar Date + + # This line is treated like whitespace and ignored by GraphQL + schema { + query: Query + } + + """ + The query type, represents all of the entry points into our object graph + """ + type Query { + me: User! + """ + Fetches the hero of a specified Star Wars film. + """ + user( + "The name of the film that the hero appears in." + id: ID! + ): User + allUsers: [User] + search(term: String!): [SearchResult!]! + myChats: [Chat!]! + } + + enum Role { + USER + ADMIN + } + + interface Node { + id: ID! + } + + union SearchResult = User | Chat | ChatMessage + + type User implements Node { + id: ID! + username: String! + email: String! + role: Role! + } + + type Chat implements Node { + id: ID! + users: [User!]! + messages: [ChatMessage!]! + } + + type ChatMessage implements Node { + id: ID! + content: String! + time: Date! + user: User! + } + + query findUser($userId: ID!) { + user(id: $userId) { + ...UserFields + } + } + + query HeroComparison($first: Int = 3) { + leftComparison: hero(episode: EMPIRE) { + ...comparisonFields + } + rightComparison: hero(episode: JEDI) { + ...comparisonFields + } + } + + fragment comparisonFields on Character { + name + friendsConnection(first: $first) { + totalCount + edges { + node { + name + } + } + } + } + + query Hero($episode: Episode, $withFriends: Boolean!) { + hero(episode: $episode) { + name + friends @include(if: $withFriends) { + name + } + } + } + `))))).toMatchInlineSnapshot + (` + "scalar Date + + schema { + query: Query + } + + """ + The query type, represents all of the entry points into our object graph + """ + type Query { + me: User! + + """ + Fetches the hero of a specified Star Wars film. + """ + user( + "The name of the film that the hero appears in." + id: ID! + ): User + allUsers: [User] + search(term: String!): [SearchResult!]! + myChats: [Chat!]! + } + + enum Role { + USER + ADMIN + } + + interface Node { + id: ID! + } + + union SearchResult = User | Chat | ChatMessage + + type User implements Node { + id: ID! + username: String! + email: String! + role: Role! + } + + type Chat implements Node { + id: ID! + users: [User!]! + messages: [ChatMessage!]! + } + + type ChatMessage implements Node { + id: ID! + content: String! + time: Date! + user: User! + } + + query findUser($userId: ID!) { + user(id: $userId) { + ...UserFields + } + } + + query HeroComparison($first: Int = 3) { + leftComparison: hero(episode: EMPIRE) { + ...comparisonFields + } + rightComparison: hero(episode: JEDI) { + ...comparisonFields + } + } + + fragment comparisonFields on Character { + name + friendsConnection(first: $first) { + totalCount + edges { + node { + name + } + } + } + } + + query Hero($episode: Episode, $withFriends: Boolean!) { + hero(episode: $episode) { + name + friends @include(if: $withFriends) { + name + } + } + } + " + `) + }) + +}) diff --git a/packages/graphql-types/test/to-type.test.ts b/packages/graphql-types/test/to-type.test.ts new file mode 100644 index 00000000..b0858bf8 --- /dev/null +++ b/packages/graphql-types/test/to-type.test.ts @@ -0,0 +1,39 @@ +import * as vi from 'vitest' +import { toType } from '@traversable/graphql-types' +import * as graphql from 'graphql' +import prettier from '@prettier/sync' + +const format = (src: string) => prettier.format( + src, + { parser: 'typescript', semi: false, printWidth: 35 } +) + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/graphql-types❳', () => { + vi.test('〖⛳️〗› ❲toType❳', () => { + vi.expect.soft(format( + toType(graphql.parse(` + type Pet { + petName: [String!] + } + type Human { + humanName: String! + pet: Pet + } + ` + )) + )).toMatchInlineSnapshot + (` + "type Pet = { + __typename?: Pet + petName?: Array + } + + type Human = { + __typename?: Human + humanName: string + pet?: Pet + } + " + `) + }) +}) diff --git a/packages/graphql-types/test/version.test.ts b/packages/graphql-types/test/version.test.ts new file mode 100644 index 00000000..17cbd6e9 --- /dev/null +++ b/packages/graphql-types/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/graphql-types' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/graphql-types❳', () => { + vi.test('〖⛳️〗› ❲VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) diff --git a/packages/graphql-types/tsconfig.build.json b/packages/graphql-types/tsconfig.build.json new file mode 100644 index 00000000..aacae424 --- /dev/null +++ b/packages/graphql-types/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [{ "path": "../registry" }] +} diff --git a/packages/graphql-types/tsconfig.json b/packages/graphql-types/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/graphql-types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/graphql-types/tsconfig.src.json b/packages/graphql-types/tsconfig.src.json new file mode 100644 index 00000000..f70f4837 --- /dev/null +++ b/packages/graphql-types/tsconfig.src.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [{ "path": "../registry" }], + "include": ["src"] +} diff --git a/packages/graphql-types/tsconfig.test.json b/packages/graphql-types/tsconfig.test.json new file mode 100644 index 00000000..1a25fb45 --- /dev/null +++ b/packages/graphql-types/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../registry" } + ], + "include": ["test"] +} diff --git a/packages/graphql-types/vite.config.ts b/packages/graphql-types/vite.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/graphql-types/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/packages/graphql/README.md b/packages/graphql/README.md new file mode 100644 index 00000000..756177b6 --- /dev/null +++ b/packages/graphql/README.md @@ -0,0 +1,40 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝗴𝗿𝗮𝗽𝗵𝗾𝗹

+
+ +

+ TODO: write me +

+ +
+ NPM Version +   + TypeScript +   + License +   + npm +   +
+ +
+ + Static Badge +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
diff --git a/packages/graphql/package.json b/packages/graphql/package.json new file mode 100644 index 00000000..da04538b --- /dev/null +++ b/packages/graphql/package.json @@ -0,0 +1,51 @@ +{ + "name": "@traversable/graphql", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/graphql" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { "include": ["**/*.ts"] }, + "generateIndex": { "include": ["**/*.ts"] } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "dependencies": { + "@traversable/graphql-types": "workspace:^", + "@traversable/registry": "workspace:^" + }, + "devDependencies": { + "graphql": "catalog:" + }, + "peerDependencies": { + "graphql": ">= 16" + }, + "peerDependenciesMeta": { + "graphql": { "optional": true } + } +} diff --git a/packages/graphql/src/__generated__/__manifest__.ts b/packages/graphql/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..a2a6f217 --- /dev/null +++ b/packages/graphql/src/__generated__/__manifest__.ts @@ -0,0 +1,51 @@ +export default { + "name": "@traversable/graphql", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/graphql" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { "include": ["**/*.ts"] }, + "generateIndex": { "include": ["**/*.ts"] } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "dependencies": { + "@traversable/graphql-types": "workspace:^", + "@traversable/registry": "workspace:^" + }, + "devDependencies": { + "graphql": "catalog:" + }, + "peerDependencies": { + "graphql": ">= 16" + }, + "peerDependenciesMeta": { + "graphql": { "optional": true } + } +} as const \ No newline at end of file diff --git a/packages/graphql/src/exports.ts b/packages/graphql/src/exports.ts new file mode 100644 index 00000000..c58b54ea --- /dev/null +++ b/packages/graphql/src/exports.ts @@ -0,0 +1 @@ +export { VERSION } from './version.js' diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts new file mode 100644 index 00000000..410a4bcb --- /dev/null +++ b/packages/graphql/src/index.ts @@ -0,0 +1 @@ +export * from './exports.js' diff --git a/packages/graphql/src/version.ts b/packages/graphql/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/graphql/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/graphql/test/version.test.ts b/packages/graphql/test/version.test.ts new file mode 100644 index 00000000..48330e1b --- /dev/null +++ b/packages/graphql/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/graphql' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/graphql❳', () => { + vi.test('〖⛳️〗› ❲VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) diff --git a/packages/graphql/tsconfig.build.json b/packages/graphql/tsconfig.build.json new file mode 100644 index 00000000..16c1c0e4 --- /dev/null +++ b/packages/graphql/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [{ "path": "../graphql-types" }, { "path": "../registry" }] +} diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/graphql/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/graphql/tsconfig.src.json b/packages/graphql/tsconfig.src.json new file mode 100644 index 00000000..c110a318 --- /dev/null +++ b/packages/graphql/tsconfig.src.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [{ "path": "../graphql-types" }, { "path": "../registry" }], + "include": ["src"] +} diff --git a/packages/graphql/tsconfig.test.json b/packages/graphql/tsconfig.test.json new file mode 100644 index 00000000..e318a1ea --- /dev/null +++ b/packages/graphql/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../graphql-types" }, + { "path": "../registry" } + ], + "include": ["test"] +} diff --git a/packages/graphql/vite.config.ts b/packages/graphql/vite.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/graphql/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/packages/json-schema-test/src/generator-bounds.ts b/packages/json-schema-test/src/generator-bounds.ts index 6fe8278c..420208de 100644 --- a/packages/json-schema-test/src/generator-bounds.ts +++ b/packages/json-schema-test/src/generator-bounds.ts @@ -1,6 +1,5 @@ import * as fc from 'fast-check' -import type { newtype } from '@traversable/registry' import { fn, Number_isFinite, Number_isNatural, Number_isSafeInteger, Object_is } from '@traversable/registry' /** @internal */ @@ -61,11 +60,11 @@ const clampArrayMax = clampMax(defaults.array[0], defaults.array[1], Number_isNa export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: model, maximum: model }) export { Bounds_int as int } -interface Bounds_int extends newtype<[ +type Bounds_int = [ minimum: number | null, maximum: number | null, multipleOf: number | null, -]> {} +] const Bounds_int : (model: fc.Arbitrary) => fc.Arbitrary @@ -77,10 +76,10 @@ const Bounds_int ]) export { Bounds_string as string } -interface Bounds_string extends newtype<[ +type Bounds_string = [ minLength: number | null, maxLength: number | null, -]> {} +] const Bounds_string : (model: fc.Arbitrary) => fc.Arbitrary @@ -92,13 +91,13 @@ const Bounds_string ) export { Bounds_number as number } -interface Bounds_number extends newtype<[ +type Bounds_number = [ minimum: number | null, maximum: number | null, multipleOf: number | null, exclusiveMinimum: boolean, exclusiveMaximum: boolean, -]> {} +] const deltaIsSubEpsilon = (x: number, y: number) => Math.abs(x - y) < Number.EPSILON @@ -131,10 +130,10 @@ const Bounds_number ) export { Bounds_array as array } -interface Bounds_array extends newtype<[ +type Bounds_array = [ minLength: number | null, maxLength: number | null, -]> {} +] const Bounds_array : (model: fc.Arbitrary) => fc.Arbitrary diff --git a/packages/json-schema-test/src/generator.ts b/packages/json-schema-test/src/generator.ts index 026ae051..00e45c82 100644 --- a/packages/json-schema-test/src/generator.ts +++ b/packages/json-schema-test/src/generator.ts @@ -1,6 +1,6 @@ import * as fc from 'fast-check' -import type { newtype, inline } from '@traversable/registry' +import type { inline } from '@traversable/registry' import { Array_isArray, fn, @@ -122,7 +122,7 @@ export interface SeedBuilder { (tie: fc.LetrecTypedTie, $: Config.byTypeName[K]): fc.Arbitrary } -export interface SeedMap extends newtype<{ [K in keyof Seed]: SeedBuilder }> {} +export type SeedMap = { [K in keyof Seed]: SeedBuilder } export const SeedMap = { ...TerminalMap, ...BoundableMap, @@ -174,17 +174,17 @@ export function JsonSchema_Number(bounds: Bounds.number = Bounds.defaults.number } export function JsonSchema_String(bounds: Bounds.string = Bounds.defaults.string): JsonSchema.String { - const [min, max, exactLength] = bounds + const [min, max] = bounds let schema: JsonSchema.String = { type: 'string' } - if (Number_isNatural(exactLength)) { - schema.minLength = exactLength - schema.maxLength = exactLength - return schema - } else { - if (Number_isNatural(min)) schema.minLength = min - if (Number_isNatural(max)) schema.maxLength = max - return schema - } + // if (Number_isNatural(exactLength)) { + // schema.minLength = exactLength + // schema.maxLength = exactLength + // return schema + // } else { + // } + if (Number_isNatural(min)) schema.minLength = min + if (Number_isNatural(max)) schema.maxLength = max + return schema } export function JsonSchema_Array( @@ -251,7 +251,8 @@ export declare namespace Gen { type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary } type Values = never | T[Exclude] type InferArb = S extends fc.Arbitrary ? T : never - interface Builder extends newtype { ['*']: fc.Arbitrary>> } + type Builder = T & BuilderStar + interface BuilderStar { ['*']: fc.Arbitrary>> } type BuildBuilder, Out extends {} = BuilderBase> = never | Builder type BuilderBase, $ extends ParseOptions = ParseOptions> = never | & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) @@ -473,7 +474,7 @@ export const SeedInvalidDataGenerator = fn.pipe( export const SchemaGenerator = fn.flow( SeedGenerator, builder => builder['*'], - (arb) => arb.map(seedToSchema), + (model) => model.map((x) => seedToSchema(x as never)), ) export declare namespace SchemaGenerator { diff --git a/packages/json-schema-types/src/to-type.ts b/packages/json-schema-types/src/to-type.ts index 651c9bb0..909f69bd 100644 --- a/packages/json-schema-types/src/to-type.ts +++ b/packages/json-schema-types/src/to-type.ts @@ -21,7 +21,7 @@ const defaultIndex = { } satisfies FoldIndex function fold(schema: JsonSchema, index: FoldIndex) { - return F.fold((x, ix) => { + return F.fold((x) => { switch (true) { default: return x satisfies never case JsonSchema.isRef(x): return index.canonizeRefName(x.$ref) diff --git a/packages/registry/src/exports.ts b/packages/registry/src/exports.ts index 5d50957b..24f525f1 100644 --- a/packages/registry/src/exports.ts +++ b/packages/registry/src/exports.ts @@ -56,6 +56,7 @@ export { stringifyKey, stringifyLiteral, } from './parse.js' +export { topologicalSort } from './topological-sort.js' export type { GlobalOptions, OptionalTreatment, SchemaOptions } from './options.js' export type { GlobalConfig, SchemaConfig } from './config.js' diff --git a/packages/registry/src/globalThis.ts b/packages/registry/src/globalThis.ts index 4d92191e..4bb4947b 100644 --- a/packages/registry/src/globalThis.ts +++ b/packages/registry/src/globalThis.ts @@ -76,7 +76,17 @@ export const Object_create: { (x: T): T } = globalThis.Object.create -export const Object_entries = globalThis.Object.entries +type Key = `${T & (string | number)}` + +type Object_entries< + T, + K extends keyof T = keyof T, + E = K extends K ? [Key, T[K]] : [string, unknown] +> = (T extends readonly any[] ? [number, T[number]] : E)[] + +export const Object_entries + : (o: T) => Object_entries + = globalThis.Object.entries export const Object_fromEntries = globalThis.Object.fromEntries diff --git a/packages/registry/src/has.ts b/packages/registry/src/has.ts index d49b478e..ef3c223a 100644 --- a/packages/registry/src/has.ts +++ b/packages/registry/src/has.ts @@ -54,7 +54,7 @@ export declare namespace has { export type loop = KS extends readonly [...infer Todo, infer K extends keyof any] ? has.loop - : T extends infer U extends {} ? U : never + : T } /** @@ -71,6 +71,7 @@ export declare namespace has { */ export function has(...params: [...KS]): (u: unknown) => u is has export function has(...params: [...KS, (u: unknown) => u is T]): (u: unknown) => u is has +export function has(...params: [...KS, (u: unknown) => boolean]): (u: unknown) => u is has // impl. export function has( ...args: diff --git a/packages/registry/src/hkt.ts b/packages/registry/src/hkt.ts index 0daec364..d6414b62 100644 --- a/packages/registry/src/hkt.ts +++ b/packages/registry/src/hkt.ts @@ -1,15 +1,14 @@ -import type { newtype } from './newtype.js' import type { Either } from './either.js' import type { Option } from './option.js' import type { Tuple } from './tuple.js' -import { typeclass as kind } from './symbol.js' +import { typeclass } from './symbol.js' -export interface HKT extends newtype<{ [0]: I, [-1]: O }> {} -export interface HKT2 extends newtype<{ [0]: { [0]: A, [1]: B }, [-1]: Z }> {} +export interface HKT { [0]: I, [-1]: O } +export interface HKT2 { [0]: { [0]: A, [1]: B }, [-1]: Z } export type Kind = (F & { [0]: T })[-1] export type Kind2 = (F & { [0]: { [0]: S, [1]: T } })[-1] -export type Bind = { [kind]?: T } +export type Bind = { [typeclass]?: T } export type Box = Partial, T = unknown> = (F & { [0]: T })[-1] export type Boxed = Box.infer @@ -37,7 +36,7 @@ export declare namespace Kind { export { Any as any, Box as lax } } export declare namespace Kind { type Product = Box, T>> type Coproduct = Box, T>> - type infer = Exclude + type infer = Exclude type of = F extends Kind.apply ? T : never type Any = Bind /** @ts-expect-error */ @@ -49,9 +48,9 @@ export type Identity = T export interface Comparator extends Bind { (left: T, right: T): number } export interface Equal extends Bind { (left: T, right: T): boolean } export interface Record extends Bind { [x: string]: T } -export interface Array extends newtype, Bind {} -export interface ReadonlyArray extends newtype, Bind {} -export interface Duplicate extends newtype<{ [0]: T, [1]: T }>, Bind { [Symbol.iterator](): Iterator } +export interface Array extends globalThis.Array, Bind {} +export interface ReadonlyArray extends globalThis.ReadonlyArray, Bind {} +export interface Duplicate extends Bind { [Symbol.iterator](): Iterator, [0]: T, [1]: T } export declare namespace Type { export { diff --git a/packages/registry/src/optics.ts b/packages/registry/src/optics.ts index 4be31794..eeb94de7 100644 --- a/packages/registry/src/optics.ts +++ b/packages/registry/src/optics.ts @@ -1,4 +1,3 @@ -import type { HKT, Kind } from './hkt.js' import { Number_isSafeInteger, Number_parseInt, @@ -147,7 +146,7 @@ const FallbackSemigroup = (fallback: unknown) => ({ return acc } else { - Object_entries(xs).reduce( + Object.entries(xs).reduce( (acc, [k, v]) => k in acc ? acc : { ...acc, [k]: f(isMissing(v) ? fallback : v) }, {} as { [x: number]: unknown } ) diff --git a/packages/registry/src/pattern.ts b/packages/registry/src/pattern.ts index ba071780..f942ecb7 100644 --- a/packages/registry/src/pattern.ts +++ b/packages/registry/src/pattern.ts @@ -1,5 +1,7 @@ export const PATTERN = { alphanumeric: '^[a-zA-Z0-9]*$', identifier: '^[$_a-zA-Z][$_a-zA-Z0-9]*$', + identifierNoDollar: '^[_a-zA-Z][_a-zA-Z0-9]*$', exponential: 'e[-|+]?', + newline: '[\r|\n]', } as const diff --git a/packages/registry/src/topological-sort.ts b/packages/registry/src/topological-sort.ts new file mode 100644 index 00000000..1e493e6a --- /dev/null +++ b/packages/registry/src/topological-sort.ts @@ -0,0 +1,133 @@ +/** + * Stolen from: @pnpm/deps.graph-sequencer + * Source code: https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer/src/index.ts + * Date: 2025-09-27 + */ + +export type Graph = Map +export type Groups = T[][] + +export interface Options { + graph: Graph + groups: Groups +} + +export interface Result { + safe: boolean + chunks: Groups + cycles: Groups +} + +/** + * Performs topological sorting on a graph while supporting node restrictions. + * + * @param {Graph} graph - The graph represented as a Map where keys are nodes and values are their outgoing edges. + * @param {T[]} includedNodes - An array of nodes that should be included in the sorting process. Other nodes will be ignored. + * @returns {Result} An object containing the result of the sorting, including safe, chunks, and cycles. + */ +export function topologicalSort(graph: Graph, includedNodes: T[] = [...graph.keys()]): Result { + // Initialize reverseGraph with empty arrays for all nodes. + const reverseGraph = new Map() + for (const key of graph.keys()) { + reverseGraph.set(key, []) + } + + // Calculate outDegree and reverseGraph for the included nodes. + const nodes = new Set(includedNodes) + const visited = new Set() + const outDegree = new Map() + for (const [from, edges] of graph.entries()) { + outDegree.set(from, 0) + for (const to of edges) { + if (nodes.has(from) && nodes.has(to)) { + changeOutDegree(from, 1) + reverseGraph.get(to)!.push(from) + } + } + if (!nodes.has(from)) { + visited.add(from) + } + } + + const chunks: T[][] = [] + const cycles: T[][] = [] + let safe = true + while (nodes.size) { + const chunk: T[] = [] + let minDegree = Number.MAX_SAFE_INTEGER + for (const node of nodes) { + const degree = outDegree.get(node)! + if (degree === 0) { + chunk.push(node) + } + minDegree = Math.min(minDegree, degree) + } + + if (minDegree === 0) { + chunk.forEach(removeNode) + chunks.push(chunk) + } else { + const cycleNodes: T[] = [] + for (const node of nodes) { + const cycle = findCycle(node) + if (cycle.length) { + cycles.push(cycle) + cycle.forEach(removeNode) + cycleNodes.push(...cycle) + + if (cycle.length > 1) { + safe = false + } + } + } + chunks.push(cycleNodes) + } + } + + return { safe, chunks, cycles } + + // Function to update the outDegree of a node. + function changeOutDegree(node: T, value: number) { + const degree = outDegree.get(node) ?? 0 + outDegree.set(node, degree + value) + } + + // Function to remove a node from the graph. + function removeNode(node: T) { + for (const from of reverseGraph.get(node)!) { + changeOutDegree(from, -1) + } + visited.add(node) + nodes.delete(node) + } + + function findCycle(startNode: T): T[] { + const queue: Array<[T, T[]]> = [[startNode, [startNode]]] + const cycleVisited = new Set() + const cycles: T[][] = [] + + while (queue.length) { + const [id, cycle] = queue.shift()! + const target = graph.get(id) + if (target === undefined) continue + for (const to of target) { + if (to === startNode) { + cycleVisited.add(to) + cycles.push([...cycle]) + continue + } + if (visited.has(to) || cycleVisited.has(to)) { + continue + } + cycleVisited.add(to) + queue.push([to, [...cycle, to]]) + } + } + + if (!cycles.length) { + return [] + } + cycles.sort((a, b) => b.length - a.length) + return cycles[0] + } +} diff --git a/packages/schema-errors/src/json.ts b/packages/schema-errors/src/json.ts index ce81c783..06d5d1f0 100644 --- a/packages/schema-errors/src/json.ts +++ b/packages/schema-errors/src/json.ts @@ -1,7 +1,6 @@ import type { Mutable, Unknown } from '@traversable/registry' import { fn, - Object_entries, Object_hasOwn, NS, } from '@traversable/registry' @@ -64,7 +63,7 @@ export function object( function validateJsonObject(got: T | Unknown, path: (keyof any)[] = defaultPath): ValidationError[] { return !JSON.isObject(got) ? ERRORS(got, path) - : Object_entries(validators).flatMap(([k, validator]) => Object_hasOwn(got, k) + : Object.entries(validators).flatMap(([k, validator]) => Object_hasOwn(got, k) ? validator(got[k], [...path, k]) : KEY_ERRORS(k, got, path) ) diff --git a/packages/schema-errors/src/validators.ts b/packages/schema-errors/src/validators.ts index 14ceed1f..ffeeca61 100644 --- a/packages/schema-errors/src/validators.ts +++ b/packages/schema-errors/src/validators.ts @@ -3,7 +3,6 @@ import { getConfig, Number_isFinite, Number_isSafeInteger, - Object_entries, Object_hasOwn, symbol, URI, @@ -228,7 +227,7 @@ export function record( const ARRAYS_ARE_OK = getConfig().schema.treatArraysAsObjects return (!got || typeof got !== 'object' || (ARRAYS_ARE_OK ? false : Array_isArray(got))) ? ERRORS(got, path) - : Object_entries(got).flatMap(([k, v]) => x.def(v, [...path, k])) + : Object.entries(got).flatMap(([k, v]) => x.def(v, [...path, k])) } validateRecord.tag = URI.record return validateRecord @@ -319,7 +318,7 @@ export function object( || typeof got !== 'object' || NON_ARRAY_CHECK(got) ) ? ERRORS(got, path) - : Object_entries(x.def).flatMap(([k, validator]): ValidationError[] => { + : Object.entries(x.def).flatMap(([k, validator]): ValidationError[] => { const IS_OPTIONAL = symbol.optional in validator && typeof validator[symbol.optional] === 'number' let LOCAL_PATH = [...path, k] return !Object_hasOwn(got, k) diff --git a/packages/schema/src/has.ts b/packages/schema/src/has.ts index 23e8ce9c..0bfbc4aa 100644 --- a/packages/schema/src/has.ts +++ b/packages/schema/src/has.ts @@ -13,6 +13,6 @@ export interface has { export function has(...args: readonly [...path: KS, leafSchema?: S]): has> export function has(...args: readonly [...path: KS, leafSchema?: S]): has export function has(...path: readonly [...KS]): has> -export function has(...args: readonly [...KS]) { +export function has(...args: readonly [...KS]): unknown { return has_(...args) } diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 83766713..83fe630d 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -20,7 +20,6 @@ import { Number_isNatural, Number_isSafeInteger, Object_assign, - Object_entries, Object_keys, parseArgs, parseKey, @@ -160,7 +159,7 @@ const serialize = (json: unknown) => { case typeof x === 'symbol': return String(x) case Array_isArray(x): return x.length === 0 ? '[]' : `[${x.map(go).join(', ')}]` case !!x && typeof x === 'object': { - const xs = Object_entries(x).map(([k, v]) => `${parseKey(k)}: ${go(v)}`) + const xs = Object.entries(x).map(([k, v]) => `${parseKey(k)}: ${go(v)}`) return xs.length === 0 ? `{}` : `{ ${xs.join(', ')} }` } } diff --git a/packages/schema/test/seed.ts b/packages/schema/test/seed.ts index 89614f03..63915222 100644 --- a/packages/schema/test/seed.ts +++ b/packages/schema/test/seed.ts @@ -105,10 +105,10 @@ const isComposite = (x: unknown) => Array_isArray(x) || (x !== null && typeof x /** @internal */ const isNumeric = (x: unknown) => typeof x === 'number' || typeof x === 'bigint' -interface SeedWithRoot< +type SeedWithRoot< Root extends keyof Builder, T extends Partial -> extends T.newtype { +> = T & { tree: T[keyof T] root: fc.Arbitrary } diff --git a/packages/typebox-test/src/generator-bounds.ts b/packages/typebox-test/src/generator-bounds.ts index 329bdb14..1f9e110a 100644 --- a/packages/typebox-test/src/generator-bounds.ts +++ b/packages/typebox-test/src/generator-bounds.ts @@ -1,12 +1,14 @@ import * as fc from 'fast-check' +import { + fn, + Number_isFinite, + Number_isNatural, + Number_isSafeInteger, + Object_is, +} from '@traversable/registry' -import type { newtype } from '@traversable/registry' -import { fn, Number_isFinite, Number_isNatural, Number_isSafeInteger, Object_is } from '@traversable/registry' - -/** @internal */ const nullable = (model: fc.Arbitrary) => fc.oneof(fc.constant(null), fc.constant(null), model) -/** @internal */ const isBigInt = (x: unknown) => typeof x === 'bigint' export const defaultDoubleConstraints = { @@ -17,8 +19,8 @@ export const defaultDoubleConstraints = { const defaultIntBounds = [-0x1000, +0x1000, null] satisfies Bounds_int const defaultBigIntBounds = [-0x1000000n, 0x1000000n, null] satisfies Bounds_bigint const defaultNumberBounds = [-0x10000, +0x10000, null, false, false] satisfies Bounds_number -const defaultStringBounds = [0, +0x40] satisfies Bounds_string -const defaultArrayBounds = [0, +0x10] satisfies Bounds_array +const defaultStringBounds = [0, +0x40, null] satisfies Bounds_string +const defaultArrayBounds = [0, +0x10, null] satisfies Bounds_array export const defaults = { int: defaultIntBounds, @@ -68,11 +70,11 @@ const clampArrayMax = clampMax(defaults.array[0], defaults.array[1], Number_isNa export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: model, maximum: model }) export { Bounds_int as int } -interface Bounds_int extends newtype<[ +type Bounds_int = [ minimum: number | null, maximum: number | null, multipleOf: number | null, -]> {} +] const Bounds_int : (model: fc.Arbitrary) => fc.Arbitrary @@ -84,11 +86,11 @@ const Bounds_int ]) export { Bounds_bigint as bigint } -interface Bounds_bigint extends newtype<[ +type Bounds_bigint = [ minimum: bigint | null, maximum: bigint | null, multipleOf: bigint | null, -]> {} +] const Bounds_bigint : (model: fc.Arbitrary) => fc.Arbitrary @@ -99,28 +101,29 @@ const Bounds_bigint ]) export { Bounds_string as string } -interface Bounds_string extends newtype<[ +type Bounds_string = [ minLength: number | null, maxLength: number | null, -]> {} + exactLength: number | null, +] const Bounds_string : (model: fc.Arbitrary) => fc.Arbitrary = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map( ([x, y, length]) => Number_isNatural(length) - ? [null, null] satisfies [any, any] + ? [null, null, length] satisfies Bounds_string // [clampString(length), clampString(length)] - : [clampStringMin(x, y), clampStringMax(y, x)] - ) + : [clampStringMin(x, y), clampStringMax(y, x), null] satisfies Bounds_string + ) export { Bounds_number as number } -interface Bounds_number extends newtype<[ +type Bounds_number = [ minimum: number | null, maximum: number | null, multipleOf: number | null, exclusiveMinimum: boolean, exclusiveMaximum: boolean, -]> {} +] const deltaIsSubEpsilon = (x: number, y: number) => Math.abs(x - y) < Number.EPSILON @@ -153,10 +156,11 @@ const Bounds_number ) export { Bounds_array as array } -interface Bounds_array extends newtype<[ +type Bounds_array = [ minLength: number | null, maxLength: number | null, -]> {} + exactLength: number | null, +] const Bounds_array : (model: fc.Arbitrary) => fc.Arbitrary @@ -166,8 +170,8 @@ const Bounds_array fc.constant(null) ).map(([x, y, exactLength]) => Number_isNatural(exactLength) - ? [null, null] - : [clampArrayMin(x, y), clampArrayMax(y, x)] + ? [null, null, exactLength] satisfies Bounds_array + : [clampArrayMin(x, y), clampArrayMax(y, x), null] satisfies Bounds_array ) @@ -207,9 +211,9 @@ export const numberBoundsToDoubleConstraints export const stringBoundsToStringConstraints : (bounds?: Bounds_string) => fc.StringConstraints - = ([minLength, maxLength] = defaultStringBounds) => ({ + = ([minLength, maxLength, _exactLength] = defaultStringBounds) => ({ minLength: minLength ?? void 0, - maxLength: maxLength ?? void 0 + maxLength: maxLength ?? void 0, }) satisfies fc.StringConstraints export const arrayBoundsToArrayConstraints diff --git a/packages/typebox-test/src/generator.ts b/packages/typebox-test/src/generator.ts index 55505c6a..61e5b4f0 100644 --- a/packages/typebox-test/src/generator.ts +++ b/packages/typebox-test/src/generator.ts @@ -1,7 +1,7 @@ import * as T from '@sinclair/typebox' import * as fc from 'fast-check' -import type { newtype, inline } from '@traversable/registry' +import type { inline } from '@traversable/registry' import { Array_isArray, fn, @@ -95,7 +95,7 @@ export interface SeedBuilder { (tie: fc.LetrecTypedTie, $: Config.byTypeName[K]): fc.Arbitrary } -export interface SeedMap extends newtype<{ [K in keyof Seed]: SeedBuilder }> {} +export type SeedMap = { [K in keyof Seed]: SeedBuilder } export const SeedMap = { ...TerminalMap, ...BoundableMap, @@ -164,33 +164,33 @@ export const T_number export const T_string : (bounds?: Bounds.string) => T.TString = (bounds = Bounds.defaults.string) => { - const [min, max, exactLength] = bounds + const [min, max, _exactLength] = bounds let options: Parameters[0] = {} - if (Number_isNatural(exactLength)) { - options.minLength = exactLength - options.maxLength = exactLength - return T.String(options) - } else { - if (Number_isNatural(min)) options.minLength = min - if (Number_isNatural(max)) options.maxLength = max - return T.String(options) - } + // if (Number_isNatural(exactLength)) { + // options.minLength = exactLength + // options.maxLength = exactLength + // return T.String(options) + // } else { + if (Number_isNatural(min)) options.minLength = min + if (Number_isNatural(max)) options.maxLength = max + return T.String(options) + // } } export const T_array : (items: T, bounds?: Bounds.array) => T.TArray = (items, bounds = Bounds.defaults.array) => { - const [min, max, exactLength] = bounds + const [min, max, _exactLength] = bounds let schema = T.Array(items) - if (Number_isNatural(exactLength)) { - schema.maxItems = exactLength - schema.minItems = exactLength - return schema - } else { - if (Number_isNatural(min)) schema.minItems = min - if (Number_isNatural(max)) schema.maxItems = max - return schema - } + // if (Number_isNatural(exactLength)) { + // schema.maxItems = exactLength + // schema.minItems = exactLength + // return schema + // } else { + if (Number_isNatural(min)) schema.minItems = min + if (Number_isNatural(max)) schema.maxItems = max + return schema + // } } const unboundedSeed = { @@ -249,7 +249,8 @@ export declare namespace Gen { type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary } type Values = never | T[Exclude] type InferArb = S extends fc.Arbitrary ? T : never - interface Builder extends newtype { ['*']: fc.Arbitrary>> } + type Builder = T & BuilderStar + interface BuilderStar { ['*']: fc.Arbitrary>> } type BuildBuilder, Out extends {} = BuilderBase> = never | Builder type BuilderBase, $ extends ParseOptions = ParseOptions> = never | & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) diff --git a/packages/typebox-types/src/to-type.ts b/packages/typebox-types/src/to-type.ts index 8424efe3..bdcd21d0 100644 --- a/packages/typebox-types/src/to-type.ts +++ b/packages/typebox-types/src/to-type.ts @@ -10,17 +10,10 @@ import * as F from './functor.js' function canBeInterface(x: unknown): boolean { return F.tagged('object')(x) - || F.tagged('array')(x) - || F.tagged('record')(x) - || F.tagged('tuple')(x) - || F.tagged('allOf')(x) -} - -function needsNewtype(x: unknown): boolean { - return F.tagged('object')(x) - || F.tagged('record')(x) - || F.tagged('tuple')(x) - || F.tagged('allOf')(x) + // || F.tagged('array')(x) + // || F.tagged('record')(x) + // || F.tagged('tuple')(x) + // || F.tagged('allOf')(x) } const algebra = F.fold((x, ix, input) => { @@ -110,15 +103,9 @@ export function toType(schematic: Partial, options?: toType.Options): const $ = parseOptions(options) let TYPE = algebra(schematic as never) if (TYPE.startsWith('(') && TYPE.endsWith(')')) TYPE = TYPE.slice(1, -1) - const NEWTYPE = !$.includeNewtypeDeclaration ? null : [ - `// @ts-expect-error: newtype hack`, - `interface newtype extends T {}`, - ].join('\n') return $.typeName === undefined ? TYPE - : $.preferInterface && canBeInterface(schematic) ? [ - needsNewtype(schematic) ? NEWTYPE : null, - `interface ${$.typeName} extends ${needsNewtype(schematic) ? `newtype<${TYPE}>` : TYPE} {}` - ].filter((_) => _ !== null).join('\n') + : $.preferInterface && canBeInterface(schematic) + ? `interface ${$.typeName} ${TYPE}` : `type ${$.typeName} = ${TYPE}` } @@ -127,7 +114,6 @@ function parseOptions(options: toType.Options = {}): Partial interface MyType { a: number } */ preferInterface: boolean - /** - * ## {@link optionsWithInterface.includeNewtypeDeclaration `toType.Options.includeNewtypeDeclaration`} - * - * Default: true - */ - includeNewtypeDeclaration?: boolean } declare const optionsWithOptionalTypeName: { diff --git a/packages/valibot-test/src/generator.ts b/packages/valibot-test/src/generator.ts index 58f7bc31..0e517ca8 100644 --- a/packages/valibot-test/src/generator.ts +++ b/packages/valibot-test/src/generator.ts @@ -2,7 +2,7 @@ import * as v from 'valibot' import * as fc from 'fast-check' import type { LowerBound } from '@traversable/valibot-types' -import type { newtype, inline } from '@traversable/registry' +import type { inline } from '@traversable/registry' import { Array_isArray, fn, @@ -157,7 +157,7 @@ export interface SeedBuilder { (tie: fc.LetrecTypedTie, $: Config.byTypeName[K]): fc.Arbitrary } -export interface SeedMap extends newtype<{ [K in keyof Seed]: SeedBuilder }> {} +export type SeedMap = { [K in keyof Seed]: SeedBuilder } export const SeedMap = { ...TerminalMap, ...BoundableMap, @@ -290,7 +290,8 @@ export declare namespace Gen { type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary } type Values = never | T[Exclude] type InferArb = S extends fc.Arbitrary ? T : never - interface Builder extends newtype { ['*']: fc.Arbitrary>> } + type Builder = T & BuilderStar + interface BuilderStar { ['*']: fc.Arbitrary>> } type BuildBuilder, Out extends {} = BuilderBase> = never | Builder type BuilderBase, $ extends ParseOptions = ParseOptions> = never | & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) diff --git a/packages/valibot-types/src/functor.ts b/packages/valibot-types/src/functor.ts index 98df8a75..d13b12e9 100644 --- a/packages/valibot-types/src/functor.ts +++ b/packages/valibot-types/src/functor.ts @@ -79,7 +79,6 @@ export declare namespace V { | V.Undefined | V.Unknown | V.Void - /** @deprecated */ | V.Promise type Unary = | V.Array @@ -177,7 +176,6 @@ export declare namespace V { interface LooseObject { type: Tag['looseObject'], entries: { [x: string]: S } } interface ObjectWithRest { type: Tag['objectWithRest'], entries: { [x: string]: S }, rest: S } interface StrictObject { type: Tag['strictObject'], entries: { [x: string]: S } } - /** @deprecated */ interface Promise { type: Tag['promise'] } } diff --git a/packages/valibot-types/src/to-string.ts b/packages/valibot-types/src/to-string.ts index aa8e1266..a9a56129 100644 --- a/packages/valibot-types/src/to-string.ts +++ b/packages/valibot-types/src/to-string.ts @@ -1,4 +1,3 @@ -import type * as v from 'valibot' import * as F from './functor.js' import { fn, Object_entries, parseKey } from '@traversable/registry' import { tagged } from './utils.js' diff --git a/packages/valibot/src/to-type.ts b/packages/valibot/src/to-type.ts index ae20487a..653a3886 100644 --- a/packages/valibot/src/to-type.ts +++ b/packages/valibot/src/to-type.ts @@ -61,12 +61,6 @@ export type WithInterface = { * // => interface MyType { a: number } */ preferInterface: boolean - /** - * ## {@link WithInterface `toType.Options.includeNewtypeDeclaration`} - * - * @default true - */ - includeNewtypeDeclaration?: boolean /** * ## {@link WithInterface `toType.Options.preserveJsDocs`} * @@ -103,28 +97,12 @@ function canBeInterface(x: unknown): boolean { || tagged('objectWithRest', x) || tagged('looseObject', x) || tagged('strictObject', x) - || tagged('array', x) - || tagged('record', x) - || tagged('tuple', x) - || tagged('looseTuple', x) - || tagged('strictTuple', x) - || tagged('tupleWithRest', x) - || tagged('intersect', x) - || tagged('set', x) - || tagged('map', x) } -function needsNewtype(x: unknown): boolean { - return tagged('object', x) - || tagged('objectWithRest', x) - || tagged('looseObject', x) - || tagged('strictObject', x) - || tagged('record', x) - || tagged('tuple', x) - || tagged('looseTuple', x) - || tagged('strictTuple', x) - || tagged('tupleWithRest', x) - || tagged('intersect', x) + +function canBeInterfaceViaExtends(x: unknown): boolean { + return tagged('set', x) + || tagged('map', x) } function preserveJsDocsEnabled(ix: F.Functor.Index) { @@ -173,6 +151,7 @@ const fold = F.fold((x, ix, input) => { case tagged('literal')(x): return stringifyLiteral(x.literal) case tagged('array')(x): return isReadonly(input) ? `ReadonlyArray<${x.item}>` : `Array<${x.item}>` case tagged('record')(x): return `Record<${x.key}, ${x.value}>` + case tagged('promise')(x): return `Promise` case tagged('intersect')(x): return x.options.length === 0 ? 'unknown' : x.options.length === 1 ? x.options[0] @@ -396,16 +375,12 @@ export function toType(type: v.BaseSchema | F.V.Hole, option const $ = parseOptions(options) let TYPE = fold(type as never, { ...F.defaultIndex, ...$ } as never) if (TYPE.startsWith('(') && TYPE.endsWith(')')) TYPE = TYPE.slice(1, -1) - const NEWTYPE = !$.includeNewtypeDeclaration ? null : [ - `// @ts-expect-error: newtype hack`, - `interface newtype extends T {}`, - ].join('\n') return $.typeName === undefined ? TYPE - : $.preferInterface && canBeInterface(type) ? [ - needsNewtype(type) ? NEWTYPE : null, - `interface ${$.typeName} extends ${needsNewtype(type) ? `newtype<${TYPE}>` : TYPE} {}` - ].filter((_) => _ !== null).join('\n') - : `type ${$.typeName} = ${TYPE}` + : $.preferInterface && canBeInterface(type) + ? `interface ${$.typeName} ${TYPE}` + : $.preferInterface && canBeInterfaceViaExtends(type) + ? `interface ${$.typeName} extends ${TYPE} {}` + : `type ${$.typeName} = ${TYPE}` } toType.unsupported = unsupported @@ -414,7 +389,6 @@ function parseOptions(options?: toType.Options): Partial function parseOptions($: toType.Options = {}): Partial { return { typeName: $?.typeName, - ...'includeNewtypeDeclaration' in $ && { includeNewtypeDeclaration: $.includeNewtypeDeclaration }, ...'preferInterface' in $ && { preferInterface: $.preferInterface }, ...'preserveJsDocs' in $ && { preserveJsDocs: $.preserveJsDocs }, } diff --git a/packages/valibot/test/to-type.fuzz.test.ts b/packages/valibot/test/to-type.fuzz.test.ts index ab6f662b..4c098c44 100644 --- a/packages/valibot/test/to-type.fuzz.test.ts +++ b/packages/valibot/test/to-type.fuzz.test.ts @@ -5,7 +5,7 @@ import * as fs from 'node:fs' import { vx } from '@traversable/valibot' import { vxTest } from '@traversable/valibot-test' -const NUM_RUNS = 100 +const NUM_RUNS = 1000 // const NUM_RUNS = 10_000 const EXCLUDE = [ @@ -60,7 +60,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/valibot❳: vx.toType fuzz tes const interfaces = gen.map((schema, ix) => { const string = vx.toString(schema as never) - const INTERFACE = vx.toType(schema as never, { typeName: `_${ix + 1}`, preferInterface: true, includeNewtypeDeclaration: false }) + const INTERFACE = vx.toType(schema as never, { typeName: `_${ix + 1}`, preferInterface: true }) return [ `const _${ix + 1} = ${string}`, `// ^?`, @@ -79,8 +79,6 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/valibot❳: vx.toType fuzz tes const interfacesOut = [ ...imports, - `import type { newtype } from '@traversable/registry'`, - '\n', ...interfaceDeps, '\n', ...interfaces, diff --git a/packages/zod-test/src/generator-bounds.ts b/packages/zod-test/src/generator-bounds.ts index a5ddab2a..5accf739 100644 --- a/packages/zod-test/src/generator-bounds.ts +++ b/packages/zod-test/src/generator-bounds.ts @@ -1,13 +1,15 @@ import * as fc from 'fast-check' - -import type { newtype } from '@traversable/registry' -import { fn, Number_isFinite, Number_isNatural, Number_isSafeInteger, Object_is } from '@traversable/registry' +import { + fn, + Number_isFinite, + Number_isNatural, + Number_isSafeInteger, + Object_is, +} from '@traversable/registry' import { Z } from '@traversable/zod-types' -/** @internal */ const nullable = (model: fc.Arbitrary) => fc.oneof(fc.constant(null), fc.constant(null), model) -/** @internal */ const isBigInt = (x: unknown) => typeof x === 'bigint' export const defaultDoubleConstraints = { @@ -18,8 +20,8 @@ export const defaultDoubleConstraints = { const defaultIntBounds = [-0x1000, +0x1000, null] satisfies Bounds_int const defaultBigIntBounds = [-0x1000000n, 0x1000000n, null] satisfies Bounds_bigint const defaultNumberBounds = [-0x10000, +0x10000, null, false, false] satisfies Bounds_number -const defaultStringBounds = [0, +0x40] satisfies Bounds_string -const defaultArrayBounds = [0, +0x10] satisfies Bounds_array +const defaultStringBounds = [0, +0x40, null] satisfies Bounds_string +const defaultArrayBounds = [0, +0x10, null] satisfies Bounds_array export const defaults = { int: defaultIntBounds, @@ -69,11 +71,11 @@ const clampArrayMax = clampMax(defaults.array[0], defaults.array[1], Number_isNa export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: model, maximum: model }) export { Bounds_int as int } -interface Bounds_int extends newtype<[ +type Bounds_int = [ minimum: number | null, maximum: number | null, multipleOf: number | null, -]> {} +] const Bounds_int : (model: fc.Arbitrary) => fc.Arbitrary @@ -85,11 +87,11 @@ const Bounds_int ]) export { Bounds_bigint as bigint } -interface Bounds_bigint extends newtype<[ +type Bounds_bigint = [ minimum: bigint | null, maximum: bigint | null, multipleOf: bigint | null, -]> {} +] const Bounds_bigint : (model: fc.Arbitrary) => fc.Arbitrary @@ -100,28 +102,28 @@ const Bounds_bigint ]) export { Bounds_string as string } -interface Bounds_string extends newtype<[ +type Bounds_string = [ minLength: number | null, maxLength: number | null, -]> {} + exactLength: number | null, +] const Bounds_string : (model: fc.Arbitrary) => fc.Arbitrary = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map( - ([x, y, length]) => Number_isNatural(length) - ? [null, null] satisfies [any, any] - // [clampString(length), clampString(length)] - : [clampStringMin(x, y), clampStringMax(y, x)] + ([x, y, exactLength]) => Number_isNatural(exactLength) + ? [null, null, exactLength] satisfies Bounds_string + : [clampStringMin(x, y), clampStringMax(y, x), null] satisfies Bounds_string ) export { Bounds_number as number } -interface Bounds_number extends newtype<[ +type Bounds_number = [ minimum: number | null, maximum: number | null, multipleOf: number | null, exclusiveMinimum: boolean, exclusiveMaximum: boolean, -]> {} +] const deltaIsSubEpsilon = (x: number, y: number) => Math.abs(x - y) < Number.EPSILON @@ -154,10 +156,11 @@ const Bounds_number ) export { Bounds_array as array } -interface Bounds_array extends newtype<[ +type Bounds_array = [ minLength: number | null, maxLength: number | null, -]> {} + exactLength: number | null, +] const Bounds_array : (model: fc.Arbitrary) => fc.Arbitrary @@ -167,8 +170,8 @@ const Bounds_array fc.constant(null) ).map(([x, y, exactLength]) => Number_isNatural(exactLength) - ? [null, null] - : [clampArrayMin(x, y), clampArrayMax(y, x)] + ? [null, null, exactLength] satisfies Bounds_array + : [clampArrayMin(x, y), clampArrayMax(y, x), null] satisfies Bounds_array ) export const intBoundsToIntegerConstraints diff --git a/packages/zod-test/src/generator-options.ts b/packages/zod-test/src/generator-options.ts index 6237d6c2..ce6628d9 100644 --- a/packages/zod-test/src/generator-options.ts +++ b/packages/zod-test/src/generator-options.ts @@ -273,7 +273,6 @@ export const defaults = { } as const satisfies OptionsBase export function parseOptions(options?: Opts): Config> -export function parseOptions(options?: Options): Config export function parseOptions(options: Options = defaults as never): Config { const { exclude = defaults.exclude, diff --git a/packages/zod-test/src/generator.ts b/packages/zod-test/src/generator.ts index a07b1120..d561b4cd 100644 --- a/packages/zod-test/src/generator.ts +++ b/packages/zod-test/src/generator.ts @@ -213,16 +213,16 @@ export const z_string = (bounds = Bounds.defaults.string) => { const [min, max, exactLength] = bounds let schema = z.string() - if (Number_isNatural(exactLength)) return ( - schema = schema.min(exactLength), - schema = schema.max(exactLength), - schema - ) - else { - if (Number_isNatural(min)) schema = schema.min(min) - if (Number_isNatural(max)) schema = schema.max(max) - return schema - } + // if (Number_isNatural(exactLength)) return ( + // schema = schema.min(exactLength), + // schema = schema.max(exactLength), + // schema + // ) + // else { + if (Number_isNatural(min)) schema = schema.min(min) + if (Number_isNatural(max)) schema = schema.max(max) + return schema + // } } export const z_array @@ -230,16 +230,16 @@ export const z_array = (elementSchema, bounds = Bounds.defaults.array) => { const [min, max, exactLength] = bounds let schema = z.array(elementSchema) - if (Number_isNatural(exactLength)) return ( - schema = schema.min(exactLength), - schema = schema.max(exactLength), - schema - ) - else { - if (Number_isNatural(min)) schema = schema.min(min) - if (Number_isNatural(max)) schema = schema.max(max) - return schema - } + // if (Number_isNatural(exactLength)) return ( + // schema = schema.min(exactLength), + // schema = schema.max(exactLength), + // schema + // ) + // else { + if (Number_isNatural(min)) schema = schema.min(min) + if (Number_isNatural(max)) schema = schema.max(max) + return schema + // } } const unboundedSeed = { @@ -296,15 +296,15 @@ export function Builder(base: Gen.Base) { export declare namespace Gen { type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary } - type Values = never | T[Exclude] + type Values = T[Exclude] type InferArb = S extends fc.Arbitrary ? T : never - /* @ts-expect-error */ - interface Builder extends T { ['*']: fc.Arbitrary>> } - type BuildBuilder, Out extends {} = BuilderBase> = never | Builder - type BuilderBase, $ extends ParseOptions = ParseOptions> = never | + type Builder = T & BuilderStar + interface BuilderStar { ['*']: fc.Arbitrary>> } + type BuildBuilder, Out extends {} = BuilderBase> = Builder + type BuilderBase, $ extends ParseOptions = ParseOptions> = & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) & { [K in Exclude<$['include'], $['exclude']>]: fc.Arbitrary } - type ParseOptions> = never | { + type ParseOptions> = { include: Options['include'] extends readonly unknown[] ? Options['include'][number] : keyof T exclude: Options['exclude'] extends readonly unknown[] ? Options['exclude'][number] : never root: Options['root'] extends keyof T ? T[Options['root']] : never @@ -526,7 +526,7 @@ const GeneratorByTag = { export function seedToValidDataGenerator(seed: Seed.F, options?: Config.Options): fc.Arbitrary export function seedToValidDataGenerator(seed: Seed.F, options?: Config.Options): fc.Arbitrary { const $ = Config.parseOptions(options) - return fold>((x, isProperty) => GeneratorByTag[bySeed[x[0]]](x as never, $, isProperty || x[0] === 7500))(seed as never) + return fold>((x, isProperty) => GeneratorByTag[bySeed[x[0]]](x as never, $, isProperty || x[0] === 7500))(seed) } /** @@ -806,7 +806,7 @@ export function seedToSchema(seed: Seed.F) { case x[0] === byTag.lazy: return z.lazy(x[1]) case x[0] === byTag.promise: return PromiseSchemaIsUnsupported('seedToSchema') } - })(seed as never) + })(seed) } /** diff --git a/packages/zod/src/makeLens.ts b/packages/zod/src/makeLens.ts index e69c1dfe..4c41e9f3 100644 --- a/packages/zod/src/makeLens.ts +++ b/packages/zod/src/makeLens.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { Key, newtype, Primitive, Showable } from '@traversable/registry' +import type { Key, Primitive, Showable } from '@traversable/registry' import { Array_from, Array_isArray, @@ -518,19 +518,13 @@ function interpreter(type: T, ..._path: (keyof any)[]) { // console.log() -interface Proxy_object extends newtype< - { [K in keyof Args[1]]: Proxy } -> { - [symbol.type]: Args[0] - [symbol.path]: KS -} +type Proxy_object = + & { [K in keyof Args[1]]: Proxy } + & { [symbol.type]: Args[0], [symbol.path]: KS } -interface Proxy_tuple extends newtype< - { [I in Extract]: Proxy } -> { - [symbol.type]: T - [symbol.path]: KS -} +type Proxy_tuple = + & { [I in Extract]: Proxy } + & { [symbol.type]: T, [symbol.path]: KS } export type Atom = | globalThis.Date @@ -571,13 +565,9 @@ interface Proxy_nonfiniteRecord extends newtype< - { [K in S[0]['enum'][keyof S[0]['enum']]]: Proxy } -> { - [DSL.traverseRecord]: Proxy - [symbol.type]: T - [symbol.path]: KS -} +type Proxy_finiteRecord = + & { [K in S[0]['enum'][keyof S[0]['enum']]]: Proxy } + & { [DSL.traverseRecord]: Proxy, [symbol.type]: T, [symbol.path]: KS } interface Proxy_primitive { [symbol.type]: T @@ -611,9 +601,9 @@ type UnionType< [...KS, disjoint: symbol.disjoint] > -interface Proxy_union extends newtype< - { [I in Extract as `${GlobalDSL['unionPrefix']}${I}`]: Proxy } -> { [symbol.type]: T, [symbol.path]: KS } +type Proxy_union = + & { [I in Extract as `${GlobalDSL['unionPrefix']}${I}`]: Proxy } + & { [symbol.type]: T, [symbol.path]: KS } type Disjoint = never | Proxy_disjointUnion< U[2], @@ -621,7 +611,7 @@ type Disjoint -interface Proxy_disjointUnion extends newtype { +type Proxy_disjointUnion = S & { [symbol.type]: T [symbol.path]: KS } diff --git a/packages/zod/src/to-type.ts b/packages/zod/src/to-type.ts index 920181ff..89921e6c 100644 --- a/packages/zod/src/to-type.ts +++ b/packages/zod/src/to-type.ts @@ -64,12 +64,6 @@ export type WithInterface = { * // => interface MyType { a: number } */ preferInterface: boolean - /** - * ## {@link WithInterface `toType.Options.includeNewtypeDeclaration`} - * - * @default true - */ - includeNewtypeDeclaration?: boolean /** * ## {@link WithInterface `toType.Options.preserveJsDocs`} * @@ -127,19 +121,11 @@ function canBeReadonly(x: unknown): boolean { function canBeInterface(x: unknown): boolean { return tagged('object', x) - || tagged('array', x) - || tagged('record', x) - || tagged('tuple', x) - || tagged('intersection', x) - || tagged('set', x) - || tagged('map', x) } -function needsNewtype(x: unknown): boolean { - return tagged('object', x) - || tagged('record', x) - || tagged('tuple', x) - || tagged('intersection', x) +function canBeInterfaceViaExtends(x: unknown): boolean { + return tagged('set', x) + || tagged('map', x) } function preserveJsDocsEnabled(ix: F.CompilerIndex) { @@ -394,16 +380,12 @@ export function toType(type: z.ZodType | z.core.$ZodType | F.Z.Hole, option const $ = parseOptions(options) let TYPE = compile(type as never, { ...F.defaultIndex, ...$ } as never) if (TYPE.startsWith('(') && TYPE.endsWith(')')) TYPE = TYPE.slice(1, -1) - const NEWTYPE = !$.includeNewtypeDeclaration ? null : [ - `// @ts-expect-error: newtype hack`, - `interface newtype extends T {}`, - ].join('\n') return $.typeName === undefined ? TYPE - : $.preferInterface && canBeInterface(type) ? [ - needsNewtype(type) ? NEWTYPE : null, - `interface ${$.typeName} extends ${needsNewtype(type) ? `newtype<${TYPE}>` : TYPE} {}` - ].filter((_) => _ !== null).join('\n') - : `type ${$.typeName} = ${TYPE}` + : $.preferInterface && canBeInterface(type) + ? `interface ${$.typeName} ${TYPE}` + : $.preferInterface && canBeInterfaceViaExtends(type) + ? `interface ${$.typeName} extends ${TYPE} {}` + : `type ${$.typeName} = ${TYPE}` } toType.unsupported = toType_unsupported @@ -413,7 +395,6 @@ function parseOptions(options?: toType.Options): Partial function parseOptions($: toType.Options = {}): Partial { return { typeName: $?.typeName, - ...'includeNewtypeDeclaration' in $ && { includeNewtypeDeclaration: $.includeNewtypeDeclaration }, ...'preferInterface' in $ && { preferInterface: $.preferInterface }, ...'preserveJsDocs' in $ && { preserveJsDocs: $.preserveJsDocs }, } diff --git a/packages/zod/test/to-type.fuzz.test.ts b/packages/zod/test/to-type.fuzz.test.ts index 6d88c9a1..f8036b74 100644 --- a/packages/zod/test/to-type.fuzz.test.ts +++ b/packages/zod/test/to-type.fuzz.test.ts @@ -67,7 +67,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/zod❳: integration tests', () const interfaces = gen.map((schema, ix) => { const string = zx.toString(schema as never) - const INTERFACE = zx.toType(schema as never, { typeName: `_${ix + 1}`, preferInterface: true, includeNewtypeDeclaration: false }) + const INTERFACE = zx.toType(schema as never, { typeName: `_${ix + 1}`, preferInterface: true }) return [ `const _${ix + 1} = ${string}`, `// ^?`, @@ -87,8 +87,6 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/zod❳: integration tests', () const interfacesOut = [ ...imports, - `import type { newtype } from '@traversable/registry'`, - '\n', ...interfaceDeps, '\n', ...interfaces, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4e5b239..c68a4b0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ catalogs: fast-check: specifier: 4.1.1 version: 4.1.1 + graphql: + specifier: 16.11.0 + version: 16.11.0 lodash.isequal: specifier: 4.5.0 version: 4.5.0 @@ -66,6 +69,9 @@ catalogs: tinybench: specifier: 3.0.4 version: 3.0.4 + typedoc: + specifier: 0.28.9 + version: 0.28.9 typescript: specifier: 5.9.2 version: 5.9.2 @@ -129,8 +135,8 @@ importers: specifier: 'catalog:' version: 3.0.4 typedoc: - specifier: ^0.28.9 - version: 0.28.10(typescript@5.9.2) + specifier: 'catalog:' + version: 0.28.9(typescript@5.9.2) typescript: specifier: 'catalog:' version: 5.9.2 @@ -321,6 +327,52 @@ importers: version: 2.1.20 publishDirectory: dist + packages/graphql: + dependencies: + '@traversable/graphql-types': + specifier: workspace:^ + version: link:../graphql-types/dist + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + devDependencies: + graphql: + specifier: 'catalog:' + version: 16.11.0 + publishDirectory: dist + + packages/graphql-test: + devDependencies: + '@prettier/sync': + specifier: 'catalog:' + version: 0.5.5(prettier@3.6.2) + '@traversable/graphql-types': + specifier: workspace:^ + version: link:../graphql-types/dist + '@traversable/json': + specifier: workspace:^ + version: link:../json/dist + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + graphql: + specifier: 'catalog:' + version: 16.11.0 + publishDirectory: dist + + packages/graphql-types: + devDependencies: + '@prettier/sync': + specifier: 'catalog:' + version: 0.5.5(prettier@3.6.2) + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + graphql: + specifier: 'catalog:' + version: 16.11.0 + publishDirectory: dist + packages/json: dependencies: fast-check: @@ -1356,36 +1408,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -1471,56 +1529,67 @@ packages: resolution: {integrity: sha512-JWnrj8qZgLWRNHr7NbpdnrQ8kcg09EBBq8jVOjmtlB3c8C6IrynAJSMhMVGME4YfTJzIkJqvSUSVJRqkDnu/aA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.48.0': resolution: {integrity: sha512-9xu92F0TxuMH0tD6tG3+GtngwdgSf8Bnz+YcsPG91/r5Vgh5LNofO48jV55priA95p3c92FLmPM7CvsVlnSbGQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.48.0': resolution: {integrity: sha512-NLtvJB5YpWn7jlp1rJiY0s+G1Z1IVmkDuiywiqUhh96MIraC0n7XQc2SZ1CZz14shqkM+XN2UrfIo7JB6UufOA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.48.0': resolution: {integrity: sha512-QJ4hCOnz2SXgCh+HmpvZkM+0NSGcZACyYS8DGbWn2PbmA0e5xUk4bIP8eqJyNXLtyB4gZ3/XyvKtQ1IFH671vQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.48.0': resolution: {integrity: sha512-Pk0qlGJnhILdIC5zSKQnprFjrGmjfDM7TPZ0FKJxRkoo+kgMRAg4ps1VlTZf8u2vohSicLg7NP+cA5qE96PaFg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.48.0': resolution: {integrity: sha512-/dNFc6rTpoOzgp5GKoYjT6uLo8okR/Chi2ECOmCZiS4oqh3mc95pThWma7Bgyk6/WTEvjDINpiBCuecPLOgBLQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.48.0': resolution: {integrity: sha512-YBwXsvsFI8CVA4ej+bJF2d9uAeIiSkqKSPQNn0Wyh4eMDY4wxuSp71BauPjQNCKK2tD2/ksJ7uhJ8X/PVY9bHQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.48.0': resolution: {integrity: sha512-FI3Rr2aGAtl1aHzbkBIamsQyuauYtTF9SDUJ8n2wMXuuxwchC3QkumZa1TEXYIv/1AUp1a25Kwy6ONArvnyeVQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.48.0': resolution: {integrity: sha512-Dx7qH0/rvNNFmCcIRe1pyQ9/H0XO4v/f0SDoafwRYwc2J7bJZ5N4CHL/cdjamISZ5Cgnon6iazAVRFlxSoHQnQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.48.0': resolution: {integrity: sha512-GUdZKTeKBq9WmEBzvFYuC88yk26vT66lQV8D5+9TgkfbewhLaTHRNATyzpQwwbHIfJvDJ3N9WJ90wK/uR3cy3Q==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.48.0': resolution: {integrity: sha512-ao58Adz/v14MWpQgYAb4a4h3fdw73DrDGtaiF7Opds5wNyEQwtO6M9dBh89nke0yoZzzaegq6J/EXs7eBebG8A==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.48.0': resolution: {integrity: sha512-kpFno46bHtjZVdRIOxqaGeiABiToo2J+st7Yce+aiAoo1H0xPi2keyQIP04n2JjDVuxBN6bSz9R6RdTK5hIppw==} @@ -2563,6 +2632,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3568,8 +3641,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typedoc@0.28.10: - resolution: {integrity: sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==} + typedoc@0.28.9: + resolution: {integrity: sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: @@ -5837,6 +5910,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.11.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -6786,7 +6861,7 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typedoc@0.28.10(typescript@5.9.2): + typedoc@0.28.9(typescript@5.9.2): dependencies: '@gerrit0/mini-shiki': 3.11.0 lunr: 2.3.9 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d152b9ae..ca8d3211 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,8 @@ packages: - bin - packages/*/ +minimumReleaseAge: 1440 + catalog: '@ark/attest': 0.44.8 '@babel/cli': 7.25.9 @@ -22,9 +24,11 @@ catalog: arktype: 2.1.20 babel-plugin-annotate-pure-calls: 0.4.0 fast-check: 4.1.1 + graphql: 16.11.0 lodash.isequal: 4.5.0 madge: 8.0.0 tinybench: 3.0.4 + typedoc: 0.28.9 typescript: 5.9.2 valibot: 1.1.0 vitest: 3.2.4 diff --git a/tsconfig.base.json b/tsconfig.base.json index b4c61481..8f24b6d6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -34,6 +34,12 @@ "@traversable/arktype-types": ["packages/arktype-types/src/index.js"], "@traversable/arktype-types/*": ["packages/arktype-types/*.js"], "@traversable/arktype/*": ["packages/arktype/*.js"], + "@traversable/graphql": ["packages/graphql/src/index.js"], + "@traversable/graphql-test": ["packages/graphql-test/src/index.js"], + "@traversable/graphql-test/*": ["packages/graphql-test/*.js"], + "@traversable/graphql-types": ["packages/graphql-types/src/index.js"], + "@traversable/graphql-types/*": ["packages/graphql-types/*.js"], + "@traversable/graphql/*": ["packages/graphql/*.js"], "@traversable/json": ["packages/json/src/index.js"], "@traversable/json-schema": ["packages/json-schema/src/index.js"], "@traversable/json-schema-test": [ diff --git a/tsconfig.build.json b/tsconfig.build.json index f3db1e87..fa2f292f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,6 +5,9 @@ { "path": "packages/arktype-test/tsconfig.build.json" }, { "path": "packages/arktype-types/tsconfig.build.json" }, { "path": "packages/arktype/tsconfig.build.json" }, + { "path": "packages/graphql-test/tsconfig.build.json" }, + { "path": "packages/graphql-types/tsconfig.build.json" }, + { "path": "packages/graphql/tsconfig.build.json" }, { "path": "packages/json-schema-test/tsconfig.build.json" }, { "path": "packages/json-schema-types/tsconfig.build.json" }, { "path": "packages/json-schema/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index bb6039f0..fe1a435d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,9 @@ { "path": "packages/arktype" }, { "path": "packages/arktype-test" }, { "path": "packages/arktype-types" }, + { "path": "packages/graphql" }, + { "path": "packages/graphql-test" }, + { "path": "packages/graphql-types" }, { "path": "packages/json" }, { "path": "packages/json-schema" }, { "path": "packages/json-schema-test" },