From c1520b7a59875a2f919158702c2df92b6742f3d8 Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Mon, 25 Nov 2024 11:00:48 -0600 Subject: [PATCH] leaving for flight, brb --- src/query-parser/common.js | 4 +- src/query-parser/types.js | 92 +++++++++++++++++++++++++++++++++++--- src/query.js | 22 ++++++--- tests/query.test.js | 21 ++++++--- 4 files changed, 119 insertions(+), 20 deletions(-) diff --git a/src/query-parser/common.js b/src/query-parser/common.js index 554ecfb..829e653 100644 --- a/src/query-parser/common.js +++ b/src/query-parser/common.js @@ -1,6 +1,6 @@ import { join } from '../fn.js' import { alpha, alphanumeric, any, char, digit, list, many, map, maybe, noCaseString, separated, seq, skip, string, until } from '../parser.js' -import { Alias, Identifier, Literal, ObjectPath } from './types.js' +import { Alias, Identifier, Literal, ObjectPath, Property } from './types.js' export const Symbol = Object.freeze({ Bracket: Object.freeze({ @@ -47,7 +47,7 @@ const str = quoted const toBoolean = v => v === 'false' ? false : true const boolean = map(([v]) => toBoolean(v), any(string('true'), string('false'))) -const property = seq(skip(Symbol.Period), identifier) +const property = map(([x]) => new Property(x), seq(skip(Symbol.Period), identifier)) const accessor = map(x => new ObjectPath(...x), seq(identifier, property, many(property))) export const literal = map(([x]) => new Literal(x), any(float, integer, str, boolean)) diff --git a/src/query-parser/types.js b/src/query-parser/types.js index 754c9b4..e5a428e 100644 --- a/src/query-parser/types.js +++ b/src/query-parser/types.js @@ -1,4 +1,5 @@ -import { is } from '../fn.js' +import { pipe } from 'bitecs' +import { assoc, assocPath, is, nth } from '../fn.js' class Value { #value @@ -26,6 +27,40 @@ export class Identifier extends Value { constructor(value) { super(value) } + + toString() { + return this.value.toString() + } + + from(component) { + return component + } +} + +export class ObjectIdentifier extends Identifier { + constructor(value) { + super(value) + } + + from(component) { + return new Proxy(component, { + get(target, entity, _receiver) { + return Object.fromEntries( + Object.entries(target).map(([k, v]) => [k, v[entity]]) + ) + } + }) + } +} + +export class Property extends Identifier { + constructor(value) { + super(value) + } + + from(component) { + return component[this.value] + } } export class Name extends Identifier { @@ -67,11 +102,11 @@ export class ObjectPath extends Identifier { } toString() { - return this.path.reduce((acc, val) => acc + val, this.value) + return `${this.value}.${this.path.join('.')}` } - from(obj) { - return this.path.reduce((obj, key) => obj && obj[key], obj) + from(component) { + return this.path.reduce((acc, v) => v.from(acc), component) } equals(value) { @@ -139,15 +174,62 @@ export class Relationship { } } +const isExactly = (Type, value) => Object.getPrototypeOf(value) === Type.prototype + export class ReturnValues { constructor(...args) { - this.values = args + this.values = args.map(x => isExactly(Identifier, x) ? new ObjectIdentifier(x) : x) + } + + #isTag(type) { + type && Object.getOwnPropertySymbols(type).find( + (s) => s.description === 'tagStore' + ) || false + } + + #forComponentType(value, type) { + if (this.#isTag(type)) { + return () => ({}) + } else if (typeof type === 'function') { + return type + } else { + return value.from(type) + //return Object.fromEntries( + // Object.entries(type).map(([k, v]) => [k, v[entity]]) + //) + } + } + + // TODO: adjust froms to accent entity, return data with built up objects as necessary + from(components) { + /* + * const { entity, query: { components } } = node + * const returns2 = components.map(c => ({ ...c, returnValue: returns.find(x => x.equals(c.name)) })) + * returns2.map(({ returnValue, type }) => getValue(returnValue, type, entity)) + * const values = returns2.filter(({ name }) => returns.includes(name)) + * .map(({ name, type, returnValue }) => ({ [name]: getValue(returnValue, type, entity) })) + * const values = components.filter(({ name }) => returns.includes(name)) + * .map(({ name, type }) => ({ [name]: getValue(type, entity) })) + * return Object.assign(obj, ...values) + */ + + const values = components.map(component => ({ ...component, returnValue: this.findName(component.name) })) + .map(({ name, type, returnValue }) => ([returnValue.toString(), this.#forComponentType(returnValue, type)])) + console.log(this, values) } find(fn) { return this.values.find(fn) } + findName(name) { + return this.find(value => value.equals(name)) + } + + filter(fn) { + return this.values.filter(fn) + } + includes(value) { return this.find(x => x.equals(value)) } diff --git a/src/query.js b/src/query.js index 32243ba..f564d9d 100644 --- a/src/query.js +++ b/src/query.js @@ -101,24 +101,32 @@ const isTag = type => ( ) || false ) -const getValue = (type, entity) => { +const getValue = (returnValue, type, entity) => { if (isTag(type)) { return {} } else if (typeof type === 'function') { return type(entity) } else { - return Object.fromEntries( - Object.entries(type).map(([k, v]) => [k, v[entity]]) - ) + return returnValue.from(type) + //return Object.fromEntries( + // Object.entries(type).map(([k, v]) => [k, v[entity]]) + //) } } const resolveNode = curry((node, obj) => { const returns = path('query.schema.returnValues', node) const { entity, query: { components } } = node - const values = components.filter(({ name }) => returns.includes(name)) - .map(({ name, type }) => ({ [name]: getValue(type, entity) })) - return Object.assign(obj, ...values) + // TODO: do this work in prepareQuery + returns.from(components) + + //const returns2 = components.map(c => ({ ...c, returnValue: returns.find(x => x.equals(c.name)) })) + //returns2.map(({ returnValue, type }) => getValue(returnValue, type, entity)) + //const values = returns2.filter(({ name }) => returns.includes(name)) + // .map(({ name, type, returnValue }) => ({ [name]: getValue(returnValue, type, entity) })) + //const values = components.filter(({ name }) => returns.includes(name)) + // .map(({ name, type }) => ({ [name]: getValue(type, entity) })) + //return Object.assign(obj, ...values) }) const resolveRelationship = edges => { diff --git a/tests/query.test.js b/tests/query.test.js index faa440f..009e44c 100644 --- a/tests/query.test.js +++ b/tests/query.test.js @@ -1,8 +1,7 @@ -import { before, describe, it } from 'node:test' -import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs' +import { before, beforeEach, describe, it } from 'node:test' +import { addComponent, addEntity, createWorld, defineComponent, resetWorld, Types } from 'bitecs' import assert from './assert.js' import { query } from '../src/query.js' -import { identity } from '../src/fn.js' let engine = {} @@ -26,7 +25,6 @@ describe('query', () => { before(() => { const world = { default: createWorld() } const component = { - Entity: identity, Player: defineComponent(null, 10), NPC: defineComponent(null, 10), Health: defineComponent({ current: Types.ui8, max: Types.ui8 }), @@ -39,7 +37,11 @@ describe('query', () => { engine = { world, component } }) - it('should execute basic queries', () => { + beforeEach(() => { + Object.values(engine.world).forEach(world => resetWorld(world)) + }) + + it('should query on single components', () => { const world = engine.world.default const { Player, NPC, Knows } = engine.component @@ -82,9 +84,16 @@ describe('query', () => { Health.max[player] = 50 Health.current[player] = 25 + + // an unspecified component should return an Entity + assert.deepEqual( + query('MATCH (e, h:Health) return e, h.max', engine).unwrap(), + [{ e: 11, h: { max: 50 } }] + ) + assert.deepEqual( query('MATCH (e:Entity, h:Health) return e, h.max', engine).unwrap(), - [{ e: 11, h: { current: 25, max: 50 } }] + [{ e: 11, h: { max: 50 } }] ) }) })