refactor tests

This commit is contained in:
Rowan 2024-11-13 13:24:29 -06:00
parent f30717a9fa
commit 3aaa48aa59
14 changed files with 242 additions and 113 deletions

View file

@ -25,6 +25,7 @@ export const apply = curry((v, f) => f(v))
export const reduce = curry((fn, init, v) => v.reduce(fn, init)) export const reduce = curry((fn, init, v) => v.reduce(fn, init))
export const map = curry((fn, v) => v.map(fn)) export const map = curry((fn, v) => v.map(fn))
export const filter = curry((fn, v) => v.filter(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 join = curry((sep, v) => v.join(sep))
export const pipe = (...f) => v => f.reduce(apply, v) export const pipe = (...f) => v => f.reduce(apply, v)

View file

@ -77,7 +77,7 @@ export const seq = (...parsers) => state => {
let acc = Result.Ok(state) let acc = Result.Ok(state)
for (const parser of parsers) { for (const parser of parsers) {
if (acc.isOk()) { if (acc.isOk()) {
acc = parser(acc.value) acc = parser(acc.unwrap())
} else { } else {
break break
} }
@ -100,11 +100,11 @@ export const until = curry((parser, state) => {
let acc = Result.Ok(state) let acc = Result.Ok(state)
while (acc.isOk()) { while (acc.isOk()) {
const result = parser(clone(acc.value)) const result = parser(clone(acc.unwrap()))
if (result.isOk()) { if (result.isOk()) {
break break
} else { } else {
acc = anyChar(acc.value) acc = anyChar(acc.unwrap())
} }
} }
@ -115,7 +115,7 @@ export const many = curry((parser, state) => {
let result = Result.Ok(state) let result = Result.Ok(state)
while (true) { while (true) {
const res = parser(clone(result.value)) const res = parser(clone(result.unwrap()))
if (res.isOk()) { if (res.isOk()) {
result = res result = res
} else { } else {
@ -131,7 +131,7 @@ export const skip = curry((parser, state) => {
const result = parser(state) const result = parser(state)
if (result.isOk()) { if (result.isOk()) {
return Result.Ok([parsed, result.value[1]]) return Result.Ok([parsed, result.unwrap()[1]])
} else { } else {
return result return result
} }
@ -142,11 +142,11 @@ export const map = curry((fn, parser, state) => {
if (result.isOk()) { if (result.isOk()) {
const [parsed] = state const [parsed] = state
const [, stream] = result.value const [, stream] = result.unwrap()
const backtrack = [parsed, stream] const backtrack = [parsed, stream]
try { try {
const parsedValue = diff(state, result.value) const parsedValue = diff(state, result.unwrap())
return succeed(fn(parsedValue), backtrack) return succeed(fn(parsedValue), backtrack)
} catch (e) { } catch (e) {
return fail('map failed', state, e) return fail('map failed', state, e)
@ -201,7 +201,7 @@ export const noCaseString = curry((str, state) => (
)) ))
export const maybe = curry((parser, state) => { export const maybe = curry((parser, state) => {
const result = parser(state) const result = parser(clone(state))
return result.isOk() ? result : succeed([], state) return result.isOk() ? result : succeed([], state)
}) })

View file

@ -1,6 +1,6 @@
import { join } from '../fn.js' 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 { alpha, alphanumeric, any, char, digit, list, many, map, maybe, noCaseString, separated, seq, skip, string, until } from '../parser.js'
import { Alias, Literal, ObjectPath } from './types.js' import { Alias, Identifier, Literal, ObjectPath } from './types.js'
export const Symbol = Object.freeze({ export const Symbol = Object.freeze({
Bracket: Object.freeze({ Bracket: Object.freeze({
@ -37,7 +37,7 @@ export const quoted = collect(seq(skip(Symbol.Quote), until(Symbol.Quote), skip(
export const number = seq(digit, many(digit)) export const number = seq(digit, many(digit))
export const signed = seq(maybe(any(Symbol.Plus, Symbol.Hyphen)), number) export const signed = seq(maybe(any(Symbol.Plus, Symbol.Hyphen)), number)
export const ws = skip(many(Symbol.Space)) export const ws = skip(many(Symbol.Space))
export const identifier = collect(seq(alpha, many(alphanumeric))) export const identifier = map(([x]) => new Identifier(x), collect(seq(alpha, many(alphanumeric))))
const float = map(([n]) => parseFloat(n, 10), collect(seq(signed, Symbol.Period, number))) const float = map(([n]) => parseFloat(n, 10), collect(seq(signed, Symbol.Period, number)))
const integer = map(([n]) => parseInt(n, 10), collect(signed)) const integer = map(([n]) => parseInt(n, 10), collect(signed))

View file

@ -1,43 +1,48 @@
import { collect, Symbol, ws } from './common.js' import { identifier, literal, Symbol, ws } from './common.js'
import { Node, Edge, KeyValuePair, Label, Name } from './types.js' import { Node, Edge, KeyValuePair, Label, Name } from './types.js'
import { curry, join } from '../fn.js' import { curry, is } from '../fn.js'
import { alpha, alphanumeric, many, maybe, map, parse, seq, skip, between, noCaseString, separated, until, list } from '../parser.js' import { many, maybe, map, seq, skip, between, noCaseString, separated, list } from '../parser.js'
const { Bracket, Colon, Comma, Hyphen, Quote } = Symbol const { Bracket, Colon, Comma, Hyphen } = Symbol
const name = map( const name = map(
([x]) => new Name(x), ([x]) => new Name(x),
collect(seq(alpha, many(alphanumeric))) identifier
) )
const label = map(([x]) => new Label(x), seq(skip(Colon), name))
const trim = curry((parser, state) => ( const trim = curry((parser, state) => (
between(ws, parser, ws, state) between(ws, parser, ws, state)
)) ))
const label = map(([x]) => new Label(x), seq(trim(skip(Colon)), identifier))
const bracketed = curry((value, { Left, Right }, state) => ( const bracketed = curry((value, { Left, Right }, state) => (
between(trim(Left), value, trim(Right), state) between(trim(Left), value, trim(Right), state)
)) ))
const quoted = map(join(''), seq(skip(Quote), until(Quote), skip(Quote)))
const kvp = map( const kvp = map(
([k, v]) => new KeyValuePair(k, v), ([k, v]) => new KeyValuePair(k, v),
separated(name, trim(Colon), quoted) separated(name, trim(Colon), literal)
) )
const kvps = list(trim(Comma), kvp) export const kvps = list(trim(Comma), kvp)
export const properties = bracketed(kvps, Bracket.Curly) export const properties = bracketed(kvps, Bracket.Curly)
const id = seq(maybe(name), label) const id = seq(maybe(name), label)
const makeObj = Ctr => params => {
const [name, label] = is(Name, params[0]) ? [params[0], params[1]] : [undefined, params[0]]
const properties = !name ? params.slice(1) : params.slice(2)
return new Ctr(name, label, properties)
}
export const node = map( export const node = map(
([name, label, ...properties]) => new Node(name, label, properties), makeObj(Node),
bracketed(seq(id, ws, maybe(properties)), Bracket.Round) bracketed(seq(id, maybe(properties)), Bracket.Round)
) )
export const edge = map( export const edge = map(
([id]) => new Edge(id), makeObj(Edge),
bracketed(id, Bracket.Square) bracketed(seq(id, maybe(properties)), Bracket.Square)
) )
const rightArrow = seq(Hyphen, Bracket.Angle.Right) const rightArrow = seq(Hyphen, Bracket.Angle.Right)
@ -45,5 +50,6 @@ const relationship = seq(skip(Hyphen), edge, skip(rightArrow), node)
const keyword = noCaseString('match') const keyword = noCaseString('match')
const params = seq(node, many(relationship)) const params = seq(node, many(relationship))
export const statement = seq(skip(keyword), ws, params) export const statement = seq(skip(keyword), ws, params)

View file

@ -4,12 +4,10 @@ import { value, Symbol, ws } from './common.js'
const keyword = noCaseString('return') const keyword = noCaseString('return')
//const params = map( const params = map(
// values => new ReturnValues(values), x => new ReturnValues(...x),
// seq(list(seq(Symbol.Comma, ws), alias)) seq(list(seq(ws, Symbol.Comma, ws), value))
//) )
const params = seq(list(seq(ws, Symbol.Comma, ws), value))
export const statement = seq(skip(keyword), ws, params) export const statement = seq(skip(keyword), ws, params)

View file

@ -17,7 +17,7 @@ export class SelectedGraph {
} }
class GraphObject { class GraphObject {
constructor({ name, label }, properties = []) { constructor(name, label, properties = []) {
this.name = name this.name = name
this.label = label this.label = label
this.properties = properties this.properties = properties
@ -25,20 +25,20 @@ class GraphObject {
} }
export class Node extends GraphObject { export class Node extends GraphObject {
constructor(id, properties) { constructor(name, label, properties) {
super(id, properties) super(name, label, properties)
} }
} }
export class Edge extends GraphObject { export class Edge extends GraphObject {
constructor(id, properties) { constructor(name, label, properties) {
super(id, properties) super(name, label, properties)
} }
} }
export class ReturnValues extends Array { export class ReturnValues extends Array {
constructor(args) { constructor(...args) {
super(args) super(...args)
} }
} }

View file

@ -1,7 +1,10 @@
export class Result { export class Result {
#value
#error
constructor(value, error) { constructor(value, error) {
this.value = value this.#value = value
this.error = error this.#error = error
} }
static Ok(value) { static Ok(value) {
@ -13,7 +16,7 @@ export class Result {
} }
isOk() { isOk() {
return this.error === null return this.#error === null
} }
isErr() { isErr() {
@ -22,7 +25,7 @@ export class Result {
map(fn) { map(fn) {
if (this.isOk()) { if (this.isOk()) {
return Result.Ok(fn(this.value)) return Result.Ok(fn(this.#value))
} else { } else {
return this return this
} }
@ -30,11 +33,29 @@ export class Result {
flatMap(fn) { flatMap(fn) {
if (this.isOk()) { if (this.isOk()) {
return fn(this.value) return fn(this.#value)
} else { } else {
return this return this
} }
} }
unwrap() {
if (this.isOk()) {
return this.#value
} else {
throw new ResultError(`failed to unwrap result: ${this.#error.message}`, this)
}
}
unwrapOr(value) {
return this.isOk() ? this.#value : value
}
} }
export class ResultError extends TypeError {
constructor(msg, result) {
super(msg)
this.result = result
}
}

31
tests/assert.js Normal file
View file

@ -0,0 +1,31 @@
import assert from 'node:assert'
import { curry } from '../src/fn.js'
import { parse } from 'node:path'
const isOk = curry((fn, result) => {
assert.doesNotThrow(() => {
const [[actual]] = result.unwrap()
fn(actual)
})
})
const isErr = (result, error, msg) => {
if (error == null) {
assert(result.isErr())
} else {
assert.throws(result.unwrap(), error, msg)
}
}
export default {
...assert,
parseOk: curry((parser, input, fn) => {
isOk(fn, parse(parser, input))
}),
parseErr: (parser, input, error, message) => {
isErr(parse(parser, input), error, message)
},
isOk,
isErr
}

View file

@ -2,8 +2,9 @@ import { describe, it } from 'node:test'
import assert from 'node:assert' import assert from 'node:assert'
import { parse } from '../../src/parser.js' import { parse } from '../../src/parser.js'
import * as Common from '../../src/query/common.js' import * as Common from '../../src/query/common.js'
import { Alias, ObjectPath } from '../../src/query/types.js' import { Alias, Identifier, Literal, ObjectPath } from '../../src/query/types.js'
const v = r => r.unwrap()[0][0]
describe('common parser library', () => { describe('common parser library', () => {
it('literals should match literal types', () => { it('literals should match literal types', () => {
const tBool = parse(Common.literal, 'true') const tBool = parse(Common.literal, 'true')
@ -16,16 +17,15 @@ describe('common parser library', () => {
const negFloat = parse(Common.literal, '-12.01') const negFloat = parse(Common.literal, '-12.01')
const string = parse(Common.literal, '"akjsdfuaio"') const string = parse(Common.literal, '"akjsdfuaio"')
const v = r => r.value[0][0].value assert.deepEqual(v(tBool), new Literal(true))
assert.strictEqual(v(tBool), true) assert.deepEqual(v(fBool), new Literal(false))
assert.strictEqual(v(fBool), false) assert.deepEqual(v(uint), new Literal(5))
assert.strictEqual(v(uint), 5) assert.deepEqual(v(posInt), new Literal(16))
assert.strictEqual(v(posInt), 16) assert.deepEqual(v(negInt), new Literal(-710))
assert.strictEqual(v(negInt), -710) assert.deepEqual(v(ufloat), new Literal(2.1))
assert.strictEqual(v(ufloat), 2.1) assert.deepEqual(v(posFloat), new Literal(0.1))
assert.strictEqual(v(posFloat), 0.1) assert.deepEqual(v(negFloat), new Literal(-12.01))
assert.strictEqual(v(negFloat), -12.01) assert.deepEqual(v(string), new Literal('akjsdfuaio'))
assert.strictEqual(v(string), 'akjsdfuaio')
}) })
it('value should match literals', () => { it('value should match literals', () => {
@ -34,29 +34,26 @@ describe('common parser library', () => {
const float = parse(Common.baseValue, '0.15') const float = parse(Common.baseValue, '0.15')
const str = parse(Common.baseValue, '"abc"') const str = parse(Common.baseValue, '"abc"')
const v = r => r.value[0][0].value assert.deepEqual(v(bool), new Literal(false))
assert.strictEqual(v(bool), false) assert.deepEqual(v(uint), new Literal(11))
assert.strictEqual(v(uint), 11) assert.deepEqual(v(float), new Literal(0.15))
assert.strictEqual(v(float), 0.15) assert.deepEqual(v(str), new Literal('abc'))
assert.strictEqual(v(str), 'abc')
}) })
it('value should match variables', () => { it('value should match variables', () => {
const identifier = parse(Common.baseValue, 'test') const identifier = parse(Common.baseValue, 'test')
const accessor = parse(Common.baseValue, 'test.value') const accessor = parse(Common.baseValue, 'test.value')
const v = r => r.value[0][0] assert.deepEqual(v(identifier), new Identifier('test'))
assert.strictEqual(v(identifier), 'test') assert.deepEqual(v(accessor), new ObjectPath(new Identifier('test'), new Identifier('value')))
assert.deepEqual(v(accessor), new ObjectPath('test', 'value'))
}) })
it('aliases should work i hope', () => { it('aliases should work i hope', () => {
const noAlias = parse(Common.value, 'test') const noAlias = parse(Common.value, 'test')
const aliased1 = parse(Common.value, 'crybaby AS cb') const aliased1 = parse(Common.value, 'crybaby AS cb')
const aliased2 = parse(Common.value, 'property.name AS name') const aliased2 = parse(Common.value, 'property.name AS name')
const v = r => r.value[0][0] assert.deepEqual(v(noAlias), new Identifier('test'))
assert.strictEqual(v(noAlias), 'test') assert.deepEqual(v(aliased1), new Alias(new Identifier('crybaby'), new Identifier('cb')))
assert.deepEqual(v(aliased1), new Alias('crybaby', 'cb')) assert.deepEqual(v(aliased2), new Alias(new ObjectPath(new Identifier('property'), new Identifier('name')), new Identifier('name')))
assert.deepEqual(v(aliased2), new Alias(new ObjectPath('property', 'name'), 'name'))
}) })
}) })

View file

@ -1,21 +1,89 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from 'node:assert' import assert from '../assert.js'
import { parse } from '../../src/parser.js'
import { node, edge, properties, statement } from '../../src/query/match.js' import { node, edge, properties, statement } from '../../src/query/match.js'
import { makeEdge, makeNode } from '../utils.js'
//describe('match parser', () => { describe('node', () => {
// it('node should match a label', () => { it('should match a label with an empty name', () => {
// const result = parse(node, '(:Node)') assert.parseOk(node, '(:Node)', actual => {
// console.log(result.error.state) const expected = makeNode(undefined, 'Node')
// assert(result.isOk()) assert.deepEqual(actual, expected)
// }) })
// })
// it('properties should do something', () => {
// const q = '(a:Node { name: "Rowan", species: "???" })' it('should match a name and label', () => {
// console.log( assert.parseOk(node, '(node:Node)', actual => {
// `parsing "${q}"\n`, const expected = makeNode('node', 'Node')
// parse(node, q).value[0][0] assert.deepEqual(actual, expected)
// ) })
// }) })
//})
it('should match a node with properties and no name', () => {
assert.parseOk(node, '(:Label { name: "Rowan", gay: true })', actual => {
const expected = makeNode(undefined, 'Label', [['name', 'Rowan'], ['gay', true]])
assert.deepEqual(actual, expected)
})
})
it('should match a node with properties and a name', () => {
assert.parseOk(node, '(name:Label { name: "Rowan", gay: true })', actual => {
const expected = makeNode('name', 'Label', [['name', 'Rowan'], ['gay', true]])
assert.deepEqual(actual, expected)
})
})
it('should handle whitespace', () => {
assert.parseOk(node, '( name : Label { name : "Rowan" , gay : true } )', actual => {
const expected = makeNode('name', 'Label', [['name', 'Rowan'], ['gay', true]])
assert.deepEqual(actual, expected)
})
})
})
describe('edge', () => {
it('should match a relationship with a name and label', () => {
assert.parseOk(edge, '[name:Label]', actual => {
const expected = makeEdge('name', 'Label', [])
assert.deepEqual(actual, expected)
})
})
it('should match a relationship with no name', () => {
assert.parseOk(edge, '[:Label]', actual => {
const expected = makeEdge(undefined, 'Label', [])
assert.deepEqual(actual, expected)
})
})
it('should match a relationship with a name and properties', () => {
assert.parseOk(edge, '[gay:Queer { binary: "-∞" }]', actual => {
const expected = makeEdge('gay', 'Queer', [['binary', '-∞']])
assert.deepEqual(actual, expected)
})
})
it('should match a relationship with properties and no name', () => {
assert.parseOk(edge, '[:Queer { binary: "-∞" }]', actual => {
const expected = makeEdge(undefined, 'Queer', [['binary', '-∞']])
assert.deepEqual(actual, expected)
})
})
it('should handle whitespace', () => {
assert.parseOk(edge, '[ : Queer { binary : "-∞" } ]', actual => {
const expected = makeEdge(undefined, 'Queer', [['binary', '-∞']])
assert.deepEqual(actual, expected)
})
})
})
describe('match', () => {
it('should match nodes', () => {
assert.parseOk(statement, '(node:Label)', actual => {
const expected = makeNode('node', 'Label')
assert.deepEqual(actual, expected)
})
})
})

View file

@ -1,23 +1,28 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from 'node:assert' import assert from '../assert.js'
import { parse } from '../../src/parser.js'
import { statement } from '../../src/query/return.js' import { statement } from '../../src/query/return.js'
import { Alias } from '../../src/query/types.js' import { Alias, Identifier, Literal } from '../../src/query/types.js'
describe('return parser', () => { describe('return parser', () => {
it('should collect a single value for a query to return', () => { it('should collect a single value for a query to return', () => {
const result = parse(statement, 'RETURN folklore AS f') assert.parseOk(statement, 'RETURN folklore AS f', actual => {
assert(result.isOk()) assert.deepEqual(actual, new Alias(new Identifier('folklore'), new Identifier('f')))
const [[selected]] = result.value })
assert.deepStrictEqual(selected, new Alias('folklore', 'f'))
}) })
//it('should collect multiple values for a query to return', () => { it('should collect multiple values for a query to return', () => {
// const result = parse(statement, 'RETURN sybil, mercury, rowan') assert.parseOk(statement, 'RETURN sybil, mercury, rowan', actual => {
// console.log(result.error.state[0]) assert.deepStrictEqual(actual, ['sybil', 'mercury', 'rowan'].map(x => new Identifier(x)))
// assert(result.isOk()) })
// const [[selected]] = result.value })
// assert.deepStrictEqual(selected, ['sybil', 'mercury', 'rowan'])
//}) it('should accept mixed values and aliases', () => {
assert.parseOk(statement, 'RETURN 19589 AS sybil, mercury AS vex, rowan', actual => {
const sybil = new Alias(new Literal(19589), new Identifier('sybil'))
const mercury = new Alias(new Identifier('mercury'), new Identifier('vex'))
const rowan = new Identifier('rowan')
assert.deepStrictEqual(actual, [sybil, mercury, rowan])
})
})
}) })

View file

@ -1,19 +1,18 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from 'node:assert' import assert from '../assert.js'
import { parse } from '../../src/parser.js' import { parse } from '../../src/parser.js'
import { statement } from '../../src/query/use.js' import { statement } from '../../src/query/use.js'
import { Identifier } from '../../src/query/types.js'
describe('use parser', () => { describe('use parser', () => {
it('should select a graph to query', () => { it('should select a graph to query', () => {
const result = parse(statement, 'USE default') assert.parseOk(statement, 'USE default', actual => {
assert(result.isOk()) assert.deepEqual(actual.identifier, new Identifier('default'))
const [[selected]] = result.value })
assert.strictEqual(selected.identifier, 'default')
}) })
it('should return an error if no graph identifier is provided', () => { it('should return an error if no graph identifier is provided', () => {
const result = parse(statement, 'USE') assert.parseErr(statement, 'USE')
assert(result.isErr())
}) })
}) })

View file

@ -1,10 +0,0 @@
import { tap } from 'node:test/reporters'
import { run } from 'node:test'
import path from 'node:path'
run({ files: [path.resolve('./tests/query/use.js')] })
.on('test:fail', () => {
process.exitCode = 1
})
.compose(tap)
.pipe(process.stdout)

13
tests/utils.js Normal file
View file

@ -0,0 +1,13 @@
import { KeyValuePair, Name, Label, Identifier, Literal, Edge } from '../src/query/types.js'
export const keyValuePair = ([k, v]) => new KeyValuePair(new Name(new Identifier(k)), new Literal(v))
export const graphObject = (name, label, props = [], Type = Node) => new Type(
name && new Name(new Identifier(name)),
new Label(new Identifier(label)),
props.map(makeKvp)
)
export const makeNode = graphObject
export const makeEdge = (name, label, props) => graphObject(name, label, props, Edge)