From 3aaa48aa59332624089cc76a6e7a5b6c3a0ce774 Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Wed, 13 Nov 2024 13:24:29 -0600 Subject: [PATCH] refactor tests --- src/fn.js | 1 + src/parser.js | 16 +++--- src/query/common.js | 4 +- src/query/match.js | 34 ++++++++----- src/query/return.js | 10 ++-- src/query/types.js | 14 ++--- src/result.js | 31 +++++++++-- tests/assert.js | 31 +++++++++++ tests/query/common.test.js | 43 ++++++++-------- tests/query/match.test.js | 102 ++++++++++++++++++++++++++++++------- tests/query/return.test.js | 33 +++++++----- tests/query/use.test.js | 13 +++-- tests/run.js | 10 ---- tests/utils.js | 13 +++++ 14 files changed, 242 insertions(+), 113 deletions(-) create mode 100644 tests/assert.js delete mode 100644 tests/run.js create mode 100644 tests/utils.js diff --git a/src/fn.js b/src/fn.js index a6e7886..fbb38fd 100644 --- a/src/fn.js +++ b/src/fn.js @@ -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 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 pipe = (...f) => v => f.reduce(apply, v) diff --git a/src/parser.js b/src/parser.js index 6fa4a2b..e5b355d 100644 --- a/src/parser.js +++ b/src/parser.js @@ -77,7 +77,7 @@ export const seq = (...parsers) => state => { let acc = Result.Ok(state) for (const parser of parsers) { if (acc.isOk()) { - acc = parser(acc.value) + acc = parser(acc.unwrap()) } else { break } @@ -100,11 +100,11 @@ export const until = curry((parser, state) => { let acc = Result.Ok(state) while (acc.isOk()) { - const result = parser(clone(acc.value)) + const result = parser(clone(acc.unwrap())) if (result.isOk()) { break } else { - acc = anyChar(acc.value) + acc = anyChar(acc.unwrap()) } } @@ -115,7 +115,7 @@ export const many = curry((parser, state) => { let result = Result.Ok(state) while (true) { - const res = parser(clone(result.value)) + const res = parser(clone(result.unwrap())) if (res.isOk()) { result = res } else { @@ -131,7 +131,7 @@ export const skip = curry((parser, state) => { const result = parser(state) if (result.isOk()) { - return Result.Ok([parsed, result.value[1]]) + return Result.Ok([parsed, result.unwrap()[1]]) } else { return result } @@ -142,11 +142,11 @@ export const map = curry((fn, parser, state) => { if (result.isOk()) { const [parsed] = state - const [, stream] = result.value + const [, stream] = result.unwrap() const backtrack = [parsed, stream] try { - const parsedValue = diff(state, result.value) + const parsedValue = diff(state, result.unwrap()) return succeed(fn(parsedValue), backtrack) } catch (e) { return fail('map failed', state, e) @@ -201,7 +201,7 @@ export const noCaseString = curry((str, state) => ( )) export const maybe = curry((parser, state) => { - const result = parser(state) + const result = parser(clone(state)) return result.isOk() ? result : succeed([], state) }) diff --git a/src/query/common.js b/src/query/common.js index 88ce614..4e87775 100644 --- a/src/query/common.js +++ b/src/query/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, Literal, ObjectPath } from './types.js' +import { Alias, Identifier, Literal, ObjectPath } from './types.js' export const Symbol = 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 signed = seq(maybe(any(Symbol.Plus, Symbol.Hyphen)), number) 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 integer = map(([n]) => parseInt(n, 10), collect(signed)) diff --git a/src/query/match.js b/src/query/match.js index 075b774..71211f2 100644 --- a/src/query/match.js +++ b/src/query/match.js @@ -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 { curry, join } from '../fn.js' -import { alpha, alphanumeric, many, maybe, map, parse, seq, skip, between, noCaseString, separated, until, list } from '../parser.js' +import { curry, is } from '../fn.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( ([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) => ( between(ws, parser, ws, state) )) +const label = map(([x]) => new Label(x), seq(trim(skip(Colon)), identifier)) + const bracketed = curry((value, { Left, Right }, state) => ( between(trim(Left), value, trim(Right), state) )) -const quoted = map(join(''), seq(skip(Quote), until(Quote), skip(Quote))) const kvp = map( ([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) 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( - ([name, label, ...properties]) => new Node(name, label, properties), - bracketed(seq(id, ws, maybe(properties)), Bracket.Round) + makeObj(Node), + bracketed(seq(id, maybe(properties)), Bracket.Round) ) export const edge = map( - ([id]) => new Edge(id), - bracketed(id, Bracket.Square) + makeObj(Edge), + bracketed(seq(id, maybe(properties)), Bracket.Square) ) 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 params = seq(node, many(relationship)) + export const statement = seq(skip(keyword), ws, params) diff --git a/src/query/return.js b/src/query/return.js index 9d70671..4633c79 100644 --- a/src/query/return.js +++ b/src/query/return.js @@ -4,12 +4,10 @@ import { value, Symbol, ws } from './common.js' const keyword = noCaseString('return') -//const params = map( -// values => new ReturnValues(values), -// seq(list(seq(Symbol.Comma, ws), alias)) -//) - -const params = seq(list(seq(ws, Symbol.Comma, ws), value)) +const params = map( + x => new ReturnValues(...x), + seq(list(seq(ws, Symbol.Comma, ws), value)) +) export const statement = seq(skip(keyword), ws, params) diff --git a/src/query/types.js b/src/query/types.js index 031868b..c2057a3 100644 --- a/src/query/types.js +++ b/src/query/types.js @@ -17,7 +17,7 @@ export class SelectedGraph { } class GraphObject { - constructor({ name, label }, properties = []) { + constructor(name, label, properties = []) { this.name = name this.label = label this.properties = properties @@ -25,20 +25,20 @@ class GraphObject { } export class Node extends GraphObject { - constructor(id, properties) { - super(id, properties) + constructor(name, label, properties) { + super(name, label, properties) } } export class Edge extends GraphObject { - constructor(id, properties) { - super(id, properties) + constructor(name, label, properties) { + super(name, label, properties) } } export class ReturnValues extends Array { - constructor(args) { - super(args) + constructor(...args) { + super(...args) } } diff --git a/src/result.js b/src/result.js index fa9cf61..e7d04db 100644 --- a/src/result.js +++ b/src/result.js @@ -1,7 +1,10 @@ export class Result { + #value + #error + constructor(value, error) { - this.value = value - this.error = error + this.#value = value + this.#error = error } static Ok(value) { @@ -13,7 +16,7 @@ export class Result { } isOk() { - return this.error === null + return this.#error === null } isErr() { @@ -22,7 +25,7 @@ export class Result { map(fn) { if (this.isOk()) { - return Result.Ok(fn(this.value)) + return Result.Ok(fn(this.#value)) } else { return this } @@ -30,11 +33,29 @@ export class Result { flatMap(fn) { if (this.isOk()) { - return fn(this.value) + return fn(this.#value) } else { 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 + } +} diff --git a/tests/assert.js b/tests/assert.js new file mode 100644 index 0000000..9bef1db --- /dev/null +++ b/tests/assert.js @@ -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 +} + diff --git a/tests/query/common.test.js b/tests/query/common.test.js index 2d79f31..28b6f27 100644 --- a/tests/query/common.test.js +++ b/tests/query/common.test.js @@ -2,8 +2,9 @@ import { describe, it } from 'node:test' import assert from 'node:assert' import { parse } from '../../src/parser.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', () => { it('literals should match literal types', () => { const tBool = parse(Common.literal, 'true') @@ -16,16 +17,15 @@ describe('common parser library', () => { const negFloat = parse(Common.literal, '-12.01') const string = parse(Common.literal, '"akjsdfuaio"') - const v = r => r.value[0][0].value - assert.strictEqual(v(tBool), true) - assert.strictEqual(v(fBool), false) - assert.strictEqual(v(uint), 5) - assert.strictEqual(v(posInt), 16) - assert.strictEqual(v(negInt), -710) - assert.strictEqual(v(ufloat), 2.1) - assert.strictEqual(v(posFloat), 0.1) - assert.strictEqual(v(negFloat), -12.01) - assert.strictEqual(v(string), 'akjsdfuaio') + assert.deepEqual(v(tBool), new Literal(true)) + assert.deepEqual(v(fBool), new Literal(false)) + assert.deepEqual(v(uint), new Literal(5)) + assert.deepEqual(v(posInt), new Literal(16)) + assert.deepEqual(v(negInt), new Literal(-710)) + assert.deepEqual(v(ufloat), new Literal(2.1)) + assert.deepEqual(v(posFloat), new Literal(0.1)) + assert.deepEqual(v(negFloat), new Literal(-12.01)) + assert.deepEqual(v(string), new Literal('akjsdfuaio')) }) it('value should match literals', () => { @@ -34,29 +34,26 @@ describe('common parser library', () => { const float = parse(Common.baseValue, '0.15') const str = parse(Common.baseValue, '"abc"') - const v = r => r.value[0][0].value - assert.strictEqual(v(bool), false) - assert.strictEqual(v(uint), 11) - assert.strictEqual(v(float), 0.15) - assert.strictEqual(v(str), 'abc') + assert.deepEqual(v(bool), new Literal(false)) + assert.deepEqual(v(uint), new Literal(11)) + assert.deepEqual(v(float), new Literal(0.15)) + assert.deepEqual(v(str), new Literal('abc')) }) it('value should match variables', () => { const identifier = parse(Common.baseValue, 'test') const accessor = parse(Common.baseValue, 'test.value') - const v = r => r.value[0][0] - assert.strictEqual(v(identifier), 'test') - assert.deepEqual(v(accessor), new ObjectPath('test', 'value')) + assert.deepEqual(v(identifier), new Identifier('test')) + assert.deepEqual(v(accessor), new ObjectPath(new Identifier('test'), new Identifier('value'))) }) it('aliases should work i hope', () => { const noAlias = parse(Common.value, 'test') const aliased1 = parse(Common.value, 'crybaby AS cb') const aliased2 = parse(Common.value, 'property.name AS name') - const v = r => r.value[0][0] - assert.strictEqual(v(noAlias), 'test') - assert.deepEqual(v(aliased1), new Alias('crybaby', 'cb')) - assert.deepEqual(v(aliased2), new Alias(new ObjectPath('property', 'name'), 'name')) + assert.deepEqual(v(noAlias), new Identifier('test')) + assert.deepEqual(v(aliased1), new Alias(new Identifier('crybaby'), new Identifier('cb'))) + assert.deepEqual(v(aliased2), new Alias(new ObjectPath(new Identifier('property'), new Identifier('name')), new Identifier('name'))) }) }) diff --git a/tests/query/match.test.js b/tests/query/match.test.js index da2fc45..9989f28 100644 --- a/tests/query/match.test.js +++ b/tests/query/match.test.js @@ -1,21 +1,89 @@ import { describe, it } from 'node:test' -import assert from 'node:assert' -import { parse } from '../../src/parser.js' +import assert from '../assert.js' import { node, edge, properties, statement } from '../../src/query/match.js' +import { makeEdge, makeNode } from '../utils.js' -//describe('match parser', () => { -// it('node should match a label', () => { -// const result = parse(node, '(:Node)') -// console.log(result.error.state) -// assert(result.isOk()) -// }) -// -// it('properties should do something', () => { -// const q = '(a:Node { name: "Rowan", species: "???" })' -// console.log( -// `parsing "${q}"\n`, -// parse(node, q).value[0][0] -// ) -// }) -//}) +describe('node', () => { + it('should match a label with an empty name', () => { + assert.parseOk(node, '(:Node)', actual => { + const expected = makeNode(undefined, 'Node') + assert.deepEqual(actual, expected) + }) + }) + + it('should match a name and label', () => { + assert.parseOk(node, '(node:Node)', actual => { + const expected = makeNode('node', 'Node') + 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) + }) + }) +}) diff --git a/tests/query/return.test.js b/tests/query/return.test.js index b6e8b57..df77021 100644 --- a/tests/query/return.test.js +++ b/tests/query/return.test.js @@ -1,23 +1,28 @@ import { describe, it } from 'node:test' -import assert from 'node:assert' -import { parse } from '../../src/parser.js' +import assert from '../assert.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', () => { it('should collect a single value for a query to return', () => { - const result = parse(statement, 'RETURN folklore AS f') - assert(result.isOk()) - const [[selected]] = result.value - assert.deepStrictEqual(selected, new Alias('folklore', 'f')) + assert.parseOk(statement, 'RETURN folklore AS f', actual => { + assert.deepEqual(actual, new Alias(new Identifier('folklore'), new Identifier('f'))) + }) }) - //it('should collect multiple values for a query to return', () => { - // const result = parse(statement, 'RETURN sybil, mercury, rowan') - // console.log(result.error.state[0]) - // assert(result.isOk()) - // const [[selected]] = result.value - // assert.deepStrictEqual(selected, ['sybil', 'mercury', 'rowan']) - //}) + it('should collect multiple values for a query to return', () => { + assert.parseOk(statement, 'RETURN sybil, mercury, rowan', actual => { + assert.deepStrictEqual(actual, ['sybil', 'mercury', 'rowan'].map(x => new Identifier(x))) + }) + }) + + 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]) + }) + }) }) diff --git a/tests/query/use.test.js b/tests/query/use.test.js index 7c3cd0a..9cad181 100644 --- a/tests/query/use.test.js +++ b/tests/query/use.test.js @@ -1,19 +1,18 @@ 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/use.js' +import { Identifier } from '../../src/query/types.js' describe('use parser', () => { it('should select a graph to query', () => { - const result = parse(statement, 'USE default') - assert(result.isOk()) - const [[selected]] = result.value - assert.strictEqual(selected.identifier, 'default') + assert.parseOk(statement, 'USE default', actual => { + assert.deepEqual(actual.identifier, new Identifier('default')) + }) }) it('should return an error if no graph identifier is provided', () => { - const result = parse(statement, 'USE') - assert(result.isErr()) + assert.parseErr(statement, 'USE') }) }) diff --git a/tests/run.js b/tests/run.js deleted file mode 100644 index 2261065..0000000 --- a/tests/run.js +++ /dev/null @@ -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) diff --git a/tests/utils.js b/tests/utils.js new file mode 100644 index 0000000..3fc47c5 --- /dev/null +++ b/tests/utils.js @@ -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) +