Skip to content

Commit

Permalink
Fixes QueryBuilder types to omit components excluded with none.
Browse files Browse the repository at this point in the history
  • Loading branch information
noahlange committed May 4, 2024
1 parent d33894b commit bf85a50
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 39 deletions.
16 changes: 16 additions & 0 deletions src/lib/QueryBuilder.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Entity } from '../ecs';

import { describe, expectTypeOf, test } from 'vitest';

import { setup } from '../test/helpers';
import { A, B } from '../test/helpers/components';

describe('query modifiers', () => {
test('.none() properly omits components', async () => {
const { ctx } = await setup();

const noB = ctx.query.all.components(A).none.components(B).first()!;

expectTypeOf(noB).toMatchTypeOf<Entity<{ a: A }>>();
});
});
3 changes: 2 additions & 1 deletion src/lib/QueryBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable prettier/prettier */
import type { Entity } from '../ecs';

import { describe, expect, test } from 'vitest';
Expand Down Expand Up @@ -69,7 +70,7 @@ describe('basic query modifiers', () => {

test('.components.none()', async () => {
const { ctx } = await setup();
const noA = ctx.query.none.components(A);
const noA = ctx.query.all.components(B).none.components(A);
expect(noA.get()).toHaveLength(count); // i.e., WithB
});
});
Expand Down
60 changes: 32 additions & 28 deletions src/lib/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
import type { ComponentClass, Context, Entity } from '../ecs';
import type { BaseType, Identifier, KeyedByType, MergeData, PartialByType, QueryStep } from '../types';
import type { Query } from '.';
import type { BaseType, Identifier, KeyedByType, PartialByType, QueryStep, Simplify, StaticTypeNames } from '../types';
import type { Query } from './Query';

import { Constraint } from '../types';

