Compare commits
3 commits
6b3539e3c8
...
ceab51b145
Author | SHA1 | Date | |
---|---|---|---|
ceab51b145 | |||
18a0f8656b | |||
7bbc6008f5 |
4 changed files with 141 additions and 116 deletions
109
src/fn.js
109
src/fn.js
|
@ -1,8 +1,11 @@
|
||||||
import { Result } from './result.js'
|
import { Result } from './result.js'
|
||||||
|
|
||||||
export function curry(func) {
|
export const identity = x => x
|
||||||
|
|
||||||
|
// functions
|
||||||
|
export function curryN(arity, func) {
|
||||||
return function curried(...args) {
|
return function curried(...args) {
|
||||||
if (args.length >= func.length) {
|
if (args.length >= arity) {
|
||||||
return func.apply(this, args)
|
return func.apply(this, args)
|
||||||
} else {
|
} else {
|
||||||
return function(...args2) {
|
return function(...args2) {
|
||||||
|
@ -12,41 +15,97 @@ export function curry(func) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const curry = fn => curryN(fn.length, fn)
|
||||||
export const flip = fn => curry((a, b) => fn(b, a))
|
export const flip = fn => curry((a, b) => fn(b, a))
|
||||||
|
export const tap = curry((fn, val) => {
|
||||||
|
fn(val)
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
|
||||||
// predicates
|
export const always = x => () => x
|
||||||
export const and = (...booleans) => booleans.every(identity)
|
export const pipe = (...f) => v => reduce(applyTo, v, f)
|
||||||
export const or = (...booleans) => booleans.some(identity)
|
export const apply = fn => args => fn.apply(null, args)
|
||||||
export const isOk = r => !is(Result, r) || r.isOk()
|
export const applyTo = curry((v, fn) => fn(v))
|
||||||
|
export const useWith = curry((fn, tfns) =>
|
||||||
|
curryN(
|
||||||
|
tfns.length,
|
||||||
|
(...args) => pipe(enumerate, map(([i, a]) => tfns[i](a)), fn)(args)
|
||||||
|
))
|
||||||
|
|
||||||
export const construct = Type => args => new Type(...args)
|
export const converge = (fn, [head, ...rest]) => curryN(
|
||||||
|
head.length,
|
||||||
|
(...args) => pipe(...rest, fn)(head(...args))
|
||||||
|
)
|
||||||
|
|
||||||
export const of = value => Array.isArray(value) ? value : [value]
|
export const diverge = (fn, tfns) => pipe(applyTo, flip(map)(tfns), apply(fn))
|
||||||
export const concat = curry((a, b) => b.concat(a))
|
|
||||||
export const head = value => value[0]
|
|
||||||
export const tail = value => value.slice(1)
|
|
||||||
export const prop = curry((p, v) => v[p])
|
|
||||||
export const apply = curry((v, f) => f(v))
|
|
||||||
export const reduce = curry((fn, init, v) => v.reduce(fn, init))
|
|
||||||
export const map = curry((fn, v) => v.map(fn))
|
|
||||||
export const filter = curry((fn, v) => v.filter(fn))
|
|
||||||
export const find = curry((fn, v) => v.find(fn))
|
|
||||||
export const join = curry((sep, v) => v.join(sep))
|
|
||||||
export const rev = v => v.slice().reverse()
|
|
||||||
export const pipe = (...f) => v => f.reduce(apply, v)
|
|
||||||
export const prepend = curry((value, iter) => [value, ...iter])
|
|
||||||
export const append = curry((value, iter) => [...iter, value])
|
|
||||||
|
|
||||||
export const identity = x => x
|
// types
|
||||||
|
export const isArray = Array.isArray
|
||||||
|
export const isNil = v => !v
|
||||||
|
export const is = curry((type, value) => value instanceof type)
|
||||||
|
|
||||||
|
// branching
|
||||||
export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v))
|
export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v))
|
||||||
export const when = curry((p, f, v) => ifElse(p, f, identity, v))
|
export const when = curry((p, f, v) => ifElse(p, f, identity, v))
|
||||||
export const unless = curry((p, f, v) => ifElse(p, identity, f, v))
|
export const unless = curry((p, f, v) => ifElse(p, identity, f, v))
|
||||||
|
|
||||||
export const isNil = v => !v
|
// predicates
|
||||||
|
export const and = (...booleans) => booleans.every(identity)
|
||||||
|
export const or = (...booleans) => booleans.some(identity)
|
||||||
|
export const isOk = value => !is(Result, value) || value.isOk()
|
||||||
|
|
||||||
|
// classes
|
||||||
|
export const construct = Type => args => new Type(...args)
|
||||||
|
|
||||||
|
// strings
|
||||||
|
export const split = curry((delim, v) => v.split(delim))
|
||||||
|
|
||||||
|
// arrays
|
||||||
|
export const length = v => v.length
|
||||||
|
export const len = length
|
||||||
|
export const of = unless(isArray, value => ([value]))
|
||||||
|
export const concat = curry((a, b) => b.concat(a))
|
||||||
export const take = curry((n, value) => value.slice(0, n))
|
export const take = curry((n, value) => value.slice(0, n))
|
||||||
export const drop = curry((n, value) => value.slice(n))
|
export const drop = curry((n, value) => value.slice(n))
|
||||||
|
export const nth = curry((index, value) => value[index])
|
||||||
|
export const head = nth(0)
|
||||||
|
export const tail = drop(1)
|
||||||
|
export const map = curry((fn, v) => v.map(fn))
|
||||||
|
export const filter = curry((fn, v) => v.filter(fn))
|
||||||
|
export const reduce = curry((fn, init, v) => v.reduce(fn, init))
|
||||||
|
export const find = curry((fn, v) => v.find(fn))
|
||||||
|
export const join = curry((sep, v) => v.join(sep))
|
||||||
|
export const rev = v => v.slice().reverse()
|
||||||
|
export const prepend = curry((value, iter) => [value, ...iter])
|
||||||
|
export const append = curry((value, iter) => [...iter, value])
|
||||||
|
export const enumerate = value => value.map((v, i) => ([i, v]))
|
||||||
|
export const flatten = curry((n, v) => v.flat(n))
|
||||||
|
export const update = curry((index, value, arr) => arr.toSpliced(index, 1, value))
|
||||||
|
|
||||||
export const is = curry((type, value) => value instanceof type)
|
// objects
|
||||||
|
const makePath = unless(isArray, split('.'))
|
||||||
|
export const assoc = curry((key, value, obj) => ({ ...obj, [key]: value }))
|
||||||
|
export const assocPath = curry((key, value, obj) => pipe(
|
||||||
|
makePath,
|
||||||
|
rev,
|
||||||
|
reduce((acc, val) => fromEntries([[val, acc]]), value),
|
||||||
|
mergeLeft(obj)
|
||||||
|
)(key))
|
||||||
|
|
||||||
|
export const prop = curry((key, obj) => obj && key && obj[key])
|
||||||
|
export const path = curry((key, obj) => reduce(flip(prop), obj, makePath(key)))
|
||||||
|
export const mergeLeft = curry((a, b) => ({ ...b, ...a }))
|
||||||
|
export const mergeRight = flip(mergeLeft)
|
||||||
|
export const keys = Object.keys
|
||||||
|
export const values = Object.values
|
||||||
|
export const entries = Object.entries
|
||||||
|
export const fromEntries = Object.fromEntries
|
||||||
|
|
||||||
|
// lenses
|
||||||
|
export const lens = curry((get, set) => ({ get, set }))
|
||||||
|
export const lensPath = path => lens(path(path), assocPath(path))
|
||||||
|
export const lensIndex = index => lens(nth(index), update(index))
|
||||||
|
export const view = curry(({ get }, obj) => get(obj))
|
||||||
|
export const set = curry((l, value, obj) => over(l, always(value), obj))
|
||||||
|
export const over = curry(({ get, set }, fn, obj) => set(fn(get(obj)), obj))
|
||||||
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { defineQuery, hasComponent } from 'bitecs'
|
|
||||||
/// danm this rules !!!
|
|
||||||
const re = /\((?<fromName>\w+)?:(?<fromLabel>\w+)\)(-\[(?<edgeName>\w+)?:(?<edgeLabel>\w+)\]->\((?<toName>\w+)?:(?<toLabel>\w+)\))*/
|
|
||||||
|
|
||||||
const node = (name, label) => ({ name, label })
|
|
||||||
|
|
||||||
const parse = s => {
|
|
||||||
const result = s.match(re)
|
|
||||||
const groups = result.groups
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: node(groups.fromName || 'a', groups.fromLabel),
|
|
||||||
to: node(groups.toName || 'b', groups.toLabel),
|
|
||||||
edge: node(groups.edgeName, groups.edgeLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const query = (world, s) => {
|
|
||||||
const meta = parse(s)
|
|
||||||
const from = world.components[meta.from.label]
|
|
||||||
const to = world.components[meta.to.label]
|
|
||||||
const edge = world.components[meta.edge.label]
|
|
||||||
|
|
||||||
const fromQuery = defineQuery([from])
|
|
||||||
const toQuery = defineQuery([to])
|
|
||||||
const edgeQuery = defineQuery([edge])
|
|
||||||
|
|
||||||
return (world) => {
|
|
||||||
const result = []
|
|
||||||
|
|
||||||
const fq = fromQuery(world)
|
|
||||||
const tq = toQuery(world)
|
|
||||||
const eq = edgeQuery(world)
|
|
||||||
|
|
||||||
for(const feid of fq) {
|
|
||||||
const targets = []
|
|
||||||
|
|
||||||
for(const eeid of eq) {
|
|
||||||
if(edge.from[eeid] === feid && hasComponent(world, to, edge.to[eeid])) {
|
|
||||||
targets.push(edge.to[eeid])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push({ [feid]: targets })
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
95
src/query.js
95
src/query.js
|
@ -1,7 +1,7 @@
|
||||||
import { defineQuery, hasComponent } from 'bitecs'
|
import { defineQuery, hasComponent } from 'bitecs'
|
||||||
import { query as q } from './query-parser/index.js'
|
import { query as q } from './query-parser/index.js'
|
||||||
import { parseAll } from './parser.js'
|
import { parseAll } from './parser.js'
|
||||||
import { curry, of, is, reduce, flip, concat, map } from './fn.js'
|
import { curry, of, is, reduce, flip, concat, map, when, assoc, mergeLeft, isNil, path, prop, ifElse, diverge, pipe, curryN, always, tap } from './fn.js'
|
||||||
import { Relationship } from './query-parser/types.js'
|
import { Relationship } from './query-parser/types.js'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -16,7 +16,7 @@ const prepareQuery = (query, component) => {
|
||||||
|
|
||||||
if (relationship) {
|
if (relationship) {
|
||||||
const { from: f, to: t, edge: e } = match
|
const { from: f, to: t, edge: e } = match
|
||||||
const [from, to, edge] = [f, t, e].map(q => prepareQuery({ match: q, returnValues }, component))
|
const [from, to, edge] = map(q => prepareQuery({ match: q, returnValues }, component), [f, t, e])
|
||||||
|
|
||||||
return { from, to, edge, relationship, schema: query }
|
return { from, to, edge, relationship, schema: query }
|
||||||
} else {
|
} else {
|
||||||
|
@ -60,10 +60,11 @@ const queryRelationship = (from, edge, to, world) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cat = flip(concat)
|
const cat = flip(concat)
|
||||||
|
const assembleQuery = (query, entities) => map(entity => of({ entity, query }), entities)
|
||||||
|
|
||||||
const executeQuery = (query, world) => {
|
const executeQuery = curry((query, world) => {
|
||||||
if (!query.relationship) {
|
if (!query.relationship) {
|
||||||
return map(id => ({ [id]: query }), query.query(world))
|
return assembleQuery(query, query.query(world))
|
||||||
}
|
}
|
||||||
|
|
||||||
const { from, to: _to, edge } = query
|
const { from, to: _to, edge } = query
|
||||||
|
@ -84,53 +85,67 @@ const executeQuery = (query, world) => {
|
||||||
..._to,
|
..._to,
|
||||||
from: {
|
from: {
|
||||||
...to,
|
...to,
|
||||||
query: () => of(Edge.to[eid])
|
query: always(of(Edge.to[eid]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = map(cat([eid]), executeQuery(next, world))
|
return pipe(
|
||||||
const expanded = map(
|
executeQuery(next),
|
||||||
([l, r]) => ({ [l]: query, ...r }),
|
map(cat([eid])),
|
||||||
results
|
map(([l, r]) => ([{ entity: l, query }, r])),
|
||||||
)
|
x => ([...acc, ...x]),
|
||||||
return [...acc, ...expanded]
|
)(world)
|
||||||
}, [], edges)
|
}, [], edges)
|
||||||
} else {
|
} else {
|
||||||
return map(id => of({ [id]: query }), edges)
|
return assembleQuery(query, edges)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const returnValues = pipe(path('query.schema.returnValues.values'), map(prop('value')))
|
||||||
|
const includes = curry((val, arr) => !isNil(val) && arr.includes(val))
|
||||||
|
|
||||||
|
const maybeAssoc = curry((pred, key, value, obj) =>
|
||||||
|
when(pred, assoc(key, value), obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolveNode = curry((returns, name, entity, obj) =>
|
||||||
|
maybeAssoc(always(includes(name, returns)), name, entity, obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolveRelationship = edges => {
|
||||||
|
const { entity, query } = edges
|
||||||
|
const Edge = query.edge.type
|
||||||
|
const returns = returnValues(edges)
|
||||||
|
|
||||||
|
const queryProp = flip(prop)(query)
|
||||||
|
const [from, edge, to] = map(queryProp, ['from', 'edge', 'to'])
|
||||||
|
const [fn, en] = [from.name, edge.name]
|
||||||
|
const tn = ifElse(prop('relationship'),
|
||||||
|
path('from.name'),
|
||||||
|
prop('name'),
|
||||||
|
to
|
||||||
|
)
|
||||||
|
|
||||||
|
return pipe(
|
||||||
|
resolveNode(returns, fn, Edge.from[entity]),
|
||||||
|
resolveNode(returns, en, entity),
|
||||||
|
resolveNode(returns, tn, Edge.to[entity]),
|
||||||
|
)({})
|
||||||
}
|
}
|
||||||
|
|
||||||
const val = v => v?.value?.value
|
const resolveReturns = map(
|
||||||
const include = returns => name => name != null && returns.includes(name)
|
pipe(
|
||||||
|
map(ifElse(
|
||||||
const resolveReturns = results => {
|
path('query.relationship'),
|
||||||
return map(result => {
|
resolveRelationship,
|
||||||
return reduce((acc, [id, query]) => {
|
diverge(resolveNode, [returnValues, path('query.schema.match.name.value.value'), prop('entity'), always({})])
|
||||||
const q = query.schema.match
|
)),
|
||||||
const inc = include(query.schema.returnValues.values.map(x => x.value))
|
reduce(mergeLeft, {})
|
||||||
if (is(Relationship, q)) {
|
)
|
||||||
const Edge = query.edge.type
|
)
|
||||||
const fn = val(q.from.name)
|
|
||||||
const en = val(q.edge.name)
|
|
||||||
const tn = val(q.to.name)
|
|
||||||
return {
|
|
||||||
...acc, ...{
|
|
||||||
...(inc(fn) && { [fn]: Edge.from[id] }),
|
|
||||||
...(inc(en) && { [en]: parseInt(id, 10) }),
|
|
||||||
...(inc(tn) && { [tn]: Edge.to[id] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const name = val(q.name)
|
|
||||||
return { ...acc, ...(inc(name) && { [name]: id }) }
|
|
||||||
}
|
|
||||||
}, {}, Object.entries(result))
|
|
||||||
}, results)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const query = curry((input, engine) => {
|
export const query = curry((input, engine) => {
|
||||||
return parseAll(q, input).map(({ use, ...rest }) => {
|
return parseAll(q, input).map(({ use, ...rest }) => {
|
||||||
|
|
||||||
const worldName = use && use.value || 'default'
|
const worldName = use && use.value || 'default'
|
||||||
const world = engine.world[worldName]
|
const world = engine.world[worldName]
|
||||||
const prepared = prepareQuery(rest, engine.component)
|
const prepared = prepareQuery(rest, engine.component)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { before, describe, it } from 'node:test'
|
import { beforeEach, describe, it } from 'node:test'
|
||||||
import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
|
import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
|
||||||
import assert from './assert.js'
|
import assert from './assert.js'
|
||||||
import { query } from '../src/query.js'
|
import { query } from '../src/query.js'
|
||||||
|
|
||||||
let engine = {}
|
let engine = {}
|
||||||
describe('query', () => {
|
describe('query', () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
const world = { default: createWorld() }
|
const world = { default: createWorld() }
|
||||||
const component = {
|
const component = {
|
||||||
Player: defineComponent(null, 10),
|
Player: defineComponent(null, 10),
|
||||||
|
|
Loading…
Reference in a new issue