graph-ecs/src/query-parser/types.js
2024-11-27 22:51:24 -06:00

392 lines
7.9 KiB
JavaScript

import { curry, is, last, mergeRightDeep, path } from '../fn.js'
import { Stream } from '../stream.js'
class Value {
value
constructor(value) {
this.value = value
}
toString() {
return this.value.toString()
}
valueOf() {
return this.value?.valueOf() ?? this.value
}
equals(other) {
if (other == null) {
return false
}
const value = this.valueOf()
return value === other
|| value === other.valueOf()
}
}
export class Identifier extends Value {
constructor(value) {
super(value)
}
from(node) {
return node[this.valueOf()]
}
pluck(node) {
const key = this.valueOf()
return { [key]: node[key] }
}
}
export class Property extends Identifier {
constructor(value) {
super(value)
}
pluck(node) {
return node[this.valueOf()]
}
}
export class Name extends Identifier {
constructor(value) {
super(value)
}
}
export class Label extends Identifier {
constructor(value) {
super(value)
}
}
export class SelectedGraph extends Identifier {
constructor(value) {
super(value)
}
}
export class Literal {
constructor(value) {
this.value = value
}
from(node) {
return this.valueOf()
}
valueOf() {
return this.value.valueOf()
}
}
export const None = new Literal(null)
export class Function {
#fn
constructor(name, fn, length = fn.length) {
this.name = name
this.#fn = fn
this.length = length
}
#pop(stack) {
const result = []
let n = Math.min(this.length, stack.length)
while(n > 0) {
result.push(stack.pop())
n--
}
result.reverse()
return result
}
apply(stack) {
const args = this.#pop(stack)
return this.#fn.apply(undefined, args)
}
}
const Associativity = Object.freeze({
Left: 0,
Right: 1
})
export class Operator extends Function {
constructor(name, fn, priority, associativity = Associativity.Left, length = fn.length) {
super(name, fn, length)
this.precedence = priority
this.associativity = associativity
}
compare(other) {
if (other.precedence < this.precedence) {
return 1
} else if (other.precedence === this.precedence) {
if (this.associativity === Associativity.Left) {
return 1
} else {
return 0
}
} else {
return -1
}
}
}
export const Is = new Operator('IS', (x, y) => typeof x === y, 2, Associativity.Right)
export const Not = new Operator('NOT', x => !x, 2, Associativity.Right)
export const And = new Operator('AND', (x, y) => x && y, 11)
export const Or = new Operator('OR', (x, y) => x || y, 12)
export const Xor = new Operator('XOR', (x, y) => x != y, 14)
export const GreaterThan = new Operator('GT', (x, y) => x > y, 6)
export const GreaterThanEqual = new Operator('GTE', (x, y) => x >= y, 6)
export const LesserThan = new Operator('LT', (x, y) => x < y, 6)
export const LesserThanEqual = new Operator('LTE', (x, y) => x <= y, 6)
export const Equal = new Operator('EQ', (x, y) => x === y, 7)
export class Filter {
constructor(...args) {
this.values = this.#shuntingYard(args)
}
#takeWhile(fn, stack) {
const result = []
let val = last(stack)
while (stack.length > 0 && fn(val)) {
result.push(stack.pop())
val = last(stack)
}
return result
}
#takeUntil(fn, stack) {
return this.#takeWhile(x => !fn(x), stack)
}
#shuntingYard(args) {
const stream = new Stream(args)
const output = []
const operators = []
const valueOf = curry((x, y) => y.valueOf() === x)
const leftParen = valueOf('(')
const comma = valueOf(',')
const rightParen = valueOf(')')
while (!stream.done()) {
const next = stream.next()
if (is(Function, next)) {
if (is(Operator, next)) {
const ops = this.#takeWhile(op => !leftParen(op) && next.compare(op) > 0, operators)
output.push.apply(output, ops)
}
operators.push(next)
} else if (comma(next)) {
output.push.apply(output, this.#takeUntil(leftParen, operators))
} else if (leftParen(next)) {
operators.push(next)
} else if (rightParen(next)) {
const ops = this.#takeUntil(leftParen, operators)
output.push.apply(output, ops)
if (!leftParen(operators.pop())) {
throw new SyntaxError('mismatched parenthesis')
}
if (is(Function, last(operators))) {
output.push(operators.pop())
}
} else {
output.push(next)
}
}
const rest = this.#takeUntil(leftParen, operators)
output.push.apply(output, rest)
if (operators.length > 0) {
throw new SyntaxError('mismatched parenthesis')
}
return output
}
passes(nodes) {
return nodes.every(node => {
const values = this.values.map(v => v.from ? v.from(node.values) : v)
const stream = new Stream(values)
const stack = []
while (!stream.done()) {
const next = stream.next()
if (is(Operator, next)) {
const result = next.apply(stack)
stack.push(result)
} else {
stack.push(next)
}
}
return stack[0]
})
}
}
export class Alias extends Identifier {
constructor(value, alias) {
super(value)
this.alias = alias
}
pluck(node) {
return { [this.alias.valueOf()]: this.value.from(node) }
}
}
export class ObjectPath extends Identifier {
constructor(value, ...path) {
super(value)
this.path = path
}
toString() {
return [this.valueOf()].concat(this.path).join('.')
}
from(node) {
return path(this.toString(), node)
}
pluck(node) {
const values = this.path.reduce((acc, v) => ({ [v.valueOf()]: v.pluck(acc) }), node[this.valueOf()])
return { [this.valueOf()]: values }
}
equals(value) {
if (is(ObjectPath, value)) {
return value != null
&& this.path.length === value.path.length
&& super.equals(value)
&& this.path.every((x, i) => x === value.path[i])
} else {
return super.equals(value)
}
}
}
class GraphObject {
constructor(name, label, properties = []) {
this.name = name
this.label = label
this.properties = properties
}
}
export class Component extends GraphObject {
constructor(name, label, properties) {
super(name, label, properties)
}
}
export class Node {
constructor(components) {
this.components = components
}
}
export const Direction = Object.freeze({
Left: 0,
Right: 1
})
export class Edge {
constructor(components) {
this.components = components
}
}
export class DirectedEdge extends Edge {
constructor(components, direction) {
super(components)
this.direction = direction
}
static fromEdge(edge, direction) {
return new DirectedEdge(edge.components, direction)
}
}
export class Relationship {
constructor(left, edge, right) {
const [from, to] = edge.direction === Direction.Right ? [left, right] : [right, left]
this.from = from
this.to = to
this.edge = edge
}
}
export class ReturnValues {
constructor(...args) {
this.values = args
}
from(values) {
// TODO: pass with entries
const returns = Object.keys(values).map(key => this.#pluck(values, key))
return Object.assign({}, ...returns)
}
#pluck(values, name) {
return this.findName(name)
.map(value => value.pluck(values))
.reduce(mergeRightDeep, {})
}
find(fn) {
return this.values.find(fn)
}
filter(fn) {
return this.values.filter(fn)
}
findName(value) {
return this.filter(x => x.equals(value))
}
}
export class KeyValuePair {
constructor(key, value) {
this.key = key
this.value = value
}
}
export class Match {
constructor(value) {
this.value = value
}
valueOf() {
return this.value
}
}
export class Query {
constructor(use, matches, filters, returnValues) {
this.use = use
this.matches = matches
this.filters = filters
this.returnValues = returnValues
}
}