/**
* A query without a qualified constraint is assumed to be "all"
*/
export interface QueryBuilderBase<T extends BaseType = {}> extends QueryBuilderAll<T> {
all: QueryBuilderAll<T>;
any: QueryBuilderAny<T>;
some: QueryBuilderAny<T>;
none: QueryBuilderBase<T>;
get(): Entity<T>[];
first(): Entity<T> | null;
references(...entities: Entity<{}>[]): this;
[Symbol.iterator](): Iterator<Entity<T>>;
export interface QueryState {
tag: Constraint | null;
ids: Identifier[];
}

interface QueryBuilderAll<T extends BaseType = {}> {
components<A extends ComponentClass[]>(...components: A): QueryBuilder<MergeData<T & KeyedByType<A>>>;
tags(...tags: string[]): QueryBuilder<T>;
interface QueryBuilderBase<T extends BaseType = {}> {
get all(): QueryBuilderAll<T>;
get any(): QueryBuilderAny<T>;
get some(): QueryBuilderAny<T>;
get none(): QueryBuilderNone<T>;

tags(...tags: string[]): QueryBuilderBase<T>;
references(...entities: Entity[]): this;

get(): Entity<Simplify<T>>[];
first(): Entity<Simplify<T>> | null;

[Symbol.iterator](): Iterator<Entity<Simplify<T>>>;
}

interface QueryBuilderAny<T extends BaseType = {}> {
components<A extends ComponentClass[]>(...components: A): QueryBuilder<MergeData<T & PartialByType<A>>>;
tags(...tags: string[]): QueryBuilder<T>;
interface QueryBuilderAll<T extends BaseType = {}> extends QueryBuilderBase<T> {
components<A extends ComponentClass[]>(...components: A): QueryBuilderBase<T & KeyedByType<A>>;
}

export interface QueryState {
tag: Constraint | null;
ids: Identifier[];
interface QueryBuilderAny<T extends BaseType = {}> extends QueryBuilderBase<T> {
components<A extends ComponentClass[]>(...components: A): QueryBuilderBase<T & PartialByType<A>>;
}

interface QueryBuilderNone<T extends BaseType = {}> extends QueryBuilderBase<T> {
components<A extends ComponentClass[]>(...components: A): QueryBuilderBase<Omit<T, StaticTypeNames<A>>>;
}

export class QueryBuilder<T extends BaseType = {}> implements QueryBuilderBase<T> {
Expand All @@ -48,6 +51,7 @@ export class QueryBuilder<T extends BaseType = {}> implements QueryBuilderBase<T
this.state.tag = Constraint.ALL;
return this as unknown as QueryBuilderAll<T>;
}

/**
* Mark query parameters as optional, with 1+ required matches.
* A | B
Expand All @@ -70,15 +74,15 @@ export class QueryBuilder<T extends BaseType = {}> implements QueryBuilderBase<T
* Mark query parameters as disqualifying.
* !(A & B)
*/
public get none(): this {
public get none(): QueryBuilderNone<T> {
this.state.tag = Constraint.NONE;
return this;
return this as unknown as QueryBuilderNone<T>;
}

/**
* Constrain results to those referencing one of several entities.
*/
public references(...entities: Entity[]): this {
public references(...entities: Entity[]) {
this.state.tag = Constraint.IN;
this.state.ids = entities.map(entity => entity.id);
return this.step();
Expand All @@ -87,9 +91,9 @@ export class QueryBuilder<T extends BaseType = {}> implements QueryBuilderBase<T
/**
* Constrain results based on one or components.
*/
public components<A extends ComponentClass[]>(...components: A): QueryBuilder<T & KeyedByType<A>> {
public components<A extends ComponentClass[]>(...components: A) {
this.state.ids.push(...components.map(c => c.type));
return this.step() as unknown as QueryBuilder<T & KeyedByType<A>>;
return this.step();
}

/**
Expand Down
20 changes: 10 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { SystemFunction } from './ecs/System';
import type { PluginClass, QueryBuilderBase } from './lib';

// Copy-pasted with permission from https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts
export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
export type Simplify<T> = { [K in keyof T]: T[K] } & {};

// Copy-pasted with permission from https://github.com/sindresorhus/type-fest/blob/main/source/union-to-intersection.d.ts
export type UnionToIntersection<Union> = (Union extends unknown ? (distributedUnion: Union) => void : never) extends (
Expand Down Expand Up @@ -93,6 +93,10 @@ export type Plugins<T extends Plugins<T>> = {
[K in keyof T]: T[K];
};

export type StaticTypeNames<T> = T extends (infer R)[] ? (R extends WithStaticType ? StaticTypeName<R> : never) : never;

export type StaticTypeName<T = $AnyOK> = T extends WithStaticType ? T['type'] : never;

/**
* Some class with a static, readonly `type` property.
*/
Expand All @@ -117,9 +121,7 @@ type InstanceOrRefKeyedByType<T> = T extends WithStaticType
* class B extends Component { static readonly type: 'b' }
* type KeyedByType<[typeof A, typeof B]> // { a: A, b: B }
*/
export type KeyedByType<A extends WithStaticType[]> = Merge<
A extends (infer B)[] ? InstanceOrRefKeyedByType<B> : never
>;
export type KeyedByType<A extends WithStaticType[]> = A extends (infer B)[] ? InstanceOrRefKeyedByType<B> : never;

/**
* Given an array of WithStaticTypes, return a merged array of key-value pairs, with optional values.
Expand All @@ -131,9 +133,9 @@ export type KeyedByType<A extends WithStaticType[]> = Merge<
* class B extends Component { static readonly type: 'b' }
* type PartialByType<[typeof A, typeof B]> // { a?: A, b?: B }
*/
export type PartialByType<A extends WithStaticType[]> = Merge<
A extends (infer B)[] ? Partial<InstanceOrRefKeyedByType<B>> : never
>;
export type PartialByType<A extends WithStaticType[]> = A extends (infer B)[]
? Partial<InstanceOrRefKeyedByType<B>>
: never;

/**
* Given an array of WithStaticTypes, return a merged array of key-value pairs, all with `never` value types
Expand All @@ -145,9 +147,7 @@ export type PartialByType<A extends WithStaticType[]> = Merge<
* class B extends Component { static readonly type: 'b' }
* type NeverByType<[typeof A, typeof B]> // { a: never, b: never }
*/
export type NeverByType<A extends WithStaticType[]> = Merge<
A extends (infer B)[] ? (B extends WithStaticType ? { [key in B['type']]: never } : never) : never
>;
export type NeverByType<T, A extends WithStaticType[]> = Omit<T, StaticTypeNames<A>>;

/**
* Used to define the contents of an entity payload: pass a data partial for a Component and an entity for an EntityRef.
Expand Down

0 comments on commit bf85a50

Please sign in to comment.